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)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Recipient {
Address(String),
Name(String),
}
pub fn classify_recipient(raw: &str) -> Result<Recipient, String> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err("recipient is empty".to_string());
}
if is_address_hex(trimmed) {
if is_zero_address(trimmed) {
return Err("refusing to send to the zero address (0x0) — funds would be burned".to_string());
}
Ok(Recipient::Address(trimmed.to_string()))
} else {
Ok(Recipient::Name(trimmed.to_lowercase()))
}
}
fn is_zero_address(s: &str) -> bool {
let stripped = s.trim_start_matches("0x").trim_start_matches("0X");
stripped.bytes().all(|b| b == b'0')
}
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_token_amount_hostile_inputs() {
assert_eq!(parse_token_amount("340282366920938463463374607431768211455"), None);
assert_eq!(parse_token_amount("340282366920938463464"), None);
assert_eq!(
parse_token_amount("340282366920938463463"),
Some(340_282_366_920_938_463_463_u128 * 1_000_000_000_000_000_000)
);
assert_eq!(parse_token_amount(&"9".repeat(60)), None);
assert_eq!(parse_token_amount("0.000000000000000001"), Some(1));
assert_eq!(parse_token_amount("0.0000000000000000009"), Some(0));
assert_eq!(parse_token_amount(&format!("1.{}", "9".repeat(40))), {
Some(1_999_999_999_999_999_999)
});
assert_eq!(parse_token_amount("1.2.3"), None); assert_eq!(parse_token_amount("1e5"), None); assert_eq!(parse_token_amount("-1"), None); assert_eq!(parse_token_amount("0x10"), None); assert_eq!(parse_token_amount(" 1.5 "), Some(1_500_000_000_000_000_000)); assert_eq!(parse_token_amount(" 1 2 "), None); assert_eq!(parse_token_amount("0"), Some(0));
assert_eq!(parse_token_amount("."), Some(0));
assert_eq!(parse_token_amount("0.0"), Some(0));
}
#[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");
}
#[test]
fn classify_recipient_distinguishes_address_from_name() {
let addr = format!("0x{}", "a".repeat(40));
assert_eq!(
classify_recipient(&addr),
Ok(Recipient::Address(addr.clone()))
);
let bare = "B".repeat(40);
assert_eq!(
classify_recipient(&format!(" {bare} ")),
Ok(Recipient::Address(bare.clone()))
);
assert_eq!(
classify_recipient(" Alice "),
Ok(Recipient::Name("alice".to_string()))
);
assert_eq!(
classify_recipient("0x1234"),
Ok(Recipient::Name("0x1234".to_string()))
);
assert!(classify_recipient("").is_err());
assert!(classify_recipient(" ").is_err());
}
#[test]
fn classify_recipient_hostile_inputs() {
assert!(classify_recipient(&format!("0x{}", "0".repeat(40))).is_err());
assert!(classify_recipient(&"0".repeat(40)).is_err());
assert!(classify_recipient(&format!("0X{}", "0".repeat(40))).is_err());
let almost = format!("0x{}1", "0".repeat(39));
assert_eq!(
classify_recipient(&almost),
Ok(Recipient::Address(almost.clone()))
);
let checksum = "0xAbC0000000000000000000000000000000000123";
assert_eq!(
classify_recipient(checksum),
Ok(Recipient::Address(checksum.to_string()))
);
assert!(matches!(
classify_recipient(&format!("0x{}", "a".repeat(39))),
Ok(Recipient::Name(_))
));
assert!(matches!(
classify_recipient(&format!("0x{}", "a".repeat(41))),
Ok(Recipient::Name(_))
));
let hexname = "deadbeef".repeat(5); assert!(matches!(
classify_recipient(&hexname),
Ok(Recipient::Address(_))
));
assert_eq!(
classify_recipient("Solidity-Bob"),
Ok(Recipient::Name("solidity-bob".to_string()))
);
}
#[test]
fn act_panel_address_only_filter() {
fn accepts(raw: &str) -> bool {
matches!(classify_recipient(raw), Ok(Recipient::Address(_)))
}
assert!(accepts(&format!("0x{}", "a".repeat(40))));
assert!(accepts(&"B".repeat(40)));
assert!(!accepts(&format!("0x{}", "0".repeat(40))));
assert!(!accepts(&"0".repeat(40)));
assert!(!accepts(""));
assert!(!accepts(" "));
assert!(!accepts("alice"));
assert!(!accepts(&format!("0x{}", "a".repeat(41))));
}
}