1use alloy_primitives::hex;
2use clap::Parser;
3use comfy_table::{modifiers::UTF8_ROUND_CORNERS, Table};
4use eyre::Result;
5use foundry_cli::{
6 opts::{BuildOpts, CompilerOpts, ProjectPathOpts},
7 utils::{cache_local_signatures, FoundryPathExt},
8};
9use foundry_common::{
10 compile::{compile_target, PathOrContractInfo, ProjectCompiler},
11 selectors::{import_selectors, SelectorImportData},
12};
13use foundry_compilers::{artifacts::output_selection::ContractOutputSelection, info::ContractInfo};
14use foundry_config::Config;
15use std::fs::canonicalize;
16
17#[derive(Clone, Debug, Parser)]
19pub enum SelectorsSubcommands {
20 #[command(visible_alias = "co")]
22 Collision {
23 first_contract: ContractInfo,
26
27 second_contract: ContractInfo,
30
31 #[command(flatten)]
32 build: Box<BuildOpts>,
33 },
34
35 #[command(visible_alias = "up")]
37 Upload {
38 #[arg(required_unless_present = "all")]
41 contract: Option<PathOrContractInfo>,
42
43 #[arg(long, required_unless_present = "contract")]
45 all: bool,
46
47 #[command(flatten)]
48 project_paths: ProjectPathOpts,
49 },
50
51 #[command(visible_alias = "ls")]
53 List {
54 #[arg(help = "The name of the contract to list selectors for.")]
56 contract: Option<String>,
57
58 #[command(flatten)]
59 project_paths: ProjectPathOpts,
60 },
61
62 #[command(visible_alias = "f")]
64 Find {
65 #[arg(help = "The selector to search for (with or without 0x prefix)")]
67 selector: String,
68
69 #[command(flatten)]
70 project_paths: ProjectPathOpts,
71 },
72
73 #[command(visible_alias = "c")]
75 Cache {
76 #[command(flatten)]
77 project_paths: ProjectPathOpts,
78 },
79}
80
81impl SelectorsSubcommands {
82 pub async fn run(self) -> Result<()> {
83 match self {
84 Self::Cache { project_paths } => {
85 sh_println!("Caching selectors for contracts in the project...")?;
86 let build_args = BuildOpts {
87 project_paths,
88 compiler: CompilerOpts {
89 extra_output: vec![ContractOutputSelection::Abi],
90 ..Default::default()
91 },
92 ..Default::default()
93 };
94
95 let project = build_args.project()?;
97 let outcome = ProjectCompiler::new().quiet(true).compile(&project)?;
98 cache_local_signatures(&outcome, Config::foundry_cache_dir().unwrap())?
99 }
100 Self::Upload { contract, all, project_paths } => {
101 let build_args = BuildOpts {
102 project_paths: project_paths.clone(),
103 compiler: CompilerOpts {
104 extra_output: vec![ContractOutputSelection::Abi],
105 ..Default::default()
106 },
107 ..Default::default()
108 };
109
110 let project = build_args.project()?;
111 let output = if let Some(contract_info) = &contract {
112 let Some(contract_name) = contract_info.name() else {
113 eyre::bail!("No contract name provided.")
114 };
115
116 let target_path = contract_info
117 .path()
118 .map(Ok)
119 .unwrap_or_else(|| project.find_contract_path(contract_name))?;
120 compile_target(&target_path, &project, false)?
121 } else {
122 ProjectCompiler::new().compile(&project)?
123 };
124 let artifacts = if all {
125 output
126 .into_artifacts_with_files()
127 .filter(|(file, _, _)| {
128 let is_sources_path = file.starts_with(&project.paths.sources);
129 let is_test = file.is_sol_test();
130
131 is_sources_path && !is_test
132 })
133 .map(|(_, contract, artifact)| (contract, artifact))
134 .collect()
135 } else {
136 let contract_info = contract.unwrap();
137 let contract = contract_info.name().unwrap().to_string();
138
139 let found_artifact = if let Some(path) = contract_info.path() {
140 output.find(project.root().join(path).as_path(), &contract)
141 } else {
142 output.find_first(&contract)
143 };
144
145 let artifact = found_artifact
146 .ok_or_else(|| {
147 eyre::eyre!(
148 "Could not find artifact `{contract}` in the compiled artifacts"
149 )
150 })?
151 .clone();
152 vec![(contract, artifact)]
153 };
154
155 let mut artifacts = artifacts.into_iter().peekable();
156 while let Some((contract, artifact)) = artifacts.next() {
157 let abi = artifact.abi.ok_or_else(|| eyre::eyre!("Unable to fetch abi"))?;
158 if abi.functions.is_empty() && abi.events.is_empty() && abi.errors.is_empty() {
159 continue
160 }
161
162 sh_println!("Uploading selectors for {contract}...")?;
163
164 import_selectors(SelectorImportData::Abi(vec![abi])).await?.describe();
166
167 if artifacts.peek().is_some() {
168 sh_println!()?
169 }
170 }
171 }
172 Self::Collision { mut first_contract, mut second_contract, build } => {
173 let project = build.project()?;
175 let mut compiler = ProjectCompiler::new().quiet(true);
176
177 if let Some(contract_path) = &mut first_contract.path {
178 let target_path = canonicalize(&*contract_path)?;
179 *contract_path = target_path.to_string_lossy().to_string();
180 compiler = compiler.files([target_path]);
181 }
182 if let Some(contract_path) = &mut second_contract.path {
183 let target_path = canonicalize(&*contract_path)?;
184 *contract_path = target_path.to_string_lossy().to_string();
185 compiler = compiler.files([target_path]);
186 }
187
188 let output = compiler.compile(&project)?;
189
190 let methods = |contract: &ContractInfo| -> eyre::Result<_> {
192 let artifact = output
193 .find_contract(contract)
194 .ok_or_else(|| eyre::eyre!("Could not find artifact for {contract}"))?;
195 artifact.method_identifiers.as_ref().ok_or_else(|| {
196 eyre::eyre!("Could not find method identifiers for {contract}")
197 })
198 };
199 let first_method_map = methods(&first_contract)?;
200 let second_method_map = methods(&second_contract)?;
201
202 let colliding_methods: Vec<(&String, &String, &String)> = first_method_map
203 .iter()
204 .filter_map(|(k1, v1)| {
205 second_method_map
206 .iter()
207 .find_map(|(k2, v2)| if **v2 == *v1 { Some((k2, v2)) } else { None })
208 .map(|(k2, v2)| (v2, k1, k2))
209 })
210 .collect();
211
212 if colliding_methods.is_empty() {
213 sh_println!("No colliding method selectors between the two contracts.")?;
214 } else {
215 let mut table = Table::new();
216 table.apply_modifier(UTF8_ROUND_CORNERS);
217 table.set_header([
218 String::from("Selector"),
219 first_contract.name,
220 second_contract.name,
221 ]);
222 for method in &colliding_methods {
223 table.add_row([method.0, method.1, method.2]);
224 }
225 sh_println!("{} collisions found:", colliding_methods.len())?;
226 sh_println!("\n{table}\n")?;
227 }
228 }
229 Self::List { contract, project_paths } => {
230 sh_println!("Listing selectors for contracts in the project...")?;
231 let build_args = BuildOpts {
232 project_paths,
233 compiler: CompilerOpts {
234 extra_output: vec![ContractOutputSelection::Abi],
235 ..Default::default()
236 },
237 ..Default::default()
238 };
239
240 let project = build_args.project()?;
242 let outcome = ProjectCompiler::new().quiet(true).compile(&project)?;
243 let artifacts = if let Some(contract) = contract {
244 let found_artifact = outcome.find_first(&contract);
245 let artifact = found_artifact
246 .ok_or_else(|| {
247 let candidates = outcome
248 .artifacts()
249 .map(|(name, _,)| name)
250 .collect::<Vec<_>>();
251 let suggestion = if let Some(suggestion) = foundry_cli::utils::did_you_mean(&contract, candidates).pop() {
252 format!("\nDid you mean `{suggestion}`?")
253 } else {
254 String::new()
255 };
256 eyre::eyre!(
257 "Could not find artifact `{contract}` in the compiled artifacts{suggestion}",
258 )
259 })?
260 .clone();
261 vec![(contract, artifact)]
262 } else {
263 outcome
264 .into_artifacts_with_files()
265 .filter(|(file, _, _)| {
266 let is_sources_path = file.starts_with(&project.paths.sources);
267 let is_test = file.is_sol_test();
268
269 is_sources_path && !is_test
270 })
271 .map(|(_, contract, artifact)| (contract, artifact))
272 .collect()
273 };
274
275 let mut artifacts = artifacts.into_iter().peekable();
276
277 while let Some((contract, artifact)) = artifacts.next() {
278 let abi = artifact.abi.ok_or_else(|| eyre::eyre!("Unable to fetch abi"))?;
279 if abi.functions.is_empty() && abi.events.is_empty() && abi.errors.is_empty() {
280 continue
281 }
282
283 sh_println!("{contract}")?;
284
285 let mut table = Table::new();
286 table.apply_modifier(UTF8_ROUND_CORNERS);
287
288 table.set_header(["Type", "Signature", "Selector"]);
289
290 for func in abi.functions() {
291 let sig = func.signature();
292 let selector = func.selector();
293 table.add_row(["Function", &sig, &hex::encode_prefixed(selector)]);
294 }
295
296 for event in abi.events() {
297 let sig = event.signature();
298 let selector = event.selector();
299 table.add_row(["Event", &sig, &hex::encode_prefixed(selector)]);
300 }
301
302 for error in abi.errors() {
303 let sig = error.signature();
304 let selector = error.selector();
305 table.add_row(["Error", &sig, &hex::encode_prefixed(selector)]);
306 }
307
308 sh_println!("\n{table}\n")?;
309
310 if artifacts.peek().is_some() {
311 sh_println!()?
312 }
313 }
314 }
315
316 Self::Find { selector, project_paths } => {
317 sh_println!("Searching for selector {selector:?} in the project...")?;
318
319 let build_args = BuildOpts {
320 project_paths,
321 compiler: CompilerOpts {
322 extra_output: vec![ContractOutputSelection::Abi],
323 ..Default::default()
324 },
325 ..Default::default()
326 };
327
328 let project = build_args.project()?;
329 let outcome = ProjectCompiler::new().quiet(true).compile(&project)?;
330 let artifacts = outcome
331 .into_artifacts_with_files()
332 .filter(|(file, _, _)| {
333 let is_sources_path = file.starts_with(&project.paths.sources);
334 let is_test = file.is_sol_test();
335 is_sources_path && !is_test
336 })
337 .collect::<Vec<_>>();
338
339 let mut table = Table::new();
340 table.apply_modifier(UTF8_ROUND_CORNERS);
341
342 table.set_header(["Type", "Signature", "Selector", "Contract"]);
343
344 for (_file, contract, artifact) in artifacts {
345 let abi = artifact.abi.ok_or_else(|| eyre::eyre!("Unable to fetch abi"))?;
346
347 let selector_bytes =
348 hex::decode(selector.strip_prefix("0x").unwrap_or(&selector))?;
349
350 for func in abi.functions() {
351 if func.selector().as_slice().starts_with(selector_bytes.as_slice()) {
352 table.add_row([
353 "Function",
354 &func.signature(),
355 &hex::encode_prefixed(func.selector()),
356 contract.as_str(),
357 ]);
358 }
359 }
360
361 for event in abi.events() {
362 if event.selector().as_slice().starts_with(selector_bytes.as_slice()) {
363 table.add_row([
364 "Event",
365 &event.signature(),
366 &hex::encode_prefixed(event.selector()),
367 contract.as_str(),
368 ]);
369 }
370 }
371
372 for error in abi.errors() {
373 if error.selector().as_slice().starts_with(selector_bytes.as_slice()) {
374 table.add_row([
375 "Error",
376 &error.signature(),
377 &hex::encode_prefixed(error.selector()),
378 contract.as_str(),
379 ]);
380 }
381 }
382 }
383
384 if table.row_count() > 0 {
385 sh_println!("\nFound {} instance(s)...", table.row_count())?;
386 sh_println!("\n{table}\n")?;
387 } else {
388 return Err(eyre::eyre!("\nSelector not found in the project."));
389 }
390 }
391 }
392 Ok(())
393 }
394}