use alloy::primitives::Address;
use std::str::FromStr;
use crate::onchain::error::{OnchainError, Result};
#[derive(Debug, Clone)]
pub struct NetworkConfig {
pub chain_id: u64,
pub rpc_url: String,
pub contracts: ContractAddresses,
}
#[derive(Debug, Clone)]
pub struct ContractAddresses {
pub proxy_factory: Address,
pub safe_factory: Address,
pub usdc: Address,
pub ctf: Address,
pub neg_risk_adapter: Address,
pub exchange: Address,
pub safe_singleton: Address,
}
impl NetworkConfig {
pub fn polygon_mainnet() -> Self {
Self {
chain_id: 137,
rpc_url: "https://polygon-rpc.com".to_string(),
contracts: ContractAddresses::polygon_mainnet(),
}
}
pub fn polygon_amoy() -> Self {
Self {
chain_id: 80002,
rpc_url: "https://rpc-amoy.polygon.technology".to_string(),
contracts: ContractAddresses::polygon_amoy(),
}
}
pub fn custom(chain_id: u64, rpc_url: String, contracts: ContractAddresses) -> Self {
Self {
chain_id,
rpc_url,
contracts,
}
}
pub fn with_rpc_url(mut self, rpc_url: String) -> Self {
self.rpc_url = rpc_url;
self
}
}
impl Default for NetworkConfig {
fn default() -> Self {
Self::polygon_mainnet()
}
}
impl ContractAddresses {
pub fn polygon_mainnet() -> Self {
Self {
proxy_factory: parse_address("0xaB45c5A4B0c941a2F231C04C3f49182e1A254052"),
safe_factory: parse_address("0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2"),
usdc: parse_address("0x2791bca1f2de4661ed88a30c99a7a9449aa84174"),
ctf: parse_address("0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"),
neg_risk_adapter: parse_address("0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296"),
exchange: parse_address("0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E"),
safe_singleton: parse_address("0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552"),
}
}
pub fn polygon_amoy() -> Self {
Self {
proxy_factory: parse_address("0x0000000000000000000000000000000000000000"), safe_factory: parse_address("0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2"), usdc: parse_address("0x9c4e1703476e875070ee25b56a58b008cfb8fa78"), ctf: parse_address("0x69308FB512518e39F9b16112fA8d994F4e2Bf8bB"),
neg_risk_adapter: parse_address("0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296"),
exchange: parse_address("0xdFE02Eb6733538f8Ea35D585af8DE5958AD99E40"),
safe_singleton: parse_address("0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552"),
}
}
}
fn parse_address(addr: &str) -> Address {
Address::from_str(addr).expect("Invalid hardcoded address")
}
pub const USDC_DECIMALS: u8 = 6;
pub fn parse_usdc(amount: &str) -> Result<alloy::primitives::U256> {
let parts: Vec<&str> = amount.split('.').collect();
match parts.len() {
1 => {
let dollars: u64 = parts[0].parse().map_err(|e| {
OnchainError::InvalidAmount(format!("Invalid USDC amount '{}': {}", amount, e))
})?;
Ok(alloy::primitives::U256::from(dollars) * alloy::primitives::U256::from(1_000_000))
}
2 => {
let dollars: u64 = if parts[0].is_empty() {
0
} else {
parts[0].parse().map_err(|e| {
OnchainError::InvalidAmount(format!("Invalid USDC amount '{}': {}", amount, e))
})?
};
let cents_str = parts[1];
if cents_str.len() > 6 {
return Err(OnchainError::InvalidAmount(
format!("Too many decimal places in '{}' (max 6)", amount)
));
}
let cents_padded = format!("{:0<6}", cents_str); let cents: u64 = cents_padded.parse().map_err(|e| {
OnchainError::InvalidAmount(format!("Invalid USDC amount '{}': {}", amount, e))
})?;
Ok(alloy::primitives::U256::from(dollars) * alloy::primitives::U256::from(1_000_000)
+ alloy::primitives::U256::from(cents))
}
_ => Err(OnchainError::InvalidAmount(format!(
"Invalid USDC amount format: '{}'",
amount
))),
}
}
pub fn format_usdc(amount: alloy::primitives::U256) -> String {
let raw = amount.to_string();
let len = raw.len();
if len <= 6 {
format!("0.{:0>6}", raw)
} else {
let dollars = &raw[..len - 6];
let cents = &raw[len - 6..];
format!("{}.{}", dollars, cents)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_polygon_mainnet_config() {
let config = NetworkConfig::polygon_mainnet();
assert_eq!(config.chain_id, 137);
assert!(config.contracts.usdc.to_string().starts_with("0x"));
}
#[test]
fn test_polygon_amoy_config() {
let config = NetworkConfig::polygon_amoy();
assert_eq!(config.chain_id, 80002);
}
#[test]
fn test_custom_config() {
let contracts = ContractAddresses::polygon_mainnet();
let config = NetworkConfig::custom(999, "http://localhost:8545".to_string(), contracts);
assert_eq!(config.chain_id, 999);
assert_eq!(config.rpc_url, "http://localhost:8545");
}
#[test]
fn test_parse_usdc_whole() {
let amount = parse_usdc("10").unwrap();
assert_eq!(amount, alloy::primitives::U256::from(10_000_000));
}
#[test]
fn test_parse_usdc_decimal() {
let amount = parse_usdc("10.5").unwrap();
assert_eq!(amount, alloy::primitives::U256::from(10_500_000));
}
#[test]
fn test_parse_usdc_full_precision() {
let amount = parse_usdc("10.123456").unwrap();
assert_eq!(amount, alloy::primitives::U256::from(10_123_456));
}
#[test]
fn test_parse_usdc_leading_zero() {
let amount = parse_usdc("0.5").unwrap();
assert_eq!(amount, alloy::primitives::U256::from(500_000));
}
#[test]
fn test_parse_usdc_no_leading() {
let amount = parse_usdc(".5").unwrap();
assert_eq!(amount, alloy::primitives::U256::from(500_000));
}
#[test]
fn test_parse_usdc_too_many_decimals() {
let result = parse_usdc("10.1234567");
assert!(result.is_err());
}
#[test]
fn test_format_usdc_whole() {
let formatted = format_usdc(alloy::primitives::U256::from(10_000_000));
assert_eq!(formatted, "10.000000");
}
#[test]
fn test_format_usdc_decimal() {
let formatted = format_usdc(alloy::primitives::U256::from(10_500_000));
assert_eq!(formatted, "10.500000");
}
#[test]
fn test_format_usdc_small() {
let formatted = format_usdc(alloy::primitives::U256::from(123));
assert_eq!(formatted, "0.000123");
}
#[test]
fn test_parse_format_roundtrip() {
let original = "10.123456";
let parsed = parse_usdc(original).unwrap();
let formatted = format_usdc(parsed);
assert_eq!(formatted, "10.123456");
}
}