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