use alloy::primitives::{Address, Bytes, U256};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct EvmConfig {
pub rpc_url: String,
pub chain_id: u64,
pub timeout_seconds: u64,
pub max_gas_limit: Option<u64>,
pub gas_price_multiplier: Option<f64>,
}
impl Default for EvmConfig {
fn default() -> Self {
Self {
rpc_url: "https://eth.llamarpc.com".to_string(),
chain_id: 1, timeout_seconds: 30,
max_gas_limit: None,
gas_price_multiplier: Some(1.1), }
}
}
impl EvmConfig {
pub fn for_chain(chain_id: u64) -> Result<Self, crate::error::EvmCommonError> {
let rpc_url = crate::chain::chain_id_to_rpc_url(chain_id)?;
Ok(Self {
rpc_url,
chain_id,
timeout_seconds: 30,
max_gas_limit: None,
gas_price_multiplier: Some(1.1),
})
}
pub fn timeout(&self) -> Duration {
Duration::from_secs(self.timeout_seconds)
}
pub fn validate(&self) -> Result<(), crate::error::EvmCommonError> {
if self.rpc_url.is_empty() {
return Err(crate::error::EvmCommonError::InvalidConfig(
"RPC URL cannot be empty".to_string(),
));
}
if !self.rpc_url.starts_with("http://")
&& !self.rpc_url.starts_with("https://")
&& !self.rpc_url.starts_with("wss://")
{
return Err(crate::error::EvmCommonError::InvalidConfig(
"RPC URL must start with http://, https://, or wss://".to_string(),
));
}
if self.chain_id == 0 {
return Err(crate::error::EvmCommonError::InvalidConfig(
"Chain ID cannot be 0".to_string(),
));
}
if self.timeout_seconds == 0 {
return Err(crate::error::EvmCommonError::InvalidConfig(
"Timeout cannot be 0".to_string(),
));
}
if let Some(multiplier) = self.gas_price_multiplier {
if multiplier <= 0.0 {
return Err(crate::error::EvmCommonError::InvalidConfig(
"Gas price multiplier must be positive".to_string(),
));
}
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct EvmAccount {
pub address: String,
pub chain_id: u64,
pub name: Option<String>,
}
impl EvmAccount {
pub fn new(address: &str, chain_id: u64) -> Result<Self, crate::error::EvmCommonError> {
crate::address::validate_evm_address(address)?;
Ok(Self {
address: address.to_string(),
chain_id,
name: None,
})
}
pub fn with_name(
address: &str,
chain_id: u64,
name: String,
) -> Result<Self, crate::error::EvmCommonError> {
let mut account = Self::new(address, chain_id)?;
account.name = Some(name);
Ok(account)
}
pub fn to_address(&self) -> Result<Address, crate::error::EvmCommonError> {
crate::address::parse_evm_address(&self.address)
}
pub fn display_address(&self) -> Result<String, crate::error::EvmCommonError> {
let addr = self.to_address()?;
Ok(format!("0x{:x}", addr))
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct EvmTransactionData {
pub to: String,
pub data: String,
pub value: String,
pub gas_limit: String,
pub gas_price: String,
pub chain_id: u64,
}
impl EvmTransactionData {
pub fn new(
to: &str,
data: &str,
value: U256,
gas_limit: u64,
gas_price: U256,
chain_id: u64,
) -> Result<Self, crate::error::EvmCommonError> {
crate::address::validate_evm_address(to)?;
if !data.starts_with("0x") {
return Err(crate::error::EvmCommonError::InvalidData(
"Transaction data must start with 0x".to_string(),
));
}
Ok(Self {
to: to.to_string(),
data: data.to_string(),
value: format!("0x{:x}", value),
gas_limit: format!("0x{:x}", gas_limit),
gas_price: format!("0x{:x}", gas_price),
chain_id,
})
}
pub fn to_address(&self) -> Result<Address, crate::error::EvmCommonError> {
crate::address::parse_evm_address(&self.to)
}
pub fn data_bytes(&self) -> Result<Bytes, crate::error::EvmCommonError> {
let hex_str = self.data.strip_prefix("0x").unwrap_or(&self.data);
alloy::hex::decode(hex_str)
.map_err(|e| {
crate::error::EvmCommonError::InvalidData(format!("Invalid hex data: {}", e))
})
.map(Bytes::from)
}
pub fn value_u256(&self) -> Result<U256, crate::error::EvmCommonError> {
U256::from_str_radix(self.value.strip_prefix("0x").unwrap_or(&self.value), 16).map_err(
|e| crate::error::EvmCommonError::InvalidData(format!("Invalid value format: {}", e)),
)
}
pub fn gas_limit_u64(&self) -> Result<u64, crate::error::EvmCommonError> {
u64::from_str_radix(
self.gas_limit.strip_prefix("0x").unwrap_or(&self.gas_limit),
16,
)
.map_err(|e| {
crate::error::EvmCommonError::InvalidData(format!("Invalid gas limit format: {}", e))
})
}
pub fn gas_price_u256(&self) -> Result<U256, crate::error::EvmCommonError> {
U256::from_str_radix(
self.gas_price.strip_prefix("0x").unwrap_or(&self.gas_price),
16,
)
.map_err(|e| {
crate::error::EvmCommonError::InvalidData(format!("Invalid gas price format: {}", e))
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct EvmToken {
pub address: String,
pub symbol: String,
pub name: String,
pub decimals: u8,
pub chain_id: u64,
}
impl EvmToken {
pub fn new(
address: &str,
symbol: String,
name: String,
decimals: u8,
chain_id: u64,
) -> Result<Self, crate::error::EvmCommonError> {
if address != "0x0000000000000000000000000000000000000000" && address != "0x0" {
crate::address::validate_evm_address(address)?;
}
Ok(Self {
address: address.to_string(),
symbol,
name,
decimals,
chain_id,
})
}
pub fn is_native(&self) -> bool {
self.address == "0x0000000000000000000000000000000000000000" || self.address == "0x0"
}
pub fn contract_address(&self) -> Result<Option<Address>, crate::error::EvmCommonError> {
if self.is_native() {
Ok(None)
} else {
Ok(Some(crate::address::parse_evm_address(&self.address)?))
}
}
pub fn format_amount(&self, raw_amount: U256) -> String {
let divisor = U256::from(10).pow(U256::from(self.decimals));
let whole = raw_amount / divisor;
let remainder = raw_amount % divisor;
if remainder == U256::ZERO {
format!("{} {}", whole, self.symbol)
} else {
let decimal_str = format!("{:0width$}", remainder, width = self.decimals as usize);
let decimal_trimmed = decimal_str.trim_end_matches('0');
if decimal_trimmed.is_empty() {
format!("{} {}", whole, self.symbol)
} else {
format!("{}.{} {}", whole, decimal_trimmed, self.symbol)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_evm_config_validation() {
let mut config = EvmConfig::default();
assert!(config.validate().is_ok());
config.rpc_url = "invalid-url".to_string();
assert!(config.validate().is_err());
config.rpc_url = "https://eth.llamarpc.com".to_string();
assert!(config.validate().is_ok());
config.chain_id = 0;
assert!(config.validate().is_err());
}
#[test]
fn test_evm_account_creation() {
let account = EvmAccount::new("0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e", 1);
assert!(account.is_ok());
let account = account.unwrap();
assert_eq!(account.chain_id, 1);
assert!(account.name.is_none());
let account = EvmAccount::with_name(
"0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e",
1,
"Test".to_string(),
);
assert!(account.is_ok());
assert_eq!(account.unwrap().name, Some("Test".to_string()));
}
#[test]
fn test_evm_token() {
let eth = EvmToken::new("0x0", "ETH".to_string(), "Ethereum".to_string(), 18, 1);
assert!(eth.is_ok());
let eth = eth.unwrap();
assert!(eth.is_native());
let usdc = EvmToken::new(
"0xA0b86a33E6417c5d6d6bE6C2e0C6C3e5d6c7D8E9",
"USDC".to_string(),
"USD Coin".to_string(),
6,
1,
);
assert!(usdc.is_ok());
let usdc = usdc.unwrap();
assert!(!usdc.is_native());
let amount = U256::from(1_000_000u64); assert_eq!(usdc.format_amount(amount), "1 USDC");
let small_amount = U256::from(500_000u64); assert_eq!(usdc.format_amount(small_amount), "0.5 USDC");
}
#[test]
fn test_evm_config_default() {
let config = EvmConfig::default();
assert_eq!(config.rpc_url, "https://eth.llamarpc.com");
assert_eq!(config.chain_id, 1);
assert_eq!(config.timeout_seconds, 30);
assert_eq!(config.max_gas_limit, None);
assert_eq!(config.gas_price_multiplier, Some(1.1));
}
#[test]
fn test_evm_config_timeout() {
let config = EvmConfig {
rpc_url: "https://test.com".to_string(),
chain_id: 1,
timeout_seconds: 60,
max_gas_limit: None,
gas_price_multiplier: None,
};
assert_eq!(config.timeout(), Duration::from_secs(60));
}
#[test]
fn test_evm_config_validate_empty_rpc_url() {
let config = EvmConfig {
rpc_url: "".to_string(),
chain_id: 1,
timeout_seconds: 30,
max_gas_limit: None,
gas_price_multiplier: None,
};
let result = config.validate();
assert!(result.is_err());
if let Err(crate::error::EvmCommonError::InvalidConfig(msg)) = result {
assert_eq!(msg, "RPC URL cannot be empty");
}
}
#[test]
fn test_evm_config_validate_invalid_rpc_url_protocol() {
let config = EvmConfig {
rpc_url: "ftp://test.com".to_string(),
chain_id: 1,
timeout_seconds: 30,
max_gas_limit: None,
gas_price_multiplier: None,
};
let result = config.validate();
assert!(result.is_err());
if let Err(crate::error::EvmCommonError::InvalidConfig(msg)) = result {
assert_eq!(msg, "RPC URL must start with http://, https://, or wss://");
}
}
#[test]
fn test_evm_config_validate_zero_chain_id() {
let config = EvmConfig {
rpc_url: "https://test.com".to_string(),
chain_id: 0,
timeout_seconds: 30,
max_gas_limit: None,
gas_price_multiplier: None,
};
let result = config.validate();
assert!(result.is_err());
if let Err(crate::error::EvmCommonError::InvalidConfig(msg)) = result {
assert_eq!(msg, "Chain ID cannot be 0");
}
}
#[test]
fn test_evm_config_validate_zero_timeout() {
let config = EvmConfig {
rpc_url: "https://test.com".to_string(),
chain_id: 1,
timeout_seconds: 0,
max_gas_limit: None,
gas_price_multiplier: None,
};
let result = config.validate();
assert!(result.is_err());
if let Err(crate::error::EvmCommonError::InvalidConfig(msg)) = result {
assert_eq!(msg, "Timeout cannot be 0");
}
}
#[test]
fn test_evm_config_validate_negative_gas_price_multiplier() {
let config = EvmConfig {
rpc_url: "https://test.com".to_string(),
chain_id: 1,
timeout_seconds: 30,
max_gas_limit: None,
gas_price_multiplier: Some(-1.0),
};
let result = config.validate();
assert!(result.is_err());
if let Err(crate::error::EvmCommonError::InvalidConfig(msg)) = result {
assert_eq!(msg, "Gas price multiplier must be positive");
}
}
#[test]
fn test_evm_config_validate_zero_gas_price_multiplier() {
let config = EvmConfig {
rpc_url: "https://test.com".to_string(),
chain_id: 1,
timeout_seconds: 30,
max_gas_limit: None,
gas_price_multiplier: Some(0.0),
};
let result = config.validate();
assert!(result.is_err());
if let Err(crate::error::EvmCommonError::InvalidConfig(msg)) = result {
assert_eq!(msg, "Gas price multiplier must be positive");
}
}
#[test]
fn test_evm_config_validate_valid_http_url() {
let config = EvmConfig {
rpc_url: "http://test.com".to_string(),
chain_id: 1,
timeout_seconds: 30,
max_gas_limit: None,
gas_price_multiplier: Some(1.0),
};
assert!(config.validate().is_ok());
}
#[test]
fn test_evm_config_validate_valid_wss_url() {
let config = EvmConfig {
rpc_url: "wss://test.com".to_string(),
chain_id: 1,
timeout_seconds: 30,
max_gas_limit: None,
gas_price_multiplier: None,
};
assert!(config.validate().is_ok());
}
#[test]
fn test_evm_config_for_chain_success() {
match EvmConfig::for_chain(1) {
Ok(config) => {
assert_eq!(config.chain_id, 1);
assert_eq!(config.timeout_seconds, 30);
assert_eq!(config.gas_price_multiplier, Some(1.1));
}
Err(_) => {
}
}
}
#[test]
fn test_evm_config_for_chain_invalid_chain() {
let result = EvmConfig::for_chain(999999);
match result {
Ok(_) => {
}
Err(_) => {
}
}
}
#[test]
fn test_evm_account_new_invalid_address() {
let result = EvmAccount::new("invalid-address", 1);
assert!(result.is_err());
}
#[test]
fn test_evm_account_with_name_invalid_address() {
let result = EvmAccount::with_name("invalid-address", 1, "Test".to_string());
assert!(result.is_err());
}
#[test]
fn test_evm_account_to_address_success() {
let account = EvmAccount::new("0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e", 1).unwrap();
let address_result = account.to_address();
assert!(address_result.is_ok());
}
#[test]
fn test_evm_account_display_address_success() {
let account = EvmAccount::new("0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e", 1).unwrap();
let display_result = account.display_address();
assert!(display_result.is_ok());
let display = display_result.unwrap();
assert!(display.starts_with("0x"));
}
#[test]
fn test_evm_transaction_data_new_success() {
let result = EvmTransactionData::new(
"0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e",
"0x1234",
U256::from(1000),
21000,
U256::from(20000000000u64),
1,
);
assert!(result.is_ok());
let tx_data = result.unwrap();
assert_eq!(tx_data.to, "0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e");
assert_eq!(tx_data.data, "0x1234");
assert_eq!(tx_data.value, "0x3e8");
assert_eq!(tx_data.gas_limit, "0x5208");
assert_eq!(tx_data.chain_id, 1);
}
#[test]
fn test_evm_transaction_data_new_invalid_address() {
let result = EvmTransactionData::new(
"invalid-address",
"0x1234",
U256::from(1000),
21000,
U256::from(20000000000u64),
1,
);
assert!(result.is_err());
}
#[test]
fn test_evm_transaction_data_new_invalid_data_format() {
let result = EvmTransactionData::new(
"0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e",
"1234", U256::from(1000),
21000,
U256::from(20000000000u64),
1,
);
assert!(result.is_err());
if let Err(crate::error::EvmCommonError::InvalidData(msg)) = result {
assert_eq!(msg, "Transaction data must start with 0x");
}
}
#[test]
fn test_evm_transaction_data_to_address() {
let tx_data = EvmTransactionData::new(
"0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e",
"0x1234",
U256::from(1000),
21000,
U256::from(20000000000u64),
1,
)
.unwrap();
let address_result = tx_data.to_address();
assert!(address_result.is_ok());
}
#[test]
fn test_evm_transaction_data_data_bytes() {
let tx_data = EvmTransactionData::new(
"0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e",
"0x1234",
U256::from(1000),
21000,
U256::from(20000000000u64),
1,
)
.unwrap();
let bytes_result = tx_data.data_bytes();
assert!(bytes_result.is_ok());
let bytes = bytes_result.unwrap();
assert_eq!(bytes.len(), 2); }
#[test]
fn test_evm_transaction_data_data_bytes_no_prefix() {
let mut tx_data = EvmTransactionData::new(
"0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e",
"0x1234",
U256::from(1000),
21000,
U256::from(20000000000u64),
1,
)
.unwrap();
tx_data.data = "abcd".to_string(); let bytes_result = tx_data.data_bytes();
assert!(bytes_result.is_ok());
}
#[test]
fn test_evm_transaction_data_data_bytes_invalid_hex() {
let mut tx_data = EvmTransactionData::new(
"0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e",
"0x1234",
U256::from(1000),
21000,
U256::from(20000000000u64),
1,
)
.unwrap();
tx_data.data = "0xgggg".to_string(); let bytes_result = tx_data.data_bytes();
assert!(bytes_result.is_err());
}
#[test]
fn test_evm_transaction_data_value_u256() {
let tx_data = EvmTransactionData::new(
"0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e",
"0x1234",
U256::from(1000),
21000,
U256::from(20000000000u64),
1,
)
.unwrap();
let value_result = tx_data.value_u256();
assert!(value_result.is_ok());
assert_eq!(value_result.unwrap(), U256::from(1000));
}
#[test]
fn test_evm_transaction_data_value_u256_no_prefix() {
let mut tx_data = EvmTransactionData::new(
"0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e",
"0x1234",
U256::from(1000),
21000,
U256::from(20000000000u64),
1,
)
.unwrap();
tx_data.value = "3e8".to_string(); let value_result = tx_data.value_u256();
assert!(value_result.is_ok());
assert_eq!(value_result.unwrap(), U256::from(1000));
}
#[test]
fn test_evm_transaction_data_value_u256_invalid() {
let mut tx_data = EvmTransactionData::new(
"0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e",
"0x1234",
U256::from(1000),
21000,
U256::from(20000000000u64),
1,
)
.unwrap();
tx_data.value = "0xgggg".to_string(); let value_result = tx_data.value_u256();
assert!(value_result.is_err());
}
#[test]
fn test_evm_transaction_data_gas_limit_u64() {
let tx_data = EvmTransactionData::new(
"0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e",
"0x1234",
U256::from(1000),
21000,
U256::from(20000000000u64),
1,
)
.unwrap();
let gas_limit_result = tx_data.gas_limit_u64();
assert!(gas_limit_result.is_ok());
assert_eq!(gas_limit_result.unwrap(), 21000);
}
#[test]
fn test_evm_transaction_data_gas_limit_u64_no_prefix() {
let mut tx_data = EvmTransactionData::new(
"0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e",
"0x1234",
U256::from(1000),
21000,
U256::from(20000000000u64),
1,
)
.unwrap();
tx_data.gas_limit = "5208".to_string(); let gas_limit_result = tx_data.gas_limit_u64();
assert!(gas_limit_result.is_ok());
assert_eq!(gas_limit_result.unwrap(), 21000);
}
#[test]
fn test_evm_transaction_data_gas_limit_u64_invalid() {
let mut tx_data = EvmTransactionData::new(
"0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e",
"0x1234",
U256::from(1000),
21000,
U256::from(20000000000u64),
1,
)
.unwrap();
tx_data.gas_limit = "0xgggg".to_string(); let gas_limit_result = tx_data.gas_limit_u64();
assert!(gas_limit_result.is_err());
}
#[test]
fn test_evm_transaction_data_gas_price_u256() {
let tx_data = EvmTransactionData::new(
"0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e",
"0x1234",
U256::from(1000),
21000,
U256::from(20000000000u64),
1,
)
.unwrap();
let gas_price_result = tx_data.gas_price_u256();
assert!(gas_price_result.is_ok());
assert_eq!(gas_price_result.unwrap(), U256::from(20000000000u64));
}
#[test]
fn test_evm_transaction_data_gas_price_u256_no_prefix() {
let mut tx_data = EvmTransactionData::new(
"0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e",
"0x1234",
U256::from(1000),
21000,
U256::from(20000000000u64),
1,
)
.unwrap();
tx_data.gas_price = "4a817c800".to_string(); let gas_price_result = tx_data.gas_price_u256();
assert!(gas_price_result.is_ok());
assert_eq!(gas_price_result.unwrap(), U256::from(20000000000u64));
}
#[test]
fn test_evm_transaction_data_gas_price_u256_invalid() {
let mut tx_data = EvmTransactionData::new(
"0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e",
"0x1234",
U256::from(1000),
21000,
U256::from(20000000000u64),
1,
)
.unwrap();
tx_data.gas_price = "0xgggg".to_string(); let gas_price_result = tx_data.gas_price_u256();
assert!(gas_price_result.is_err());
}
#[test]
fn test_evm_token_native_with_long_zero_address() {
let token = EvmToken::new(
"0x0000000000000000000000000000000000000000",
"ETH".to_string(),
"Ethereum".to_string(),
18,
1,
)
.unwrap();
assert!(token.is_native());
}
#[test]
fn test_evm_token_native_with_short_zero_address() {
let token = EvmToken::new("0x0", "ETH".to_string(), "Ethereum".to_string(), 18, 1).unwrap();
assert!(token.is_native());
}
#[test]
fn test_evm_token_contract_address_native() {
let token = EvmToken::new("0x0", "ETH".to_string(), "Ethereum".to_string(), 18, 1).unwrap();
let contract_address = token.contract_address().unwrap();
assert!(contract_address.is_none());
}
#[test]
fn test_evm_token_contract_address_erc20() {
let token = EvmToken::new(
"0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e",
"TEST".to_string(),
"Test Token".to_string(),
18,
1,
)
.unwrap();
let contract_address = token.contract_address().unwrap();
assert!(contract_address.is_some());
}
#[test]
fn test_evm_token_format_amount_zero() {
let token = EvmToken::new(
"0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e",
"TEST".to_string(),
"Test Token".to_string(),
18,
1,
)
.unwrap();
let formatted = token.format_amount(U256::ZERO);
assert_eq!(formatted, "0 TEST");
}
#[test]
fn test_evm_token_format_amount_whole_number() {
let token = EvmToken::new(
"0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e",
"TEST".to_string(),
"Test Token".to_string(),
18,
1,
)
.unwrap();
let amount = U256::from(10).pow(U256::from(18)) * U256::from(5); let formatted = token.format_amount(amount);
assert_eq!(formatted, "5 TEST");
}
#[test]
fn test_evm_token_format_amount_decimal() {
let token = EvmToken::new(
"0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e",
"TEST".to_string(),
"Test Token".to_string(),
6, 1,
)
.unwrap();
let amount = U256::from(1_500_000); let formatted = token.format_amount(amount);
assert_eq!(formatted, "1.5 TEST");
}
#[test]
fn test_evm_token_format_amount_trailing_zeros_trimmed() {
let token = EvmToken::new(
"0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e",
"TEST".to_string(),
"Test Token".to_string(),
6,
1,
)
.unwrap();
let amount = U256::from(1_200_000); let formatted = token.format_amount(amount);
assert_eq!(formatted, "1.2 TEST");
}
#[test]
fn test_evm_token_format_amount_small_decimals() {
let token = EvmToken::new(
"0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e",
"TEST".to_string(),
"Test Token".to_string(),
18,
1,
)
.unwrap();
let amount = U256::from(1); let formatted = token.format_amount(amount);
assert_eq!(formatted, "0.000000000000000001 TEST");
}
#[test]
fn test_evm_token_new_invalid_address() {
let result = EvmToken::new(
"invalid-address",
"TEST".to_string(),
"Test Token".to_string(),
18,
1,
);
assert!(result.is_err());
}
}