1use alloy_primitives::hex;
2use clap::Parser;
3use comfy_table::{
4 Table,
5 presets::{ASCII_FULL, ASCII_MARKDOWN},
6};
7use eyre::Result;
8use foundry_cli::{
9 opts::{BuildOpts, ProjectPathOpts},
10 utils::{FoundryPathExt, LoadConfig, cache_local_signatures, cache_signatures_from_abis},
11};
12use foundry_common::{
13 compile::{
14 PathOrContractInfo, ProjectCompiler, compile_abi_project, compile_abi_project_cached,
15 },
16 external_compiler::is_external_artifact,
17 fs::canonicalize_path,
18 selectors::{SelectorImportData, import_selectors},
19 shell,
20};
21use foundry_compilers::{
22 Project, ProjectCompileOutput,
23 artifacts::{
24 ConfigurableContractArtifact,
25 output_selection::{ContractOutputSelection, EvmOutputSelection, OutputSelection},
26 },
27 info::ContractInfo,
28 multi::MultiCompiler,
29};
30use std::{collections::BTreeMap, path::Path};
31
32#[derive(Clone, Debug, Parser)]
34pub enum SelectorsSubcommands {
35 #[command(visible_alias = "co")]
37 Collision {
38 first_contract: ContractInfo,
41
42 second_contract: ContractInfo,
45
46 #[command(flatten)]
47 build: Box<BuildOpts>,
48 },
49
50 #[command(visible_alias = "up")]
52 Upload {
53 #[arg(required_unless_present = "all")]
56 contract: Option<PathOrContractInfo>,
57
58 #[arg(long, required_unless_present = "contract")]
60 all: bool,
61
62 #[command(flatten)]
63 project_paths: ProjectPathOpts,
64 },
65
66 #[command(visible_alias = "ls")]
68 List {
69 #[arg(help = "The name of the contract to list selectors for.")]
71 contract: Option<String>,
72
73 #[command(flatten)]
74 project_paths: ProjectPathOpts,
75
76 #[arg(long, help = "Do not group the selectors by contract in separate tables.")]
77 no_group: bool,
78 },
79
80 #[command(visible_alias = "f")]
82 Find {
83 #[arg(help = "The selector to search for (with or without 0x prefix)")]
85 selector: String,
86
87 #[command(flatten)]
88 project_paths: ProjectPathOpts,
89 },
90
91 #[command(visible_alias = "c")]
93 Cache {
94 #[arg(long, help = "Path to a folder containing additional abis to include in the cache")]
95 extra_abis_path: Option<String>,
96 #[command(flatten)]
97 project_paths: ProjectPathOpts,
98 },
99}
100
101impl SelectorsSubcommands {
102 pub async fn run(self) -> Result<()> {
103 match self {
104 Self::Cache { project_paths, extra_abis_path } => {
105 if let Some(extra_abis_path) = extra_abis_path {
106 sh_status!("Caching selectors for ABIs at {extra_abis_path}")?;
107 cache_signatures_from_abis(extra_abis_path)?;
108 }
109
110 sh_status!("Caching selectors for contracts in the project...")?;
111 let (mut project, compiler) = project_from_paths(project_paths)?;
112 let outcome = compile_abi_project(&mut project, compiler.quiet(true))?;
113 cache_local_signatures(&outcome)?;
114 }
115 Self::Upload { contract, all, project_paths } => {
116 let (mut project, compiler) = project_from_paths(project_paths)?;
117 let output = if let Some(contract_info) = &contract {
118 let Some(contract_name) = contract_info.name() else {
119 eyre::bail!("No contract name provided.");
120 };
121
122 let target_path = contract_info
123 .path()
124 .map(Ok)
125 .unwrap_or_else(|| project.find_contract_path(contract_name))?;
126 compile_abi_project(&mut project, compiler.target_files([target_path]))?
127 } else {
128 compile_abi_project(&mut project, compiler)?
129 };
130 let artifacts = if all {
131 selector_artifacts(output, &project.paths.sources)
132 } else {
133 let contract_info = contract.unwrap();
134 let contract = contract_info.name().unwrap().to_string();
135
136 let found_artifact = if let Some(path) = contract_info.path() {
137 output.find(project.root().join(path).as_path(), &contract)
138 } else {
139 output.find_first(&contract)
140 };
141
142 let artifact = found_artifact
143 .ok_or_else(|| {
144 eyre::eyre!(
145 "Could not find artifact `{contract}` in the compiled artifacts"
146 )
147 })?
148 .clone();
149 vec![(contract, artifact)]
150 };
151
152 let mut abis = Vec::with_capacity(artifacts.len());
153 for (contract, artifact) in artifacts {
154 let abi = artifact.abi.ok_or_else(|| eyre::eyre!("Unable to fetch abi"))?;
155 if abi.functions.is_empty() && abi.events.is_empty() && abi.errors.is_empty() {
156 continue;
157 }
158
159 sh_status!("Uploading selectors for {contract}...")?;
160 abis.push(abi);
161 }
162 if !abis.is_empty() {
163 import_selectors(SelectorImportData::Abi(abis)).await?.describe();
164 }
165 }
166 Self::Collision { mut first_contract, mut second_contract, build } => {
167 let user_extra_output = !build.compiler.extra_output.is_empty()
169 || !build.compiler.extra_output_files.is_empty();
170 let config = build.load_config_with_dependencies()?;
171 let mut project = config.project()?;
172 if !user_extra_output && !project.build_info {
173 project.no_artifacts = true;
174 project.update_output_selection(|selection| {
175 *selection = OutputSelection::common_output_selection([
176 ContractOutputSelection::Evm(EvmOutputSelection::MethodIdentifiers)
177 .to_string(),
178 ]);
179 });
180 }
181 let mut compiler = ProjectCompiler::new()
182 .external_compilers(&config)
183 .external_artifacts(false)
184 .quiet(true);
185 if project.no_artifacts
186 && project.cached
187 && !config.force
188 && project.artifacts.additional_files == Default::default()
189 && !config.deny.warnings()
191 && [&first_contract, &second_contract].into_iter().all(|contract| {
193 contract.path.is_some()
194 || (config.external_compilers.is_empty()
195 && project.find_contract_path(&contract.name).is_ok())
196 })
197 {
198 project.no_artifacts = false;
199 project.update_output_selection(|selection| {
200 *selection =
201 std::mem::take(selection).with_output("*", "*", ["abi".to_string()]);
202 });
203 compiler = compiler.cache_abi();
204 }
205
206 if let Some(contract_path) = &mut first_contract.path {
207 let target_path = canonicalize_path(&*contract_path)?;
208 *contract_path = target_path.to_string_lossy().to_string();
209 compiler = compiler.target_files([target_path]);
210 }
211 if let Some(contract_path) = &mut second_contract.path {
212 let target_path = canonicalize_path(&*contract_path)?;
213 *contract_path = target_path.to_string_lossy().to_string();
214 compiler = compiler.target_files([target_path]);
215 }
216
217 let output = compiler.compile(&project)?;
218
219 let methods = |contract: &ContractInfo| -> eyre::Result<_> {
221 let artifact = output
222 .find_contract(contract)
223 .ok_or_else(|| eyre::eyre!("Could not find artifact for {contract}"))?;
224 artifact.method_identifiers.as_ref().ok_or_else(|| {
225 eyre::eyre!("Could not find method identifiers for {contract}")
226 })
227 };
228 let first_method_map = methods(&first_contract)?;
229 let second_method_map = methods(&second_contract)?;
230
231 let colliding_methods: Vec<(&String, &String, &String)> = first_method_map
232 .iter()
233 .filter_map(|(k1, v1)| {
234 second_method_map
235 .iter()
236 .find_map(|(k2, v2)| (**v2 == *v1).then_some((k2, v2)))
237 .map(|(k2, v2)| (v2, k1, k2))
238 })
239 .collect();
240
241 if colliding_methods.is_empty() {
242 sh_println!("No colliding method selectors between the two contracts.")?;
243 } else {
244 let mut table = Table::new();
245 if shell::is_markdown() {
246 table.load_style(ASCII_MARKDOWN);
247 } else {
248 table.load_style(ASCII_FULL.with_rounded_corners());
249 }
250 table.set_header([
251 String::from("Selector"),
252 first_contract.name,
253 second_contract.name,
254 ]);
255 for method in &colliding_methods {
256 #[allow(clippy::tuple_array_conversions)]
257 table.add_row(<[_; 3]>::from(*method));
258 }
259 sh_println!("{} collisions found:", colliding_methods.len())?;
260 sh_println!("\n{table}\n")?;
261 }
262 }
263 Self::List { contract, project_paths, no_group } => {
264 sh_status!("Listing selectors for contracts in the project...")?;
265 let (mut project, compiler) = project_from_paths(project_paths)?;
266 let target_path = contract
267 .as_ref()
268 .filter(|_| project.no_artifacts)
269 .and_then(|contract| project.find_contract_path(contract).ok());
270 let compiler = if let Some(target_path) = target_path {
271 compiler.target_files([target_path])
272 } else {
273 compiler
274 };
275 let outcome = compile_abi_project(&mut project, compiler.quiet(true))?;
276 let artifacts = if let Some(contract) = contract {
277 let found_artifact = outcome.find_first(&contract);
278 let artifact = found_artifact
279 .ok_or_else(|| {
280 let candidates = outcome
281 .artifacts()
282 .map(|(name, _,)| name)
283 .collect::<Vec<_>>();
284 let suggestion = if let Some(suggestion) = foundry_cli::utils::did_you_mean(&contract, candidates).pop() {
285 format!("\nDid you mean `{suggestion}`?")
286 } else {
287 String::new()
288 };
289 eyre::eyre!(
290 "Could not find artifact `{contract}` in the compiled artifacts{suggestion}",
291 )
292 })?
293 .clone();
294 vec![(contract, artifact)]
295 } else {
296 selector_artifacts(outcome, &project.paths.sources)
297 };
298
299 let mut artifacts = artifacts.into_iter();
300
301 #[derive(PartialEq, PartialOrd, Eq, Ord)]
302 enum SelectorType {
303 Function,
304 Event,
305 Error,
306 }
307 impl std::fmt::Display for SelectorType {
308 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309 match self {
310 Self::Function => write!(f, "Function"),
311 Self::Event => write!(f, "Event"),
312 Self::Error => write!(f, "Error"),
313 }
314 }
315 }
316
317 let mut selectors =
318 BTreeMap::<String, BTreeMap<SelectorType, Vec<(String, String)>>>::new();
319
320 for (contract, artifact) in artifacts.by_ref() {
321 let abi = artifact.abi.ok_or_else(|| eyre::eyre!("Unable to fetch abi"))?;
322
323 let contract_selectors = selectors.entry(contract).or_default();
324
325 for func in abi.functions() {
326 let sig = func.signature();
327 let selector = func.selector();
328 contract_selectors
329 .entry(SelectorType::Function)
330 .or_default()
331 .push((hex::encode_prefixed(selector), sig));
332 }
333
334 for event in abi.events() {
335 let sig = event.signature();
336 let selector = event.selector();
337 contract_selectors
338 .entry(SelectorType::Event)
339 .or_default()
340 .push((hex::encode_prefixed(selector), sig));
341 }
342
343 for error in abi.errors() {
344 let sig = error.signature();
345 let selector = error.selector();
346 contract_selectors
347 .entry(SelectorType::Error)
348 .or_default()
349 .push((hex::encode_prefixed(selector), sig));
350 }
351 }
352
353 if no_group {
354 let mut table = Table::new();
355 if shell::is_markdown() {
356 table.load_style(ASCII_MARKDOWN);
357 } else {
358 table.load_style(ASCII_FULL.with_rounded_corners());
359 }
360 table.set_header(["Type", "Signature", "Selector", "Contract"]);
361
362 for (contract, contract_selectors) in selectors {
363 for (selector_type, selectors) in contract_selectors {
364 for (selector, sig) in selectors {
365 table.add_row([
366 selector_type.to_string(),
367 sig,
368 selector,
369 contract.clone(),
370 ]);
371 }
372 }
373 }
374
375 sh_println!("\n{table}")?;
376 } else {
377 for (idx, (contract, contract_selectors)) in selectors.into_iter().enumerate() {
378 sh_println!("{}{contract}", if idx == 0 { "" } else { "\n" })?;
379 let mut table = Table::new();
380 if shell::is_markdown() {
381 table.load_style(ASCII_MARKDOWN);
382 } else {
383 table.load_style(ASCII_FULL.with_rounded_corners());
384 }
385 table.set_header(["Type", "Signature", "Selector"]);
386
387 for (selector_type, selectors) in contract_selectors {
388 for (selector, sig) in selectors {
389 table.add_row([selector_type.to_string(), sig, selector]);
390 }
391 }
392 sh_println!("\n{table}")?;
393 }
394 }
395 }
396
397 Self::Find { selector, project_paths } => {
398 sh_status!("Searching for selector {selector:?} in the project...")?;
399
400 let (mut project, compiler) = project_from_paths(project_paths)?;
401 let outcome = compile_abi_project_cached(&mut project, compiler.quiet(true))?;
402 let artifacts = selector_artifacts(outcome, &project.paths.sources);
403
404 let mut table = Table::new();
405 if shell::is_markdown() {
406 table.load_style(ASCII_MARKDOWN);
407 } else {
408 table.load_style(ASCII_FULL.with_rounded_corners());
409 }
410
411 table.set_header(["Type", "Signature", "Selector", "Contract"]);
412
413 let selector_str = selector.strip_prefix("0x").unwrap_or(selector.as_str());
414 let selector_bytes = hex::decode(selector_str)?;
415
416 for (contract, artifact) in artifacts {
417 let abi = artifact.abi.ok_or_else(|| eyre::eyre!("Unable to fetch abi"))?;
418
419 for func in abi.functions() {
420 if func.selector().as_slice().starts_with(selector_bytes.as_slice()) {
421 table.add_row([
422 "Function",
423 &func.signature(),
424 &hex::encode_prefixed(func.selector()),
425 contract.as_str(),
426 ]);
427 }
428 }
429
430 for event in abi.events() {
431 if event.selector().as_slice().starts_with(selector_bytes.as_slice()) {
432 table.add_row([
433 "Event",
434 &event.signature(),
435 &hex::encode_prefixed(event.selector()),
436 contract.as_str(),
437 ]);
438 }
439 }
440
441 for error in abi.errors() {
442 if error.selector().as_slice().starts_with(selector_bytes.as_slice()) {
443 table.add_row([
444 "Error",
445 &error.signature(),
446 &hex::encode_prefixed(error.selector()),
447 contract.as_str(),
448 ]);
449 }
450 }
451 }
452
453 if table.row_count() > 0 {
454 sh_status!("Found {} instance(s)...", table.row_count())?;
455 sh_println!("\n{table}\n")?;
456 } else {
457 return Err(eyre::eyre!("\nSelector not found in the project."));
458 }
459 }
460 }
461 Ok(())
462 }
463}
464
465fn selector_artifacts(
466 output: ProjectCompileOutput,
467 sources: &Path,
468) -> Vec<(String, ConfigurableContractArtifact)> {
469 output
470 .into_artifacts()
471 .filter(|(id, _)| {
472 (id.source.starts_with(sources) || is_external_artifact(&id.build_id))
473 && !id.source.is_sol_test()
474 })
475 .map(|(id, artifact)| {
476 let name = if is_external_artifact(&id.build_id) {
477 id.name
478 } else {
479 id.name.split('.').next().unwrap().to_owned()
481 };
482 (name, artifact)
483 })
484 .collect()
485}
486
487fn project_from_paths(
488 project_paths: ProjectPathOpts,
489) -> Result<(Project<MultiCompiler>, ProjectCompiler)> {
490 let build = BuildOpts { project_paths, ..Default::default() };
491 let config = build.load_config_with_dependencies()?;
492 let compiler = ProjectCompiler::new()
493 .external_compilers(&config)
494 .external_artifacts(false)
495 .dynamic_test_linking(config.dynamic_test_linking);
496 let mut project = config.project()?;
497 if !project.build_info {
498 project.no_artifacts = true;
499 }
500 Ok((project, compiler))
501}