diff --git a/src/address.rs b/src/address.rs new file mode 100644 index 00000000..3077314a --- /dev/null +++ b/src/address.rs @@ -0,0 +1,144 @@ +use anyhow::ensure; +use fvm_shared::address::{Address, Network}; +use fvm_shared::ActorID; + +// '0x' + 20bytes +const ETH_ADDRESS_LENGTH: usize = 42; +const EAM_NAMESPACE: ActorID = 10; + +fn is_valid_prefix(s: &str, n: Network) -> bool { + if s.len() < 2 { + return false; + } + + match n { + Network::Mainnet => s.starts_with("f") || s.starts_with("0x"), + Network::Testnet => s.starts_with("t") || s.starts_with("0x"), + } +} + +pub fn parse_address(raw: &str, n: Network) -> anyhow::Result
{ + let s = raw.trim().to_lowercase(); + + ensure!(is_valid_prefix(&s, n), "Not a valid {:?} address", n); + + if s.len() > 2 && s.starts_with("0x") { + // Expecting an eth address, perform further validation + ensure!(s.len() == ETH_ADDRESS_LENGTH, "Invalid address length"); + ensure!( + s.chars().skip(2).all(|c| c.is_ascii_hexdigit()), + "Invalid characters in address" + ); + + let addr = hex::decode(&s[2..])?; + Ok(Address::new_delegated(EAM_NAMESPACE, &addr)?) + } else { + Ok(n.parse_address(&s)?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use fvm_shared::address::set_current_network; + + #[test] + fn test_check_address_prefix() { + // Valid cases + assert!(is_valid_prefix("f123...", Network::Mainnet)); + assert!(is_valid_prefix("0x123...", Network::Mainnet)); + assert!(is_valid_prefix("t456...", Network::Testnet)); + assert!(is_valid_prefix("0x789...", Network::Testnet)); + + // Wrong network + assert!(!is_valid_prefix("f123...", Network::Testnet)); + assert!(!is_valid_prefix("t456...", Network::Mainnet)); + + // Bad length + assert!(!is_valid_prefix("f", Network::Mainnet)); + assert!(!is_valid_prefix("t", Network::Testnet)); + assert!(!is_valid_prefix("", Network::Mainnet)); // Empty string + assert!(!is_valid_prefix("abc", Network::Mainnet)); // Short address + + // Invalid prefixes + assert!(!is_valid_prefix("g123...", Network::Mainnet)); + assert!(!is_valid_prefix("h456...", Network::Testnet)); + assert!(!is_valid_prefix("123...", Network::Mainnet)); + assert!(!is_valid_prefix("456...", Network::Testnet)); + } + + #[test] + fn test_parse_mainnet_address() { + let addr_str = "f1alg2sxw32ns3ech2w7r3dmp2gl2fputkl7x7jta"; + let addr = parse_address(addr_str, Network::Mainnet).unwrap(); + + set_current_network(Network::Mainnet); // Required to correctly stringify address + assert_eq!(addr.to_string(), addr_str); + } + + #[test] + fn test_parse_testnet_address() { + let addr_str = "t410f2oekwcmo2pueydmaq53eic2i62crtbeyuzx2gmy"; + let addr = parse_address(addr_str, Network::Testnet).unwrap(); + + set_current_network(Network::Testnet); // Required to correctly stringify address + assert_eq!(addr.to_string(), addr_str); + } + + #[test] + fn test_parse_wrong_network() { + let m_addr_str = "f1alg2sxw32ns3ech2w7r3dmp2gl2fputkl7x7jta"; + let err = parse_address(m_addr_str, Network::Testnet).unwrap_err(); + assert_eq!(err.to_string(), "Not a valid Testnet address"); + + let t_addr_str = "t410f2oekwcmo2pueydmaq53eic2i62crtbeyuzx2gmy"; + let err = parse_address(t_addr_str, Network::Mainnet).unwrap_err(); + assert_eq!(err.to_string(), "Not a valid Mainnet address"); + } + + #[test] + fn test_parse_eth_address_testnet() { + let addr_str = "0xd388ab098ed3e84c0d808776440b48f685198498"; + let addr = parse_address(addr_str, Network::Testnet).unwrap(); + + let exp_addr_str = "t410f2oekwcmo2pueydmaq53eic2i62crtbeyuzx2gmy"; + let exp_addr = parse_address(exp_addr_str, Network::Testnet).unwrap(); + + assert_eq!(exp_addr, addr); + } + + #[test] + fn test_parse_eth_address_mainnet() { + let addr_str = "0xd388ab098ed3e84c0d808776440b48f685198498"; + let addr = parse_address(addr_str, Network::Mainnet).unwrap(); + + let exp_addr_str = "f410f2oekwcmo2pueydmaq53eic2i62crtbeyuzx2gmy"; + let exp_addr = parse_address(exp_addr_str, Network::Mainnet).unwrap(); + + assert_eq!(exp_addr, addr); + } + + #[test] + fn test_parse_eth_address_too_short() { + let addr_str = "0xd3"; + let e = parse_address(addr_str, Network::Mainnet).err().unwrap(); + + assert_eq!(e.to_string(), "Invalid address length"); + } + + #[test] + fn test_parse_eth_address_too_long() { + let addr_str = "0xd388ab098ed3e84c0d808776440b48f68519849812"; + let e = parse_address(addr_str, Network::Mainnet).err().unwrap(); + + assert_eq!(e.to_string(), "Invalid address length"); + } + + #[test] + fn test_parse_eth_address_invalid_chars() { + let addr_str = "0xd3!8ab098ed3e84c0d808776440b48f685198498"; + let e = parse_address(addr_str, Network::Mainnet).err().unwrap(); + + assert_eq!(e.to_string(), "Invalid characters in address"); + } +} diff --git a/src/faucet/views.rs b/src/faucet/views.rs index a1d8d247..c08761d8 100644 --- a/src/faucet/views.rs +++ b/src/faucet/views.rs @@ -9,10 +9,8 @@ use leptos_use::*; use super::utils::faucet_address; use crate::faucet::utils::sign_with_secret_key; use crate::{ - lotus_json::LotusJson, - message::message_transfer, - rpc_context::Provider, - utils::{catch_all, parse_address}, + address::parse_address, lotus_json::LotusJson, message::message_transfer, + rpc_context::Provider, utils::catch_all, }; #[component] @@ -64,7 +62,7 @@ pub fn Faucet(target_network: Network) -> impl IntoView { let target_balance = create_local_resource_with_initial_value( move || target_address.get(), move |address| async move { - if let Ok((address, _network)) = parse_address(&address) { + if let Ok(address) = parse_address(&address, target_network) { Provider::from_network(target_network) .wallet_balance(address) .await @@ -183,11 +181,8 @@ pub fn Faucet(target_network: Network) -> impl IntoView {