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: &dyn crate::http::HttpClient,
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 .send(crate::http::Request::get(&url))
210 .await
211 .map_err(|e| ScopeError::Api(format!("Etherscan source fetch failed: {}", e)))?;
212
213 let text = &response.body;
214
215 let api_response: EtherscanSourceResponse = serde_json::from_str(text)
216 .map_err(|e| ScopeError::Api(format!("Failed to parse Etherscan response: {}", e)))?;
217
218 if api_response.status != "1" || api_response.result.is_empty() {
219 return Err(ScopeError::NotFound(format!(
220 "Contract source not verified for {} on {}",
221 address, chain
222 )));
223 }
224
225 let result = &api_response.result[0];
226
227 if result.source_code.is_empty() || result.abi == "Contract source code not verified" {
229 return Err(ScopeError::NotFound(format!(
230 "Contract {} is not verified on {} Etherscan",
231 address, chain
232 )));
233 }
234
235 let parsed_abi: Vec<AbiEntry> = if result.abi.starts_with('[') {
237 serde_json::from_str(&result.abi).unwrap_or_default()
238 } else {
239 Vec::new()
240 };
241
242 let optimization_used = result.optimization_used == "1";
243 let optimization_runs: u32 = result.runs.parse().unwrap_or(200);
244
245 let implementation_address = if !result.implementation.is_empty() {
246 Some(result.implementation.clone())
247 } else {
248 None
249 };
250
251 Ok(ContractSource {
252 contract_name: result.contract_name.clone(),
253 source_code: result.source_code.clone(),
254 abi: result.abi.clone(),
255 compiler_version: result.compiler_version.clone(),
256 optimization_used,
257 optimization_runs,
258 evm_version: result.evm_version.clone(),
259 license_type: result.license_type.clone(),
260 is_proxy: result.proxy == "1",
261 implementation_address,
262 constructor_arguments: result.constructor_arguments.clone(),
263 library: result.library.clone(),
264 swarm_source: result.swarm_source.clone(),
265 parsed_abi,
266 })
267}
268
269pub fn extract_source_files(source: &ContractSource) -> Vec<SourceFile> {
276 let code = &source.source_code;
277
278 if code.starts_with("{{") && code.ends_with("}}") {
280 let inner = &code[1..code.len() - 1];
282 if let Ok(standard_json) = serde_json::from_str::<serde_json::Value>(inner) {
283 return extract_from_standard_json(&standard_json);
284 }
285 }
286
287 if code.starts_with('{')
289 && let Ok(json) = serde_json::from_str::<serde_json::Value>(code)
290 {
291 return extract_from_standard_json(&json);
292 }
293
294 vec![SourceFile {
296 path: format!("{}.sol", source.contract_name),
297 content: code.clone(),
298 }]
299}
300
301#[derive(Debug, Clone)]
303pub struct SourceFile {
304 pub path: String,
306 pub content: String,
308}
309
310fn extract_from_standard_json(json: &serde_json::Value) -> Vec<SourceFile> {
311 let mut files = Vec::new();
312
313 if let Some(sources) = json.get("sources").and_then(|s| s.as_object()) {
314 for (path, source) in sources {
315 if let Some(content) = source.get("content").and_then(|c| c.as_str()) {
316 files.push(SourceFile {
317 path: path.clone(),
318 content: content.to_string(),
319 });
320 }
321 }
322 }
323
324 files
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
332 fn test_chain_to_etherscan_id() {
333 assert_eq!(chain_to_etherscan_id("ethereum"), Some("1"));
334 assert_eq!(chain_to_etherscan_id("polygon"), Some("137"));
335 assert_eq!(chain_to_etherscan_id("bsc"), Some("56"));
336 assert_eq!(chain_to_etherscan_id("solana"), None);
337 }
338
339 #[test]
340 fn test_abi_entry_signature() {
341 let entry = AbiEntry {
342 entry_type: "function".to_string(),
343 name: "transfer".to_string(),
344 inputs: vec![
345 AbiParam {
346 name: "to".to_string(),
347 param_type: "address".to_string(),
348 indexed: false,
349 components: vec![],
350 },
351 AbiParam {
352 name: "amount".to_string(),
353 param_type: "uint256".to_string(),
354 indexed: false,
355 components: vec![],
356 },
357 ],
358 outputs: vec![],
359 state_mutability: "nonpayable".to_string(),
360 };
361 assert_eq!(entry.signature(), "transfer(address,uint256)");
362 }
363
364 #[test]
365 fn test_abi_entry_is_state_changing() {
366 let view_fn = AbiEntry {
367 entry_type: "function".to_string(),
368 name: "balanceOf".to_string(),
369 inputs: vec![],
370 outputs: vec![],
371 state_mutability: "view".to_string(),
372 };
373 assert!(!view_fn.is_state_changing());
374
375 let write_fn = AbiEntry {
376 entry_type: "function".to_string(),
377 name: "transfer".to_string(),
378 inputs: vec![],
379 outputs: vec![],
380 state_mutability: "nonpayable".to_string(),
381 };
382 assert!(write_fn.is_state_changing());
383 }
384
385 #[test]
386 fn test_extract_single_file_source() {
387 let source = ContractSource {
388 contract_name: "TestToken".to_string(),
389 source_code: "pragma solidity ^0.8.0;\ncontract TestToken {}".to_string(),
390 abi: "[]".to_string(),
391 compiler_version: "v0.8.19".to_string(),
392 optimization_used: true,
393 optimization_runs: 200,
394 evm_version: "paris".to_string(),
395 license_type: "MIT".to_string(),
396 is_proxy: false,
397 implementation_address: None,
398 constructor_arguments: String::new(),
399 library: String::new(),
400 swarm_source: String::new(),
401 parsed_abi: vec![],
402 };
403 let files = extract_source_files(&source);
404 assert_eq!(files.len(), 1);
405 assert_eq!(files[0].path, "TestToken.sol");
406 assert!(files[0].content.contains("pragma solidity"));
407 }
408
409 #[test]
410 fn test_extract_multi_file_source() {
411 let json_source = r#"{"sources":{"contracts/Token.sol":{"content":"pragma solidity ^0.8.0;"},"contracts/Lib.sol":{"content":"library Lib {}"}}}"#;
412 let wrapped = format!("{{{}}}", json_source);
413 let source = ContractSource {
414 contract_name: "Token".to_string(),
415 source_code: wrapped,
416 abi: "[]".to_string(),
417 compiler_version: "v0.8.19".to_string(),
418 optimization_used: true,
419 optimization_runs: 200,
420 evm_version: "paris".to_string(),
421 license_type: "MIT".to_string(),
422 is_proxy: false,
423 implementation_address: None,
424 constructor_arguments: String::new(),
425 library: String::new(),
426 swarm_source: String::new(),
427 parsed_abi: vec![],
428 };
429 let files = extract_source_files(&source);
430 assert_eq!(files.len(), 2);
431 }
432
433 #[test]
434 fn test_chain_to_etherscan_id_all() {
435 assert_eq!(chain_to_etherscan_id("ethereum"), Some("1"));
436 assert_eq!(chain_to_etherscan_id("eth"), Some("1"));
437 assert_eq!(chain_to_etherscan_id("polygon"), Some("137"));
438 assert_eq!(chain_to_etherscan_id("matic"), Some("137"));
439 assert_eq!(chain_to_etherscan_id("arbitrum"), Some("42161"));
440 assert_eq!(chain_to_etherscan_id("arb"), Some("42161"));
441 assert_eq!(chain_to_etherscan_id("optimism"), Some("10"));
442 assert_eq!(chain_to_etherscan_id("op"), Some("10"));
443 assert_eq!(chain_to_etherscan_id("base"), Some("8453"));
444 assert_eq!(chain_to_etherscan_id("bsc"), Some("56"));
445 assert_eq!(chain_to_etherscan_id("bnb"), Some("56"));
446 assert_eq!(chain_to_etherscan_id("solana"), None);
447 assert_eq!(chain_to_etherscan_id("tron"), None);
448 }
449
450 #[test]
451 fn test_abi_entry_selector() {
452 let entry = AbiEntry {
453 entry_type: "function".to_string(),
454 name: "transfer".to_string(),
455 inputs: vec![
456 AbiParam {
457 name: "to".to_string(),
458 param_type: "address".to_string(),
459 indexed: false,
460 components: vec![],
461 },
462 AbiParam {
463 name: "amount".to_string(),
464 param_type: "uint256".to_string(),
465 indexed: false,
466 components: vec![],
467 },
468 ],
469 outputs: vec![],
470 state_mutability: "nonpayable".to_string(),
471 };
472 let selector = entry.selector();
473 assert!(selector.starts_with("0x"));
474 assert_eq!(selector.len(), 10);
475 }
476
477 #[test]
478 fn test_abi_entry_signature_no_params() {
479 let entry = AbiEntry {
480 entry_type: "function".to_string(),
481 name: "pause".to_string(),
482 inputs: vec![],
483 outputs: vec![],
484 state_mutability: "nonpayable".to_string(),
485 };
486 assert_eq!(entry.signature(), "pause()");
487 }
488
489 #[test]
490 fn test_abi_entry_pure_not_state_changing() {
491 let entry = AbiEntry {
492 entry_type: "function".to_string(),
493 name: "add".to_string(),
494 inputs: vec![],
495 outputs: vec![],
496 state_mutability: "pure".to_string(),
497 };
498 assert!(!entry.is_state_changing());
499 }
500
501 #[test]
502 fn test_abi_entry_payable_is_state_changing() {
503 let entry = AbiEntry {
504 entry_type: "function".to_string(),
505 name: "deposit".to_string(),
506 inputs: vec![],
507 outputs: vec![],
508 state_mutability: "payable".to_string(),
509 };
510 assert!(entry.is_state_changing());
511 }
512
513 #[test]
514 fn test_abi_entry_event_not_state_changing() {
515 let entry = AbiEntry {
516 entry_type: "event".to_string(),
517 name: "Transfer".to_string(),
518 inputs: vec![],
519 outputs: vec![],
520 state_mutability: String::new(),
521 };
522 assert!(!entry.is_state_changing());
523 }
524
525 #[test]
526 fn test_abi_param_canonical_type_tuple() {
527 let param = AbiParam {
528 name: "data".to_string(),
529 param_type: "tuple".to_string(),
530 indexed: false,
531 components: vec![
532 AbiParam {
533 name: "a".to_string(),
534 param_type: "address".to_string(),
535 indexed: false,
536 components: vec![],
537 },
538 AbiParam {
539 name: "b".to_string(),
540 param_type: "uint256".to_string(),
541 indexed: false,
542 components: vec![],
543 },
544 ],
545 };
546 assert_eq!(param.canonical_type(), "(address,uint256)");
547 }
548
549 #[test]
550 fn test_abi_param_canonical_type_nested_tuple() {
551 let param = AbiParam {
552 name: "nested".to_string(),
553 param_type: "tuple".to_string(),
554 indexed: false,
555 components: vec![AbiParam {
556 name: "inner".to_string(),
557 param_type: "tuple".to_string(),
558 indexed: false,
559 components: vec![AbiParam {
560 name: "x".to_string(),
561 param_type: "uint256".to_string(),
562 indexed: false,
563 components: vec![],
564 }],
565 }],
566 };
567 assert_eq!(param.canonical_type(), "((uint256))");
568 }
569
570 #[test]
571 fn test_extract_standard_json_source() {
572 let json_str = r#"{"sources":{"A.sol":{"content":"pragma solidity ^0.8.0;"}}}"#;
573 let source = ContractSource {
574 contract_name: "A".to_string(),
575 source_code: json_str.to_string(),
576 abi: "[]".to_string(),
577 compiler_version: "v0.8.19".to_string(),
578 optimization_used: false,
579 optimization_runs: 200,
580 evm_version: "paris".to_string(),
581 license_type: "MIT".to_string(),
582 is_proxy: false,
583 implementation_address: None,
584 constructor_arguments: String::new(),
585 library: String::new(),
586 swarm_source: String::new(),
587 parsed_abi: vec![],
588 };
589 let files = extract_source_files(&source);
590 assert_eq!(files.len(), 1);
591 assert_eq!(files[0].path, "A.sol");
592 }
593
594 #[test]
595 fn test_extract_invalid_json_fallback() {
596 let source = ContractSource {
597 contract_name: "Token".to_string(),
598 source_code: "{invalid json".to_string(),
599 abi: "[]".to_string(),
600 compiler_version: "v0.8.19".to_string(),
601 optimization_used: false,
602 optimization_runs: 200,
603 evm_version: "paris".to_string(),
604 license_type: "MIT".to_string(),
605 is_proxy: false,
606 implementation_address: None,
607 constructor_arguments: String::new(),
608 library: String::new(),
609 swarm_source: String::new(),
610 parsed_abi: vec![],
611 };
612 let files = extract_source_files(&source);
613 assert_eq!(files.len(), 1);
614 assert_eq!(files[0].path, "Token.sol");
615 }
616
617 #[test]
618 fn test_extract_standard_json_no_sources_key() {
619 let json_str = r#"{"settings":{"optimizer":{"enabled":true}}}"#;
620 let source = ContractSource {
621 contract_name: "Token".to_string(),
622 source_code: json_str.to_string(),
623 abi: "[]".to_string(),
624 compiler_version: "v0.8.19".to_string(),
625 optimization_used: false,
626 optimization_runs: 200,
627 evm_version: "paris".to_string(),
628 license_type: "MIT".to_string(),
629 is_proxy: false,
630 implementation_address: None,
631 constructor_arguments: String::new(),
632 library: String::new(),
633 swarm_source: String::new(),
634 parsed_abi: vec![],
635 };
636 let files = extract_source_files(&source);
637 assert_eq!(files.len(), 0);
638 }
639
640 #[test]
641 fn test_sha2_256_deterministic() {
642 let hash1 = sha2_256(b"hello");
643 let hash2 = sha2_256(b"hello");
644 assert_eq!(hash1, hash2);
645 let hash3 = sha2_256(b"world");
646 assert_ne!(hash1, hash3);
647 }
648
649 #[test]
650 fn test_contract_source_serialization() {
651 let source = ContractSource {
652 contract_name: "Test".to_string(),
653 source_code: "code".to_string(),
654 abi: "[]".to_string(),
655 compiler_version: "v0.8.19".to_string(),
656 optimization_used: true,
657 optimization_runs: 200,
658 evm_version: "paris".to_string(),
659 license_type: "MIT".to_string(),
660 is_proxy: true,
661 implementation_address: Some("0x123".to_string()),
662 constructor_arguments: "0xdeadbeef".to_string(),
663 library: "SafeMath:0xabc".to_string(),
664 swarm_source: "ipfs://Qm123".to_string(),
665 parsed_abi: vec![],
666 };
667 let json = serde_json::to_string(&source).unwrap();
668 let deserialized: ContractSource = serde_json::from_str(&json).unwrap();
669 assert_eq!(deserialized.contract_name, "Test");
670 assert!(deserialized.is_proxy);
671 assert_eq!(
672 deserialized.implementation_address,
673 Some("0x123".to_string())
674 );
675 }
676
677 #[test]
678 fn test_extract_wrapped_invalid_inner_json() {
679 let source = ContractSource {
680 contract_name: "Token".to_string(),
681 source_code: "{{not valid json}}".to_string(),
682 abi: "[]".to_string(),
683 compiler_version: "v0.8.19".to_string(),
684 optimization_used: false,
685 optimization_runs: 200,
686 evm_version: "paris".to_string(),
687 license_type: "MIT".to_string(),
688 is_proxy: false,
689 implementation_address: None,
690 constructor_arguments: String::new(),
691 library: String::new(),
692 swarm_source: String::new(),
693 parsed_abi: vec![],
694 };
695 let files = extract_source_files(&source);
696 assert_eq!(files.len(), 1);
697 assert_eq!(files[0].path, "Token.sol");
698 }
699
700 #[test]
701 fn test_extract_standard_json_source_missing_content() {
702 let json_str = r#"{"sources":{"A.sol":{},"B.sol":{"content":"valid"}}}"#;
703 let source = ContractSource {
704 contract_name: "Token".to_string(),
705 source_code: json_str.to_string(),
706 abi: "[]".to_string(),
707 compiler_version: "v0.8.19".to_string(),
708 optimization_used: false,
709 optimization_runs: 200,
710 evm_version: "paris".to_string(),
711 license_type: "MIT".to_string(),
712 is_proxy: false,
713 implementation_address: None,
714 constructor_arguments: String::new(),
715 library: String::new(),
716 swarm_source: String::new(),
717 parsed_abi: vec![],
718 };
719 let files = extract_source_files(&source);
720 assert_eq!(files.len(), 1);
721 assert_eq!(files[0].path, "B.sol");
722 assert_eq!(files[0].content, "valid");
723 }
724
725 #[test]
726 fn test_extract_standard_json_empty_sources_object() {
727 let json_str = r#"{"sources":{}}"#;
728 let source = ContractSource {
729 contract_name: "Token".to_string(),
730 source_code: json_str.to_string(),
731 abi: "[]".to_string(),
732 compiler_version: "v0.8.19".to_string(),
733 optimization_used: false,
734 optimization_runs: 200,
735 evm_version: "paris".to_string(),
736 license_type: "MIT".to_string(),
737 is_proxy: false,
738 implementation_address: None,
739 constructor_arguments: String::new(),
740 library: String::new(),
741 swarm_source: String::new(),
742 parsed_abi: vec![],
743 };
744 let files = extract_source_files(&source);
745 assert_eq!(files.len(), 0);
746 }
747
748 #[test]
749 fn test_extract_standard_json_sources_not_object() {
750 let json_str = r#"{"sources":["file1.sol","file2.sol"]}"#;
751 let source = ContractSource {
752 contract_name: "Token".to_string(),
753 source_code: json_str.to_string(),
754 abi: "[]".to_string(),
755 compiler_version: "v0.8.19".to_string(),
756 optimization_used: false,
757 optimization_runs: 200,
758 evm_version: "paris".to_string(),
759 license_type: "MIT".to_string(),
760 is_proxy: false,
761 implementation_address: None,
762 constructor_arguments: String::new(),
763 library: String::new(),
764 swarm_source: String::new(),
765 parsed_abi: vec![],
766 };
767 let files = extract_source_files(&source);
768 assert_eq!(files.len(), 0);
769 }
770
771 #[test]
772 fn test_source_file_struct() {
773 let sf = SourceFile {
774 path: "contracts/Token.sol".to_string(),
775 content: "pragma solidity ^0.8.0;".to_string(),
776 };
777 assert_eq!(sf.path, "contracts/Token.sol");
778 let cloned = sf.clone();
779 assert_eq!(cloned.content, sf.content);
780 }
781
782 #[test]
783 fn test_abi_entry_constructor_not_state_changing() {
784 let entry = AbiEntry {
785 entry_type: "constructor".to_string(),
786 name: String::new(),
787 inputs: vec![],
788 outputs: vec![],
789 state_mutability: String::new(),
790 };
791 assert!(!entry.is_state_changing());
792 }
793
794 #[test]
795 fn test_abi_entry_fallback_not_state_changing() {
796 let entry = AbiEntry {
797 entry_type: "fallback".to_string(),
798 name: String::new(),
799 inputs: vec![],
800 outputs: vec![],
801 state_mutability: "payable".to_string(),
802 };
803 assert!(!entry.is_state_changing());
804 }
805
806 #[test]
807 fn test_abi_entry_selector_consistency() {
808 let entry = AbiEntry {
809 entry_type: "function".to_string(),
810 name: "transfer".to_string(),
811 inputs: vec![
812 AbiParam {
813 name: "to".to_string(),
814 param_type: "address".to_string(),
815 indexed: false,
816 components: vec![],
817 },
818 AbiParam {
819 name: "value".to_string(),
820 param_type: "uint256".to_string(),
821 indexed: false,
822 components: vec![],
823 },
824 ],
825 outputs: vec![],
826 state_mutability: "nonpayable".to_string(),
827 };
828 let sig = entry.signature();
829 let sel = entry.selector();
830 assert_eq!(sig, "transfer(address,uint256)");
831 assert!(sel.starts_with("0x"));
832 assert_eq!(sel.len(), 10);
833 }
834}