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 = project_from_paths(project_paths)?;
102 let outcome =
103 compile_abi_project(&mut project, ProjectCompiler::new().quiet(true))?;
104 cache_local_signatures(&outcome)?;
105 }
106 Self::Upload { contract, all, project_paths } => {
107 let mut project = project_from_paths(project_paths)?;
108 let output = if let Some(contract_info) = &contract {
109 let Some(contract_name) = contract_info.name() else {
110 eyre::bail!("No contract name provided.");
111 };
112
113 let target_path = contract_info
114 .path()
115 .map(Ok)
116 .unwrap_or_else(|| project.find_contract_path(contract_name))?;
117 compile_abi_project(&mut project, ProjectCompiler::new().files([target_path]))?
118 } else {
119 compile_abi_project(&mut project, ProjectCompiler::new())?
120 };
121 let artifacts = if all {
122 output
123 .into_artifacts_with_files()
124 .filter(|(file, _, _)| {
125 let is_sources_path = file.starts_with(&project.paths.sources);
126 let is_test = file.is_sol_test();
127
128 is_sources_path && !is_test
129 })
130 .map(|(_, contract, artifact)| (contract, artifact))
131 .collect()
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 artifacts = artifacts.into_iter().peekable();
153 while let Some((contract, artifact)) = artifacts.next() {
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
161 import_selectors(SelectorImportData::Abi(vec![abi])).await?.describe();
163
164 if artifacts.peek().is_some() {
165 sh_println!()?
166 }
167 }
168 }
169 Self::Collision { mut first_contract, mut second_contract, build } => {
170 let user_extra_output = !build.compiler.extra_output.is_empty()
172 || !build.compiler.extra_output_files.is_empty();
173 let mut project = build.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_preset(ASCII_MARKDOWN);
226 } else {
227 table.apply_modifier(UTF8_ROUND_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 = project_from_paths(project_paths)?;
245 let outcome =
246 compile_abi_project(&mut project, ProjectCompiler::new().quiet(true))?;
247 let artifacts = if let Some(contract) = contract {
248 let found_artifact = outcome.find_first(&contract);
249 let artifact = found_artifact
250 .ok_or_else(|| {
251 let candidates = outcome
252 .artifacts()
253 .map(|(name, _,)| name)
254 .collect::<Vec<_>>();
255 let suggestion = if let Some(suggestion) = foundry_cli::utils::did_you_mean(&contract, candidates).pop() {
256 format!("\nDid you mean `{suggestion}`?")
257 } else {
258 String::new()
259 };
260 eyre::eyre!(
261 "Could not find artifact `{contract}` in the compiled artifacts{suggestion}",
262 )
263 })?
264 .clone();
265 vec![(contract, artifact)]
266 } else {
267 outcome
268 .into_artifacts_with_files()
269 .filter(|(file, _, _)| {
270 let is_sources_path = file.starts_with(&project.paths.sources);
271 let is_test = file.is_sol_test();
272
273 is_sources_path && !is_test
274 })
275 .map(|(_, contract, artifact)| (contract, artifact))
276 .collect()
277 };
278
279 let mut artifacts = artifacts.into_iter();
280
281 #[derive(PartialEq, PartialOrd, Eq, Ord)]
282 enum SelectorType {
283 Function,
284 Event,
285 Error,
286 }
287 impl std::fmt::Display for SelectorType {
288 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289 match self {
290 Self::Function => write!(f, "Function"),
291 Self::Event => write!(f, "Event"),
292 Self::Error => write!(f, "Error"),
293 }
294 }
295 }
296
297 let mut selectors =
298 BTreeMap::<String, BTreeMap<SelectorType, Vec<(String, String)>>>::new();
299
300 for (contract, artifact) in artifacts.by_ref() {
301 let abi = artifact.abi.ok_or_else(|| eyre::eyre!("Unable to fetch abi"))?;
302
303 let contract_selectors = selectors.entry(contract.clone()).or_default();
304
305 for func in abi.functions() {
306 let sig = func.signature();
307 let selector = func.selector();
308 contract_selectors
309 .entry(SelectorType::Function)
310 .or_default()
311 .push((hex::encode_prefixed(selector), sig));
312 }
313
314 for event in abi.events() {
315 let sig = event.signature();
316 let selector = event.selector();
317 contract_selectors
318 .entry(SelectorType::Event)
319 .or_default()
320 .push((hex::encode_prefixed(selector), sig));
321 }
322
323 for error in abi.errors() {
324 let sig = error.signature();
325 let selector = error.selector();
326 contract_selectors
327 .entry(SelectorType::Error)
328 .or_default()
329 .push((hex::encode_prefixed(selector), sig));
330 }
331 }
332
333 if no_group {
334 let mut table = Table::new();
335 if shell::is_markdown() {
336 table.load_preset(ASCII_MARKDOWN);
337 } else {
338 table.apply_modifier(UTF8_ROUND_CORNERS);
339 }
340 table.set_header(["Type", "Signature", "Selector", "Contract"]);
341
342 for (contract, contract_selectors) in selectors {
343 for (selector_type, selectors) in contract_selectors {
344 for (selector, sig) in selectors {
345 table.add_row([
346 selector_type.to_string(),
347 sig,
348 selector,
349 contract.clone(),
350 ]);
351 }
352 }
353 }
354
355 sh_println!("\n{table}")?;
356 } else {
357 for (idx, (contract, contract_selectors)) in selectors.into_iter().enumerate() {
358 sh_println!("{}{contract}", if idx == 0 { "" } else { "\n" })?;
359 let mut table = Table::new();
360 if shell::is_markdown() {
361 table.load_preset(ASCII_MARKDOWN);
362 } else {
363 table.apply_modifier(UTF8_ROUND_CORNERS);
364 }
365 table.set_header(["Type", "Signature", "Selector"]);
366
367 for (selector_type, selectors) in contract_selectors {
368 for (selector, sig) in selectors {
369 table.add_row([selector_type.to_string(), sig, selector]);
370 }
371 }
372 sh_println!("\n{table}")?;
373 }
374 }
375 }
376
377 Self::Find { selector, project_paths } => {
378 sh_status!("Searching for selector {selector:?} in the project...")?;
379
380 let mut project = project_from_paths(project_paths)?;
381 let outcome =
382 compile_abi_project(&mut project, ProjectCompiler::new().quiet(true))?;
383 let artifacts = outcome
384 .into_artifacts_with_files()
385 .filter(|(file, _, _)| {
386 let is_sources_path = file.starts_with(&project.paths.sources);
387 let is_test = file.is_sol_test();
388 is_sources_path && !is_test
389 })
390 .collect::<Vec<_>>();
391
392 let mut table = Table::new();
393 if shell::is_markdown() {
394 table.load_preset(ASCII_MARKDOWN);
395 } else {
396 table.apply_modifier(UTF8_ROUND_CORNERS);
397 }
398
399 table.set_header(["Type", "Signature", "Selector", "Contract"]);
400
401 let selector_str = selector.strip_prefix("0x").unwrap_or(selector.as_str());
402 let selector_bytes = hex::decode(selector_str)?;
403
404 for (_file, contract, artifact) in artifacts {
405 let abi = artifact.abi.ok_or_else(|| eyre::eyre!("Unable to fetch abi"))?;
406
407 for func in abi.functions() {
408 if func.selector().as_slice().starts_with(selector_bytes.as_slice()) {
409 table.add_row([
410 "Function",
411 &func.signature(),
412 &hex::encode_prefixed(func.selector()),
413 contract.as_str(),
414 ]);
415 }
416 }
417
418 for event in abi.events() {
419 if event.selector().as_slice().starts_with(selector_bytes.as_slice()) {
420 table.add_row([
421 "Event",
422 &event.signature(),
423 &hex::encode_prefixed(event.selector()),
424 contract.as_str(),
425 ]);
426 }
427 }
428
429 for error in abi.errors() {
430 if error.selector().as_slice().starts_with(selector_bytes.as_slice()) {
431 table.add_row([
432 "Error",
433 &error.signature(),
434 &hex::encode_prefixed(error.selector()),
435 contract.as_str(),
436 ]);
437 }
438 }
439 }
440
441 if table.row_count() > 0 {
442 sh_status!("Found {} instance(s)...", table.row_count())?;
443 sh_println!("\n{table}\n")?;
444 } else {
445 return Err(eyre::eyre!("\nSelector not found in the project."));
446 }
447 }
448 }
449 Ok(())
450 }
451}
452
453fn project_from_paths(project_paths: ProjectPathOpts) -> Result<Project<MultiCompiler>> {
454 let config = BuildOpts { project_paths, ..Default::default() }.load_config()?;
455 let mut project = config.project()?;
456 if !project.build_info {
457 project.no_artifacts = true;
458 }
459 Ok(project)
460}