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