1use crate::error::{Result, ScopeError};
14use serde::{Deserialize, Serialize};
15
16const ETHERSCAN_V2_API: &str = "https://api.etherscan.io/v2/api";
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ContractSource {
22 pub contract_name: String,
24 pub source_code: String,
26 pub abi: String,
28 pub compiler_version: String,
30 pub optimization_used: bool,
32 pub optimization_runs: u32,
34 pub evm_version: String,
36 pub license_type: String,
38 pub is_proxy: bool,
40 pub implementation_address: Option<String>,
42 pub constructor_arguments: String,
44 pub library: String,
46 pub swarm_source: String,
48 pub parsed_abi: Vec<AbiEntry>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct AbiEntry {
55 #[serde(rename = "type")]
57 pub entry_type: String,
58 #[serde(default)]
60 pub name: String,
61 #[serde(default)]
63 pub inputs: Vec<AbiParam>,
64 #[serde(default)]
66 pub outputs: Vec<AbiParam>,
67 #[serde(default, rename = "stateMutability")]
69 pub state_mutability: String,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct AbiParam {
75 pub name: String,
77 #[serde(rename = "type")]
79 pub param_type: String,
80 #[serde(default)]
82 pub indexed: bool,
83 #[serde(default)]
85 pub components: Vec<AbiParam>,
86}
87
88impl AbiEntry {
89 pub fn signature(&self) -> String {
91 let params: Vec<String> = self.inputs.iter().map(|p| p.canonical_type()).collect();
92 format!("{}({})", self.name, params.join(","))
93 }
94
95 pub fn selector(&self) -> String {
97 let sig = self.signature();
98 let hash = sha2_256(sig.as_bytes());
99 format!("0x{}", hex::encode(&hash[..4]))
100 }
101
102 pub fn is_state_changing(&self) -> bool {
104 self.entry_type == "function"
105 && self.state_mutability != "view"
106 && self.state_mutability != "pure"
107 }
108}
109
110impl AbiParam {
111 fn canonical_type(&self) -> String {
113 if self.param_type == "tuple" {
114 let components: Vec<String> =
115 self.components.iter().map(|c| c.canonical_type()).collect();
116 format!("({})", components.join(","))
117 } else {
118 self.param_type.clone()
119 }
120 }
121}
122
123fn sha2_256(data: &[u8]) -> [u8; 32] {
125 use sha2::{Digest, Sha256};
126 let mut hasher = Sha256::new();
127 hasher.update(data);
128 let result = hasher.finalize();
129 let mut out = [0u8; 32];
130 out.copy_from_slice(&result);
131 out
132}
133
134fn chain_to_etherscan_id(chain: &str) -> Option<&'static str> {
136 match chain.to_lowercase().as_str() {
137 "ethereum" | "eth" => Some("1"),
138 "polygon" | "matic" => Some("137"),
139 "arbitrum" | "arb" => Some("42161"),
140 "optimism" | "op" => Some("10"),
141 "base" => Some("8453"),
142 "bsc" | "bnb" => Some("56"),
143 _ => None,
144 }
145}
146
147#[derive(Deserialize)]
149struct EtherscanSourceResponse {
150 status: String,
151 #[allow(dead_code)]
152 message: String,
153 result: Vec<EtherscanSourceResult>,
154}
155
156#[derive(Deserialize)]
157struct EtherscanSourceResult {
158 #[serde(rename = "SourceCode")]
159 source_code: String,
160 #[serde(rename = "ABI")]
161 abi: String,
162 #[serde(rename = "ContractName")]
163 contract_name: String,
164 #[serde(rename = "CompilerVersion")]
165 compiler_version: String,
166 #[serde(rename = "OptimizationUsed")]
167 optimization_used: String,
168 #[serde(rename = "Runs")]
169 runs: String,
170 #[serde(rename = "ConstructorArguments")]
171 constructor_arguments: String,
172 #[serde(rename = "EVMVersion")]
173 evm_version: String,
174 #[serde(rename = "Library")]
175 library: String,
176 #[serde(rename = "LicenseType")]
177 license_type: String,
178 #[serde(rename = "Proxy")]
179 proxy: String,
180 #[serde(rename = "Implementation")]
181 implementation: String,
182 #[serde(rename = "SwarmSource")]
183 swarm_source: String,
184}
185
186pub async fn fetch_contract_source(
191 address: &str,
192 chain: &str,
193 http_client: &reqwest::Client,
194) -> Result<ContractSource> {
195 let chain_id = chain_to_etherscan_id(chain).ok_or_else(|| {
196 ScopeError::Chain(format!(
197 "Chain '{}' does not have Etherscan source code support",
198 chain
199 ))
200 })?;
201
202 let api_key = std::env::var("ETHERSCAN_API_KEY").unwrap_or_default();
203 let url = format!(
204 "{}?chainid={}&module=contract&action=getsourcecode&address={}&apikey={}",
205 ETHERSCAN_V2_API, chain_id, address, api_key
206 );
207
208 let response = http_client
209 .get(&url)
210 .send()
211 .await
212 .map_err(|e| ScopeError::Api(format!("Etherscan source fetch failed: {}", e)))?;
213
214 let text = response
215 .text()
216 .await
217 .map_err(|e| ScopeError::Api(format!("Failed to read Etherscan response: {}", e)))?;
218
219 let api_response: EtherscanSourceResponse = serde_json::from_str(&text)
220 .map_err(|e| ScopeError::Api(format!("Failed to parse Etherscan response: {}", e)))?;
221
222 if api_response.status != "1" || api_response.result.is_empty() {
223 return Err(ScopeError::NotFound(format!(
224 "Contract source not verified for {} on {}",
225 address, chain
226 )));
227 }
228
229 let result = &api_response.result[0];
230
231 if result.source_code.is_empty() || result.abi == "Contract source code not verified" {
233 return Err(ScopeError::NotFound(format!(
234 "Contract {} is not verified on {} Etherscan",
235 address, chain
236 )));
237 }
238
239 let parsed_abi: Vec<AbiEntry> = if result.abi.starts_with('[') {
241 serde_json::from_str(&result.abi).unwrap_or_default()
242 } else {
243 Vec::new()
244 };
245
246 let optimization_used = result.optimization_used == "1";
247 let optimization_runs: u32 = result.runs.parse().unwrap_or(200);
248
249 let implementation_address = if !result.implementation.is_empty() {
250 Some(result.implementation.clone())
251 } else {
252 None
253 };
254
255 Ok(ContractSource {
256 contract_name: result.contract_name.clone(),
257 source_code: result.source_code.clone(),
258 abi: result.abi.clone(),
259 compiler_version: result.compiler_version.clone(),
260 optimization_used,
261 optimization_runs,
262 evm_version: result.evm_version.clone(),
263 license_type: result.license_type.clone(),
264 is_proxy: result.proxy == "1",
265 implementation_address,
266 constructor_arguments: result.constructor_arguments.clone(),
267 library: result.library.clone(),
268 swarm_source: result.swarm_source.clone(),
269 parsed_abi,
270 })
271}
272
273pub fn extract_source_files(source: &ContractSource) -> Vec<SourceFile> {
280 let code = &source.source_code;
281
282 if code.starts_with("{{") && code.ends_with("}}") {
284 let inner = &code[1..code.len() - 1];
286 if let Ok(standard_json) = serde_json::from_str::<serde_json::Value>(inner) {
287 return extract_from_standard_json(&standard_json);
288 }
289 }
290
291 if code.starts_with('{')
293 && let Ok(json) = serde_json::from_str::<serde_json::Value>(code)
294 {
295 return extract_from_standard_json(&json);
296 }
297
298 vec![SourceFile {
300 path: format!("{}.sol", source.contract_name),
301 content: code.clone(),
302 }]
303}
304
305#[derive(Debug, Clone)]
307pub struct SourceFile {
308 pub path: String,
310 pub content: String,
312}
313
314fn extract_from_standard_json(json: &serde_json::Value) -> Vec<SourceFile> {
315 let mut files = Vec::new();
316
317 if let Some(sources) = json.get("sources").and_then(|s| s.as_object()) {
318 for (path, source) in sources {
319 if let Some(content) = source.get("content").and_then(|c| c.as_str()) {
320 files.push(SourceFile {
321 path: path.clone(),
322 content: content.to_string(),
323 });
324 }
325 }
326 }
327
328 files
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 #[test]
336 fn test_chain_to_etherscan_id() {
337 assert_eq!(chain_to_etherscan_id("ethereum"), Some("1"));
338 assert_eq!(chain_to_etherscan_id("polygon"), Some("137"));
339 assert_eq!(chain_to_etherscan_id("bsc"), Some("56"));
340 assert_eq!(chain_to_etherscan_id("solana"), None);
341 }
342
343 #[test]
344 fn test_abi_entry_signature() {
345 let entry = AbiEntry {
346 entry_type: "function".to_string(),
347 name: "transfer".to_string(),
348 inputs: vec![
349 AbiParam {
350 name: "to".to_string(),
351 param_type: "address".to_string(),
352 indexed: false,
353 components: vec![],
354 },
355 AbiParam {
356 name: "amount".to_string(),
357 param_type: "uint256".to_string(),
358 indexed: false,
359 components: vec![],
360 },
361 ],
362 outputs: vec![],
363 state_mutability: "nonpayable".to_string(),
364 };
365 assert_eq!(entry.signature(), "transfer(address,uint256)");
366 }
367
368 #[test]
369 fn test_abi_entry_is_state_changing() {
370 let view_fn = AbiEntry {
371 entry_type: "function".to_string(),
372 name: "balanceOf".to_string(),
373 inputs: vec![],
374 outputs: vec![],
375 state_mutability: "view".to_string(),
376 };
377 assert!(!view_fn.is_state_changing());
378
379 let write_fn = AbiEntry {
380 entry_type: "function".to_string(),
381 name: "transfer".to_string(),
382 inputs: vec![],
383 outputs: vec![],
384 state_mutability: "nonpayable".to_string(),
385 };
386 assert!(write_fn.is_state_changing());
387 }
388
389 #[test]
390 fn test_extract_single_file_source() {
391 let source = ContractSource {
392 contract_name: "TestToken".to_string(),
393 source_code: "pragma solidity ^0.8.0;\ncontract TestToken {}".to_string(),
394 abi: "[]".to_string(),
395 compiler_version: "v0.8.19".to_string(),
396 optimization_used: true,
397 optimization_runs: 200,
398 evm_version: "paris".to_string(),
399 license_type: "MIT".to_string(),
400 is_proxy: false,
401 implementation_address: None,
402 constructor_arguments: String::new(),
403 library: String::new(),
404 swarm_source: String::new(),
405 parsed_abi: vec![],
406 };
407 let files = extract_source_files(&source);
408 assert_eq!(files.len(), 1);
409 assert_eq!(files[0].path, "TestToken.sol");
410 assert!(files[0].content.contains("pragma solidity"));
411 }
412
413 #[test]
414 fn test_extract_multi_file_source() {
415 let json_source = r#"{"sources":{"contracts/Token.sol":{"content":"pragma solidity ^0.8.0;"},"contracts/Lib.sol":{"content":"library Lib {}"}}}"#;
416 let wrapped = format!("{{{}}}", json_source);
417 let source = ContractSource {
418 contract_name: "Token".to_string(),
419 source_code: wrapped,
420 abi: "[]".to_string(),
421 compiler_version: "v0.8.19".to_string(),
422 optimization_used: true,
423 optimization_runs: 200,
424 evm_version: "paris".to_string(),
425 license_type: "MIT".to_string(),
426 is_proxy: false,
427 implementation_address: None,
428 constructor_arguments: String::new(),
429 library: String::new(),
430 swarm_source: String::new(),
431 parsed_abi: vec![],
432 };
433 let files = extract_source_files(&source);
434 assert_eq!(files.len(), 2);
435 }
436
437 #[test]
438 fn test_chain_to_etherscan_id_all() {
439 assert_eq!(chain_to_etherscan_id("ethereum"), Some("1"));
440 assert_eq!(chain_to_etherscan_id("eth"), Some("1"));
441 assert_eq!(chain_to_etherscan_id("polygon"), Some("137"));
442 assert_eq!(chain_to_etherscan_id("matic"), Some("137"));
443 assert_eq!(chain_to_etherscan_id("arbitrum"), Some("42161"));
444 assert_eq!(chain_to_etherscan_id("arb"), Some("42161"));
445 assert_eq!(chain_to_etherscan_id("optimism"), Some("10"));
446 assert_eq!(chain_to_etherscan_id("op"), Some("10"));
447 assert_eq!(chain_to_etherscan_id("base"), Some("8453"));
448 assert_eq!(chain_to_etherscan_id("bsc"), Some("56"));
449 assert_eq!(chain_to_etherscan_id("bnb"), Some("56"));
450 assert_eq!(chain_to_etherscan_id("solana"), None);
451 assert_eq!(chain_to_etherscan_id("tron"), None);
452 }
453
454 #[test]
455 fn test_abi_entry_selector() {
456 let entry = AbiEntry {
457 entry_type: "function".to_string(),
458 name: "transfer".to_string(),
459 inputs: vec![
460 AbiParam {
461 name: "to".to_string(),
462 param_type: "address".to_string(),
463 indexed: false,
464 components: vec![],
465 },
466 AbiParam {
467 name: "amount".to_string(),
468 param_type: "uint256".to_string(),
469 indexed: false,
470 components: vec![],
471 },
472 ],
473 outputs: vec![],
474 state_mutability: "nonpayable".to_string(),
475 };
476 let selector = entry.selector();
477 assert!(selector.starts_with("0x"));
478 assert_eq!(selector.len(), 10);
479 }
480
481 #[test]
482 fn test_abi_entry_signature_no_params() {
483 let entry = AbiEntry {
484 entry_type: "function".to_string(),
485 name: "pause".to_string(),
486 inputs: vec![],
487 outputs: vec![],
488 state_mutability: "nonpayable".to_string(),
489 };
490 assert_eq!(entry.signature(), "pause()");
491 }
492
493 #[test]
494 fn test_abi_entry_pure_not_state_changing() {
495 let entry = AbiEntry {
496 entry_type: "function".to_string(),
497 name: "add".to_string(),
498 inputs: vec![],
499 outputs: vec![],
500 state_mutability: "pure".to_string(),
501 };
502 assert!(!entry.is_state_changing());
503 }
504
505 #[test]
506 fn test_abi_entry_payable_is_state_changing() {
507 let entry = AbiEntry {
508 entry_type: "function".to_string(),
509 name: "deposit".to_string(),
510 inputs: vec![],
511 outputs: vec![],
512 state_mutability: "payable".to_string(),
513 };
514 assert!(entry.is_state_changing());
515 }
516
517 #[test]
518 fn test_abi_entry_event_not_state_changing() {
519 let entry = AbiEntry {
520 entry_type: "event".to_string(),
521 name: "Transfer".to_string(),
522 inputs: vec![],
523 outputs: vec![],
524 state_mutability: String::new(),
525 };
526 assert!(!entry.is_state_changing());
527 }
528
529 #[test]
530 fn test_abi_param_canonical_type_tuple() {
531 let param = AbiParam {
532 name: "data".to_string(),
533 param_type: "tuple".to_string(),
534 indexed: false,
535 components: vec![
536 AbiParam {
537 name: "a".to_string(),
538 param_type: "address".to_string(),
539 indexed: false,
540 components: vec![],
541 },
542 AbiParam {
543 name: "b".to_string(),
544 param_type: "uint256".to_string(),
545 indexed: false,
546 components: vec![],
547 },
548 ],
549 };
550 assert_eq!(param.canonical_type(), "(address,uint256)");
551 }
552
553 #[test]
554 fn test_abi_param_canonical_type_nested_tuple() {
555 let param = AbiParam {
556 name: "nested".to_string(),
557 param_type: "tuple".to_string(),
558 indexed: false,
559 components: vec![AbiParam {
560 name: "inner".to_string(),
561 param_type: "tuple".to_string(),
562 indexed: false,
563 components: vec![AbiParam {
564 name: "x".to_string(),
565 param_type: "uint256".to_string(),
566 indexed: false,
567 components: vec![],
568 }],
569 }],
570 };
571 assert_eq!(param.canonical_type(), "((uint256))");
572 }
573
574 #[test]
575 fn test_extract_standard_json_source() {
576 let json_str = r#"{"sources":{"A.sol":{"content":"pragma solidity ^0.8.0;"}}}"#;
577 let source = ContractSource {
578 contract_name: "A".to_string(),
579 source_code: json_str.to_string(),
580 abi: "[]".to_string(),
581 compiler_version: "v0.8.19".to_string(),
582 optimization_used: false,
583 optimization_runs: 200,
584 evm_version: "paris".to_string(),
585 license_type: "MIT".to_string(),
586 is_proxy: false,
587 implementation_address: None,
588 constructor_arguments: String::new(),
589 library: String::new(),
590 swarm_source: String::new(),
591 parsed_abi: vec![],
592 };
593 let files = extract_source_files(&source);
594 assert_eq!(files.len(), 1);
595 assert_eq!(files[0].path, "A.sol");
596 }
597
598 #[test]
599 fn test_extract_invalid_json_fallback() {
600 let source = ContractSource {
601 contract_name: "Token".to_string(),
602 source_code: "{invalid json".to_string(),
603 abi: "[]".to_string(),
604 compiler_version: "v0.8.19".to_string(),
605 optimization_used: false,
606 optimization_runs: 200,
607 evm_version: "paris".to_string(),
608 license_type: "MIT".to_string(),
609 is_proxy: false,
610 implementation_address: None,
611 constructor_arguments: String::new(),
612 library: String::new(),
613 swarm_source: String::new(),
614 parsed_abi: vec![],
615 };
616 let files = extract_source_files(&source);
617 assert_eq!(files.len(), 1);
618 assert_eq!(files[0].path, "Token.sol");
619 }
620
621 #[test]
622 fn test_extract_standard_json_no_sources_key() {
623 let json_str = r#"{"settings":{"optimizer":{"enabled":true}}}"#;
624 let source = ContractSource {
625 contract_name: "Token".to_string(),
626 source_code: json_str.to_string(),
627 abi: "[]".to_string(),
628 compiler_version: "v0.8.19".to_string(),
629 optimization_used: false,
630 optimization_runs: 200,
631 evm_version: "paris".to_string(),
632 license_type: "MIT".to_string(),
633 is_proxy: false,
634 implementation_address: None,
635 constructor_arguments: String::new(),
636 library: String::new(),
637 swarm_source: String::new(),
638 parsed_abi: vec![],
639 };
640 let files = extract_source_files(&source);
641 assert_eq!(files.len(), 0);
642 }
643
644 #[test]
645 fn test_sha2_256_deterministic() {
646 let hash1 = sha2_256(b"hello");
647 let hash2 = sha2_256(b"hello");
648 assert_eq!(hash1, hash2);
649 let hash3 = sha2_256(b"world");
650 assert_ne!(hash1, hash3);
651 }
652
653 #[test]
654 fn test_contract_source_serialization() {
655 let source = ContractSource {
656 contract_name: "Test".to_string(),
657 source_code: "code".to_string(),
658 abi: "[]".to_string(),
659 compiler_version: "v0.8.19".to_string(),
660 optimization_used: true,
661 optimization_runs: 200,
662 evm_version: "paris".to_string(),
663 license_type: "MIT".to_string(),
664 is_proxy: true,
665 implementation_address: Some("0x123".to_string()),
666 constructor_arguments: "0xdeadbeef".to_string(),
667 library: "SafeMath:0xabc".to_string(),
668 swarm_source: "ipfs://Qm123".to_string(),
669 parsed_abi: vec![],
670 };
671 let json = serde_json::to_string(&source).unwrap();
672 let deserialized: ContractSource = serde_json::from_str(&json).unwrap();
673 assert_eq!(deserialized.contract_name, "Test");
674 assert!(deserialized.is_proxy);
675 assert_eq!(
676 deserialized.implementation_address,
677 Some("0x123".to_string())
678 );
679 }
680
681 #[test]
682 fn test_extract_wrapped_invalid_inner_json() {
683 let source = ContractSource {
684 contract_name: "Token".to_string(),
685 source_code: "{{not valid json}}".to_string(),
686 abi: "[]".to_string(),
687 compiler_version: "v0.8.19".to_string(),
688 optimization_used: false,
689 optimization_runs: 200,
690 evm_version: "paris".to_string(),
691 license_type: "MIT".to_string(),
692 is_proxy: false,
693 implementation_address: None,
694 constructor_arguments: String::new(),
695 library: String::new(),
696 swarm_source: String::new(),
697 parsed_abi: vec![],
698 };
699 let files = extract_source_files(&source);
700 assert_eq!(files.len(), 1);
701 assert_eq!(files[0].path, "Token.sol");
702 }
703
704 #[test]
705 fn test_extract_standard_json_source_missing_content() {
706 let json_str = r#"{"sources":{"A.sol":{},"B.sol":{"content":"valid"}}}"#;
707 let source = ContractSource {
708 contract_name: "Token".to_string(),
709 source_code: json_str.to_string(),
710 abi: "[]".to_string(),
711 compiler_version: "v0.8.19".to_string(),
712 optimization_used: false,
713 optimization_runs: 200,
714 evm_version: "paris".to_string(),
715 license_type: "MIT".to_string(),
716 is_proxy: false,
717 implementation_address: None,
718 constructor_arguments: String::new(),
719 library: String::new(),
720 swarm_source: String::new(),
721 parsed_abi: vec![],
722 };
723 let files = extract_source_files(&source);
724 assert_eq!(files.len(), 1);
725 assert_eq!(files[0].path, "B.sol");
726 assert_eq!(files[0].content, "valid");
727 }
728
729 #[test]
730 fn test_extract_standard_json_empty_sources_object() {
731 let json_str = r#"{"sources":{}}"#;
732 let source = ContractSource {
733 contract_name: "Token".to_string(),
734 source_code: json_str.to_string(),
735 abi: "[]".to_string(),
736 compiler_version: "v0.8.19".to_string(),
737 optimization_used: false,
738 optimization_runs: 200,
739 evm_version: "paris".to_string(),
740 license_type: "MIT".to_string(),
741 is_proxy: false,
742 implementation_address: None,
743 constructor_arguments: String::new(),
744 library: String::new(),
745 swarm_source: String::new(),
746 parsed_abi: vec![],
747 };
748 let files = extract_source_files(&source);
749 assert_eq!(files.len(), 0);
750 }
751
752 #[test]
753 fn test_extract_standard_json_sources_not_object() {
754 let json_str = r#"{"sources":["file1.sol","file2.sol"]}"#;
755 let source = ContractSource {
756 contract_name: "Token".to_string(),
757 source_code: json_str.to_string(),
758 abi: "[]".to_string(),
759 compiler_version: "v0.8.19".to_string(),
760 optimization_used: false,
761 optimization_runs: 200,
762 evm_version: "paris".to_string(),
763 license_type: "MIT".to_string(),
764 is_proxy: false,
765 implementation_address: None,
766 constructor_arguments: String::new(),
767 library: String::new(),
768 swarm_source: String::new(),
769 parsed_abi: vec![],
770 };
771 let files = extract_source_files(&source);
772 assert_eq!(files.len(), 0);
773 }
774
775 #[test]
776 fn test_source_file_struct() {
777 let sf = SourceFile {
778 path: "contracts/Token.sol".to_string(),
779 content: "pragma solidity ^0.8.0;".to_string(),
780 };
781 assert_eq!(sf.path, "contracts/Token.sol");
782 let cloned = sf.clone();
783 assert_eq!(cloned.content, sf.content);
784 }
785
786 #[test]
787 fn test_abi_entry_constructor_not_state_changing() {
788 let entry = AbiEntry {
789 entry_type: "constructor".to_string(),
790 name: String::new(),
791 inputs: vec![],
792 outputs: vec![],
793 state_mutability: String::new(),
794 };
795 assert!(!entry.is_state_changing());
796 }
797
798 #[test]
799 fn test_abi_entry_fallback_not_state_changing() {
800 let entry = AbiEntry {
801 entry_type: "fallback".to_string(),
802 name: String::new(),
803 inputs: vec![],
804 outputs: vec![],
805 state_mutability: "payable".to_string(),
806 };
807 assert!(!entry.is_state_changing());
808 }
809
810 #[test]
811 fn test_abi_entry_selector_consistency() {
812 let entry = AbiEntry {
813 entry_type: "function".to_string(),
814 name: "transfer".to_string(),
815 inputs: vec![
816 AbiParam {
817 name: "to".to_string(),
818 param_type: "address".to_string(),
819 indexed: false,
820 components: vec![],
821 },
822 AbiParam {
823 name: "value".to_string(),
824 param_type: "uint256".to_string(),
825 indexed: false,
826 components: vec![],
827 },
828 ],
829 outputs: vec![],
830 state_mutability: "nonpayable".to_string(),
831 };
832 let sig = entry.signature();
833 let sel = entry.selector();
834 assert_eq!(sig, "transfer(address,uint256)");
835 assert!(sel.starts_with("0x"));
836 assert_eq!(sel.len(), 10);
837 }
838}