pub fn short_addr(addr: &str) -> String {
let stripped = addr.trim_start_matches("0x");
if stripped.len() < 8 {
return addr.to_string();
}
format!("0x{}…{}", &stripped[..4], &stripped[stripped.len() - 4..])
}
pub fn is_address_hex(s: &str) -> bool {
let stripped = s.trim_start_matches("0x").trim_start_matches("0X");
stripped.len() == 40 && stripped.bytes().all(|b| b.is_ascii_hexdigit())
}
pub fn parse_token_amount(raw: &str) -> Option<u128> {
let raw = raw.trim();
if raw.is_empty() {
return None;
}
let (whole_s, frac_s) = match raw.split_once('.') {
Some((w, f)) => (w, f),
None => (raw, ""),
};
let whole: u128 = if whole_s.is_empty() {
0
} else {
whole_s.parse().ok()?
};
if frac_s.bytes().any(|b| !b.is_ascii_digit()) {
return None;
}
let mut frac: u128 = 0;
let mut scale: u128 = 1_000_000_000_000_000_000;
for ch in frac_s.chars().take(18) {
let d = ch.to_digit(10)? as u128;
scale /= 10;
frac = frac.checked_add(d.checked_mul(scale)?)?;
}
let whole_wei = whole.checked_mul(1_000_000_000_000_000_000)?;
whole_wei.checked_add(frac)
}
pub fn parse_address(hex: &str) -> Result<[u8; 20], String> {
let stripped = hex.trim_start_matches("0x").trim_start_matches("0X");
if stripped.len() != 40 {
return Err(format!("address must be 40 hex chars, got {}", stripped.len()));
}
let mut out = [0u8; 20];
let bytes = stripped.as_bytes();
for i in 0..20 {
let hi = hex_nibble(bytes[i * 2])?;
let lo = hex_nibble(bytes[i * 2 + 1])?;
out[i] = (hi << 4) | lo;
}
Ok(out)
}
fn hex_nibble(b: u8) -> Result<u8, String> {
match b {
b'0'..=b'9' => Ok(b - b'0'),
b'a'..=b'f' => Ok(b - b'a' + 10),
b'A'..=b'F' => Ok(b - b'A' + 10),
_ => Err(format!("non-hex byte {b}")),
}
}
pub fn bytes_to_hex_str(bytes: &[u8]) -> String {
let mut s = String::with_capacity(2 + bytes.len() * 2);
s.push_str("0x");
for b in bytes {
s.push_str(&format!("{b:02x}"));
}
s
}
pub fn tx_short_hash(tx_hash: &str) -> String {
let stripped = tx_hash.trim_start_matches("0x");
if stripped.len() < 12 {
return tx_hash.to_string();
}
format!("{}…{}", &stripped[..6], &stripped[stripped.len() - 4..])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn short_addr_abbreviates_and_passes_through_short() {
assert_eq!(short_addr("0x1234567890abcdef"), "0x1234…cdef");
assert_eq!(short_addr("0xabcd"), "0xabcd"); }
#[test]
fn is_address_hex_checks_length_and_charset() {
assert!(is_address_hex(&format!("0x{}", "a".repeat(40))));
assert!(is_address_hex(&"F".repeat(40)));
assert!(!is_address_hex("0x1234")); assert!(!is_address_hex(&"g".repeat(40))); }
#[test]
fn parse_token_amount_handles_whole_and_fractional() {
assert_eq!(parse_token_amount("1"), Some(1_000_000_000_000_000_000));
assert_eq!(parse_token_amount("1.5"), Some(1_500_000_000_000_000_000));
assert_eq!(parse_token_amount("0.000001"), Some(1_000_000_000_000));
assert_eq!(parse_token_amount(""), None);
assert_eq!(parse_token_amount("abc"), None);
assert_eq!(parse_token_amount("1.2x"), None);
}
#[test]
fn parse_address_roundtrips_with_bytes_to_hex() {
let addr = "0x00112233445566778899aabbccddeeff00112233";
let bytes = parse_address(addr).unwrap();
assert_eq!(bytes[0], 0x00);
assert_eq!(bytes[19], 0x33);
assert_eq!(bytes_to_hex_str(&bytes), addr);
assert!(parse_address("0x1234").is_err()); assert!(parse_address(&"z".repeat(40)).is_err()); }
#[test]
fn tx_short_hash_abbreviates_and_passes_through_short() {
assert_eq!(tx_short_hash("0xabcdef1234567890"), "abcdef…7890");
assert_eq!(tx_short_hash("0xabcd"), "0xabcd");
}
}