1use ethers_core::types::U256;
2use tiny_keccak::{Hasher, Keccak};
3use tracing::warn;
4
5#[must_use]
8#[allow(clippy::expect_used)]
9pub fn wei_to_eth_f64(wei: U256) -> f64 {
10 let wei_str = wei.to_string();
11 let wei_f64: f64 = wei_str
14 .parse()
15 .expect("U256::to_string() always produces a valid decimal for f64::parse()");
16 wei_f64 / 1_000_000_000_000_000_000.0
17}
18
19#[must_use]
22#[allow(clippy::expect_used)]
23pub fn token_to_f64(amount: U256, decimals: u32) -> f64 {
24 let amt_str = amount.to_string();
25 let amt_f64: f64 = amt_str
27 .parse()
28 .expect("U256::to_string() always produces a valid decimal for f64::parse()");
29 let divisor = 10f64.powi(decimals as i32);
30 amt_f64 / divisor
31}
32
33#[must_use]
35pub fn compute_topology_fingerprint(addresses: &[String]) -> [u8; 32] {
36 let mut fingerprint = [0u8; 32];
37 for address in addresses {
38 fingerprint = xor_into_fingerprint(&fingerprint, address);
39 }
40 fingerprint
41}
42
43#[must_use]
46pub fn xor_into_fingerprint(current: &[u8; 32], address: &str) -> [u8; 32] {
47 match address_hash(address) {
48 Ok(hash) => {
49 let mut result = [0u8; 32];
50 for i in 0..32 {
51 result[i] = current[i] ^ hash[i];
52 }
53 result
54 }
55 Err(msg) => {
56 warn!("{}", msg);
57 *current
58 }
59 }
60}
61
62fn address_hash(address: &str) -> Result<[u8; 32], String> {
64 let address_clean = address.trim_start_matches("0x");
65 let address_bytes =
66 hex::decode(address_clean).map_err(|e| format!("Invalid hex address {address}: {e}"))?;
67
68 if address_bytes.len() != 20 {
69 return Err(format!(
70 "Invalid address length: {} (expected 20 bytes)",
71 address_bytes.len()
72 ));
73 }
74
75 let mut hasher = Keccak::v256();
76 hasher.update(&address_bytes);
77 let mut output = [0u8; 32];
78 hasher.finalize(&mut output);
79 Ok(output)
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn test_wei_conversion() {
88 let one_eth = U256::from(1000000000000000000u64);
89 assert!((wei_to_eth_f64(one_eth) - 1.0).abs() < 1e-9);
90
91 let half_eth = U256::from(500000000000000000u64);
92 assert!((wei_to_eth_f64(half_eth) - 0.5).abs() < 1e-9);
93 }
94
95 #[test]
96 fn test_token_conversion() {
97 let one_usdc = U256::from(1000000u64); assert!((token_to_f64(one_usdc, 6) - 1.0).abs() < 1e-9);
99
100 let one_dai = U256::from(1000000000000000000u64); assert!((token_to_f64(one_dai, 18) - 1.0).abs() < 1e-9);
102 }
103
104 #[test]
105 fn test_fingerprint_xor_commutative() {
106 let addresses = vec![
107 "0x1234567890abcdef1234567890abcdef12345678".to_string(),
108 "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd".to_string(),
109 ];
110 let fp1 = compute_topology_fingerprint(&addresses);
111
112 let reversed = vec![addresses[1].clone(), addresses[0].clone()];
113 let fp2 = compute_topology_fingerprint(&reversed);
114
115 assert_eq!(fp1, fp2);
116 }
117
118 #[test]
119 fn test_fingerprint_xor_self_inverse() {
120 let addresses = vec![
121 "0x1234567890abcdef1234567890abcdef12345678".to_string(),
122 "0x1234567890abcdef1234567890abcdef12345678".to_string(),
123 ];
124 let fp = compute_topology_fingerprint(&addresses);
125 assert_eq!(fp, [0u8; 32]);
126 }
127
128 #[test]
129 fn test_fingerprint_invalid_address_unchanged() {
130 let fp = xor_into_fingerprint(&[0u8; 32], "not_hex");
131 assert_eq!(fp, [0u8; 32]);
132 }
133}