1use alloy_primitives::hex;
2use clap::Parser;
3use comfy_table::{Table, modifiers::UTF8_ROUND_CORNERS, presets::ASCII_MARKDOWN};
4use eyre::Result;
5use foundry_cli::{
6 opts::{BuildOpts, CompilerOpts, ProjectPathOpts},
7 utils::{FoundryPathExt, cache_local_signatures, cache_signatures_from_abis},
8};
9use foundry_common::{
10 compile::{PathOrContractInfo, ProjectCompiler, compile_target},
11 selectors::{SelectorImportData, import_selectors},
12 shell,
13};
14use foundry_compilers::{artifacts::output_selection::ContractOutputSelection, info::ContractInfo};
15use std::{collections::BTreeMap, 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 #[arg(long, help = "Do not group the selectors by contract in separate tables.")]
62 no_group: bool,
63 },
64
65 #[command(visible_alias = "f")]
67 Find {
68 #[arg(help = "The selector to search for (with or without 0x prefix)")]
70 selector: String,
71
72 #[command(flatten)]
73 project_paths: ProjectPathOpts,
74 },
75
76 #[command(visible_alias = "c")]
78 Cache {
79 #[arg(long, help = "Path to a folder containing additional abis to include in the cache")]
80 extra_abis_path: Option<String>,
81 #[command(flatten)]
82 project_paths: ProjectPathOpts,
83 },
84}
85
86impl SelectorsSubcommands {
87 pub async fn run(self) -> Result<()> {
88 match self {
89 Self::Cache { project_paths, extra_abis_path } => {
90 if let Some(extra_abis_path) = extra_abis_path {
91 sh_println!("Caching selectors for ABIs at {extra_abis_path}")?;
92 cache_signatures_from_abis(extra_abis_path)?;
93 }
94
95 sh_println!("Caching selectors for contracts in the project...")?;
96 let build_args = BuildOpts {
97 project_paths,
98 compiler: CompilerOpts {
99 extra_output: vec![ContractOutputSelection::Abi],
100 ..Default::default()
101 },
102 ..Default::default()
103 };
104
105 let project = build_args.project()?;
107 let outcome = ProjectCompiler::new().quiet(true).compile(&project)?;
108 cache_local_signatures(&outcome)?;
109 }
110 Self::Upload { contract, all, project_paths } => {
111 let build_args = BuildOpts {
112 project_paths: project_paths.clone(),
113 compiler: CompilerOpts {
114 extra_output: vec![ContractOutputSelection::Abi],
115 ..Default::default()
116 },
117 ..Default::default()
118 };
119
120 let project = build_args.project()?;
121 let output = if let Some(contract_info) = &contract {
122 let Some(contract_name) = contract_info.name() else {
123 eyre::bail!("No contract name provided.")
124 };
125
126 let target_path = contract_info
127 .path()
128 .map(Ok)
129 .unwrap_or_else(|| project.find_contract_path(contract_name))?;
130 compile_target(&target_path, &project, false)?
131 } else {
132 ProjectCompiler::new().compile(&project)?
133 };
134 let artifacts = if all {
135 output
136 .into_artifacts_with_files()
137 .filter(|(file, _, _)| {
138 let is_sources_path = file.starts_with(&project.paths.sources);
139 let is_test = file.is_sol_test();
140
141 is_sources_path && !is_test
142 })
143 .map(|(_, contract, artifact)| (contract, artifact))
144 .collect()
145 } else {
146 let contract_info = contract.unwrap();
147 let contract = contract_info.name().unwrap().to_string();
148
149 let found_artifact = if let Some(path) = contract_info.path() {
150 output.find(project.root().join(path).as_path(), &contract)
151 } else {
152 output.find_first(&contract)
153 };
154
155 let artifact = found_artifact
156 .ok_or_else(|| {
157 eyre::eyre!(
158 "Could not find artifact `{contract}` in the compiled artifacts"
159 )
160 })?
161 .clone();
162 vec![(contract, artifact)]
163 };
164
165 let mut artifacts = artifacts.into_iter().peekable();
166 while let Some((contract, artifact)) = artifacts.next() {
167 let abi = artifact.abi.ok_or_else(|| eyre::eyre!("Unable to fetch abi"))?;
168 if abi.functions.is_empty() && abi.events.is_empty() && abi.errors.is_empty() {
169 continue;
170 }
171
172 sh_println!("Uploading selectors for {contract}...")?;
173
174 import_selectors(SelectorImportData::Abi(vec![abi])).await?.describe();
176
177 if artifacts.peek().is_some() {
178 sh_println!()?
179 }
180 }
181 }
182 Self::Collision { mut first_contract, mut second_contract, build } => {
183 let project = build.project()?;
185 let mut compiler = ProjectCompiler::new().quiet(true);
186
187 if let Some(contract_path) = &mut first_contract.path {
188 let target_path = canonicalize(&*contract_path)?;
189 *contract_path = target_path.to_string_lossy().to_string();
190 compiler = compiler.files([target_path]);
191 }
192 if let Some(contract_path) = &mut second_contract.path {
193 let target_path = canonicalize(&*contract_path)?;
194 *contract_path = target_path.to_string_lossy().to_string();
195 compiler = compiler.files([target_path]);
196 }
197
198 let output = compiler.compile(&project)?;
199
200 let methods = |contract: &ContractInfo| -> eyre::Result<_> {
202 let artifact = output
203 .find_contract(contract)
204 .ok_or_else(|| eyre::eyre!("Could not find artifact for {contract}"))?;
205 artifact.method_identifiers.as_ref().ok_or_else(|| {
206 eyre::eyre!("Could not find method identifiers for {contract}")
207 })
208 };
209 let first_method_map = methods(&first_contract)?;
210 let second_method_map = methods(&second_contract)?;
211
212 let colliding_methods: Vec<(&String, &String, &String)> = first_method_map
213 .iter()
214 .filter_map(|(k1, v1)| {
215 second_method_map
216 .iter()
217 .find_map(|(k2, v2)| if **v2 == *v1 { Some((k2, v2)) } else { None })
218 .map(|(k2, v2)| (v2, k1, k2))
219 })
220 .collect();
221
222 if colliding_methods.is_empty() {
223 sh_println!("No colliding method selectors between the two contracts.")?;
224 } else {
225 let mut table = Table::new();
226 if shell::is_markdown() {
227 table.load_preset(ASCII_MARKDOWN);
228 } else {
229 table.apply_modifier(UTF8_ROUND_CORNERS);
230 }
231 table.set_header([
232 String::from("Selector"),
233 first_contract.name,
234 second_contract.name,
235 ]);
236 for method in &colliding_methods {
237 table.add_row([method.0, method.1, method.2]);
238 }
239 sh_println!("{} collisions found:", colliding_methods.len())?;
240 sh_println!("\n{table}\n")?;
241 }
242 }
243 Self::List { contract, project_paths, no_group } => {
244 sh_println!("Listing selectors for contracts in the project...")?;
245 let build_args = BuildOpts {
246 project_paths,
247 compiler: CompilerOpts {
248 extra_output: vec![ContractOutputSelection::Abi],
249 ..Default::default()
250 },
251 ..Default::default()
252 };
253
254 let project = build_args.project()?;
256 let outcome = ProjectCompiler::new().quiet(true).compile(&project)?;
257 let artifacts = if let Some(contract) = contract {
258 let found_artifact = outcome.find_first(&contract);
259 let artifact = found_artifact
260 .ok_or_else(|| {
261 let candidates = outcome
262 .artifacts()
263 .map(|(name, _,)| name)
264 .collect::<Vec<_>>();
265 let suggestion = if let Some(suggestion) = foundry_cli::utils::did_you_mean(&contract, candidates).pop() {
266 format!("\nDid you mean `{suggestion}`?")
267 } else {
268 String::new()
269 };
270 eyre::eyre!(
271 "Could not find artifact `{contract}` in the compiled artifacts{suggestion}",
272 )
273 })?
274 .clone();
275 vec![(contract, artifact)]
276 } else {
277 outcome
278 .into_artifacts_with_files()
279 .filter(|(file, _, _)| {
280 let is_sources_path = file.starts_with(&project.paths.sources);
281 let is_test = file.is_sol_test();
282
283 is_sources_path && !is_test
284 })
285 .map(|(_, contract, artifact)| (contract, artifact))
286 .collect()
287 };
288
289 let mut artifacts = artifacts.into_iter().peekable();
290
291 #[derive(PartialEq, PartialOrd, Eq, Ord)]
292 enum SelectorType {
293 Function,
294 Event,
295 Error,
296 }
297 impl std::fmt::Display for SelectorType {
298 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
299 match self {
300 Self::Function => write!(f, "Function"),
301 Self::Event => write!(f, "Event"),
302 Self::Error => write!(f, "Error"),
303 }
304 }
305 }
306
307 let mut selectors =
308 BTreeMap::<String, BTreeMap<SelectorType, Vec<(String, String)>>>::new();
309
310 for (contract, artifact) in artifacts.by_ref() {
311 let abi = artifact.abi.ok_or_else(|| eyre::eyre!("Unable to fetch abi"))?;
312
313 let contract_selectors = selectors.entry(contract.clone()).or_default();
314
315 for func in abi.functions() {
316 let sig = func.signature();
317 let selector = func.selector();
318 contract_selectors
319 .entry(SelectorType::Function)
320 .or_default()
321 .push((hex::encode_prefixed(selector), sig));
322 }
323
324 for event in abi.events() {
325 let sig = event.signature();
326 let selector = event.selector();
327 contract_selectors
328 .entry(SelectorType::Event)
329 .or_default()
330 .push((hex::encode_prefixed(selector), sig));
331 }
332
333 for error in abi.errors() {
334 let sig = error.signature();
335 let selector = error.selector();
336 contract_selectors
337 .entry(SelectorType::Error)
338 .or_default()
339 .push((hex::encode_prefixed(selector), sig));
340 }
341 }
342
343 if no_group {
344 let mut table = Table::new();
345 if shell::is_markdown() {
346 table.load_preset(ASCII_MARKDOWN);
347 } else {
348 table.apply_modifier(UTF8_ROUND_CORNERS);
349 }
350 table.set_header(["Type", "Signature", "Selector", "Contract"]);
351
352 for (contract, contract_selectors) in selectors {
353 for (selector_type, selectors) in contract_selectors {
354 for (selector, sig) in selectors {
355 table.add_row([
356 selector_type.to_string(),
357 sig,
358 selector,
359 contract.to_string(),
360 ]);
361 }
362 }
363 }
364
365 sh_println!("\n{table}")?;
366 } else {
367 for (idx, (contract, contract_selectors)) in selectors.into_iter().enumerate() {
368 sh_println!("{}{contract}", if idx == 0 { "" } else { "\n" })?;
369 let mut table = Table::new();
370 if shell::is_markdown() {
371 table.load_preset(ASCII_MARKDOWN);
372 } else {
373 table.apply_modifier(UTF8_ROUND_CORNERS);
374 }
375 table.set_header(["Type", "Signature", "Selector"]);
376
377 for (selector_type, selectors) in contract_selectors {
378 for (selector, sig) in selectors {
379 table.add_row([selector_type.to_string(), sig, selector]);
380 }
381 }
382 sh_println!("\n{table}")?;
383 }
384 }
385 }
386
387 Self::Find { selector, project_paths } => {
388 sh_println!("Searching for selector {selector:?} in the project...")?;
389
390 let build_args = BuildOpts {
391 project_paths,
392 compiler: CompilerOpts {
393 extra_output: vec![ContractOutputSelection::Abi],
394 ..Default::default()
395 },
396 ..Default::default()
397 };
398
399 let project = build_args.project()?;
400 let outcome = ProjectCompiler::new().quiet(true).compile(&project)?;
401 let artifacts = outcome
402 .into_artifacts_with_files()
403 .filter(|(file, _, _)| {
404 let is_sources_path = file.starts_with(&project.paths.sources);
405 let is_test = file.is_sol_test();
406 is_sources_path && !is_test
407 })
408 .collect::<Vec<_>>();
409
410 let mut table = Table::new();
411 if shell::is_markdown() {
412 table.load_preset(ASCII_MARKDOWN);
413 } else {
414 table.apply_modifier(UTF8_ROUND_CORNERS);
415 }
416
417 table.set_header(["Type", "Signature", "Selector", "Contract"]);
418
419 for (_file, contract, artifact) in artifacts {
420 let abi = artifact.abi.ok_or_else(|| eyre::eyre!("Unable to fetch abi"))?;
421
422 let selector_bytes =
423 hex::decode(selector.strip_prefix("0x").unwrap_or(&selector))?;
424
425 for func in abi.functions() {
426 if func.selector().as_slice().starts_with(selector_bytes.as_slice()) {
427 table.add_row([
428 "Function",
429 &func.signature(),
430 &hex::encode_prefixed(func.selector()),
431 contract.as_str(),
432 ]);
433 }
434 }
435
436 for event in abi.events() {
437 if event.selector().as_slice().starts_with(selector_bytes.as_slice()) {
438 table.add_row([
439 "Event",
440 &event.signature(),
441 &hex::encode_prefixed(event.selector()),
442 contract.as_str(),
443 ]);
444 }
445 }
446
447 for error in abi.errors() {
448 if error.selector().as_slice().starts_with(selector_bytes.as_slice()) {
449 table.add_row([
450 "Error",
451 &error.signature(),
452 &hex::encode_prefixed(error.selector()),
453 contract.as_str(),
454 ]);
455 }
456 }
457 }
458
459 if table.row_count() > 0 {
460 sh_println!("\nFound {} instance(s)...", table.row_count())?;
461 sh_println!("\n{table}\n")?;
462 } else {
463 return Err(eyre::eyre!("\nSelector not found in the project."));
464 }
465 }
466 }
467 Ok(())
468 }
469}