Skip to main content

nox_core/
utils.rs

1use ethers_core::types::U256;
2use tiny_keccak::{Hasher, Keccak};
3use tracing::warn;
4
5/// Convert a U256 wei amount to an f64 ETH amount.
6/// Loses precision above ~9 ETH (f64 mantissa limit). Display/estimation only.
7#[must_use]
8#[allow(clippy::expect_used)]
9pub fn wei_to_eth_f64(wei: U256) -> f64 {
10    let wei_str = wei.to_string();
11    // expect: U256::to_string() always produces valid decimal; unwrap_or(0.0) would
12    // silently make all TXs appear free, corrupting profitability decisions.
13    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/// Convert a U256 token amount to an f64 with the given decimals.
20/// Same f64 precision limitation as `wei_to_eth_f64`. Display/estimation only.
21#[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    // expect: same rationale as wei_to_eth_f64 -- silent 0.0 corrupts revenue calculations.
26    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/// Compute the full XOR topology fingerprint from a list of Ethereum addresses.
34#[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/// XOR a 32-byte address hash into the fingerprint.
44/// Matches Solidity `topologyFingerprint ^ keccak256(abi.encodePacked(nodeAddress))`.
45#[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
62/// keccak256 of a raw 20-byte Ethereum address. Matches Solidity `abi.encodePacked`.
63fn 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); // 6 decimals
98        assert!((token_to_f64(one_usdc, 6) - 1.0).abs() < 1e-9);
99
100        let one_dai = U256::from(1000000000000000000u64); // 18 decimals
101        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}