From e0d35fe6dbe860aa375655993afd9702259ed009 Mon Sep 17 00:00:00 2001 From: Warm Beer Date: Thu, 9 Apr 2026 10:54:17 +0200 Subject: [PATCH 1/2] feat: add custom EVM network support for local Anvil testnets Introduces --evm-rpc-url, --evm-payment-token, and --evm-payment-vault CLI flags that override --evm-network, allowing nodes to point at a private Anvil instance with operator-deployed contracts instead of the public Arbitrum networks. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/bin/ant-node/cli.rs | 38 +++++++++++++++++++++++++++++++++++++- src/config.rs | 20 ++++++++++++++++++-- src/node.rs | 7 ++++++- src/payment/wallet.rs | 17 +++++++++++------ 4 files changed, 72 insertions(+), 10 deletions(-) diff --git a/src/bin/ant-node/cli.rs b/src/bin/ant-node/cli.rs index 8eb9fa51..b1d68c66 100644 --- a/src/bin/ant-node/cli.rs +++ b/src/bin/ant-node/cli.rs @@ -50,6 +50,9 @@ pub struct Cli { pub rewards_address: Option, /// EVM network for payment processing. + /// + /// Ignored when `--evm-rpc-url` is set (which selects a custom EVM + /// network instead — used by the local-Anvil testnet flow). #[arg( long, value_enum, @@ -58,6 +61,22 @@ pub struct Cli { )] pub evm_network: CliEvmNetwork, + /// HTTP RPC URL of a custom EVM (e.g. a local Anvil instance). + /// When set, --evm-payment-token and --evm-payment-vault must also + /// be set, and they together override --evm-network. + #[arg(long, env = "ANT_EVM_RPC_URL")] + pub evm_rpc_url: Option, + + /// ANT token contract address on the custom EVM. + /// Required iff --evm-rpc-url is set. + #[arg(long, env = "ANT_EVM_PAYMENT_TOKEN")] + pub evm_payment_token: Option, + + /// Payment vault contract address on the custom EVM. + /// Required iff --evm-rpc-url is set. + #[arg(long, env = "ANT_EVM_PAYMENT_VAULT")] + pub evm_payment_vault: Option, + /// Metrics port for Prometheus scraping (0 to disable). #[arg(long, default_value = "9100", env = "ANT_METRICS_PORT")] pub metrics_port: u16, @@ -239,10 +258,27 @@ impl Cli { config.upgrade.stop_on_upgrade = self.stop_on_upgrade; // Payment config (payment verification is always on) + // Custom EVM (--evm-rpc-url) overrides the --evm-network preset. + let evm_network = if let Some(rpc_url) = self.evm_rpc_url { + let payment_token_address = self.evm_payment_token.ok_or_else(|| { + color_eyre::eyre::eyre!("--evm-payment-token is required when --evm-rpc-url is set") + })?; + let payment_vault_address = self.evm_payment_vault.ok_or_else(|| { + color_eyre::eyre::eyre!("--evm-payment-vault is required when --evm-rpc-url is set") + })?; + EvmNetworkConfig::Custom { + rpc_url, + payment_token_address, + payment_vault_address, + } + } else { + self.evm_network.into() + }; + config.payment = PaymentConfig { cache_capacity: self.cache_capacity, rewards_address: self.rewards_address, - evm_network: self.evm_network.into(), + evm_network, metrics_port: self.metrics_port, }; diff --git a/src/config.rs b/src/config.rs index a9f569c3..dbf0880d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -182,14 +182,30 @@ pub struct UpgradeConfig { } /// EVM network for payment processing. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] +/// +/// `Custom` is used by the local-Anvil testnet flow in +/// `deploy/testnet-v2/`: when an operator stands up a private Anvil +/// instance with the ANT token + payment vault contracts deployed, +/// every node points at the Anvil RPC and the deployed addresses +/// instead of one of the public Arbitrum networks. +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case", tag = "type")] pub enum EvmNetworkConfig { /// Arbitrum One mainnet. #[default] ArbitrumOne, /// Arbitrum Sepolia testnet. ArbitrumSepolia, + /// Local / private EVM (e.g. Anvil) with operator-supplied + /// contract addresses. + Custom { + /// HTTP RPC URL of the EVM node (e.g. `http://1.2.3.4:8545`). + rpc_url: String, + /// Deployed ANT token (ERC-20) contract address. + payment_token_address: String, + /// Deployed payment vault contract address. + payment_vault_address: String, + }, } /// Payment verification configuration. diff --git a/src/node.rs b/src/node.rs index 4dc2a6d8..362eaa9d 100644 --- a/src/node.rs +++ b/src/node.rs @@ -375,9 +375,14 @@ impl NodeBuilder { }; // Create payment verifier - let evm_network = match config.payment.evm_network { + let evm_network = match &config.payment.evm_network { EvmNetworkConfig::ArbitrumOne => EvmNetwork::ArbitrumOne, EvmNetworkConfig::ArbitrumSepolia => EvmNetwork::ArbitrumSepoliaTest, + EvmNetworkConfig::Custom { + rpc_url, + payment_token_address, + payment_vault_address, + } => EvmNetwork::new_custom(rpc_url, payment_token_address, payment_vault_address), }; let payment_config = PaymentVerifierConfig { evm: EvmVerifierConfig { diff --git a/src/payment/wallet.rs b/src/payment/wallet.rs index 7ec53dff..5592d52c 100644 --- a/src/payment/wallet.rs +++ b/src/payment/wallet.rs @@ -28,12 +28,17 @@ impl WalletConfig { /// # Errors /// /// Returns an error if the address string is invalid. - pub fn new(rewards_address: Option<&str>, evm_network: EvmNetworkConfig) -> Result { + pub fn new(rewards_address: Option<&str>, evm_network: &EvmNetworkConfig) -> Result { let rewards_address = rewards_address.map(parse_rewards_address).transpose()?; let network = match evm_network { EvmNetworkConfig::ArbitrumOne => EvmNetwork::ArbitrumOne, EvmNetworkConfig::ArbitrumSepolia => EvmNetwork::ArbitrumSepoliaTest, + EvmNetworkConfig::Custom { + rpc_url, + payment_token_address, + payment_vault_address, + } => EvmNetwork::new_custom(rpc_url, payment_token_address, payment_vault_address), }; Ok(Self { @@ -170,7 +175,7 @@ mod tests { fn test_wallet_config_new() { let config = WalletConfig::new( Some("0x742d35Cc6634C0532925a3b844Bc9e7595916Da2"), - EvmNetworkConfig::ArbitrumSepolia, + &EvmNetworkConfig::ArbitrumSepolia, ); assert!(config.is_ok()); let config = config.expect("valid config"); @@ -180,7 +185,7 @@ mod tests { #[test] fn test_wallet_config_no_address() { - let config = WalletConfig::new(None, EvmNetworkConfig::ArbitrumOne); + let config = WalletConfig::new(None, &EvmNetworkConfig::ArbitrumOne); assert!(config.is_ok()); let config = config.expect("valid config"); assert!(!config.has_rewards_address()); @@ -216,7 +221,7 @@ mod tests { #[test] fn test_get_rewards_address_none() { - let config = WalletConfig::new(None, EvmNetworkConfig::ArbitrumOne).expect("valid config"); + let config = WalletConfig::new(None, &EvmNetworkConfig::ArbitrumOne).expect("valid config"); assert!(config.get_rewards_address().is_none()); } @@ -224,7 +229,7 @@ mod tests { fn test_get_rewards_address_some() { let config = WalletConfig::new( Some("0x742d35Cc6634C0532925a3b844Bc9e7595916Da2"), - EvmNetworkConfig::ArbitrumOne, + &EvmNetworkConfig::ArbitrumOne, ) .expect("valid config"); assert!(config.get_rewards_address().is_some()); @@ -255,7 +260,7 @@ mod tests { #[test] fn test_wallet_config_invalid_address() { - let result = WalletConfig::new(Some("invalid"), EvmNetworkConfig::ArbitrumOne); + let result = WalletConfig::new(Some("invalid"), &EvmNetworkConfig::ArbitrumOne); assert!(result.is_err()); } } From b63a7d8eef2232cd7f18a0b9ef6ac58070d72e22 Mon Sep 17 00:00:00 2001 From: Warm Beer Date: Wed, 15 Apr 2026 12:11:51 +0200 Subject: [PATCH 2/2] refactor: centralize EvmNetworkConfig -> EvmNetwork mapping Addresses Copilot review feedback on PR #72. - Add `EvmNetworkConfig::into_evm_network(self)` as the single place that resolves a config into the concrete `evmlib::Network`, removing the duplicated match arms in `node.rs` and `payment/wallet.rs`. - Revert `WalletConfig::new` to take `EvmNetworkConfig` by value, restoring the pre-existing API. Consuming `self` in `into_evm_network` keeps clippy's `needless_pass_by_value` happy. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/config.rs | 18 ++++++++++++++++++ src/node.rs | 14 ++------------ src/payment/wallet.rs | 23 +++++++---------------- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/src/config.rs b/src/config.rs index dbf0880d..e1d9d743 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,6 @@ //! Configuration for ant-node. +use evmlib::Network as EvmNetwork; use serde::{Deserialize, Serialize}; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; use std::path::{Path, PathBuf}; @@ -208,6 +209,23 @@ pub enum EvmNetworkConfig { }, } +impl EvmNetworkConfig { + /// Resolve this config into the concrete `evmlib` network used by + /// the payment verifier and the rewards wallet. + #[must_use] + pub fn into_evm_network(self) -> EvmNetwork { + match self { + Self::ArbitrumOne => EvmNetwork::ArbitrumOne, + Self::ArbitrumSepolia => EvmNetwork::ArbitrumSepoliaTest, + Self::Custom { + rpc_url, + payment_token_address, + payment_vault_address, + } => EvmNetwork::new_custom(&rpc_url, &payment_token_address, &payment_vault_address), + } + } +} + /// Payment verification configuration. /// /// All new data requires EVM payment on Arbitrum — there is no way to diff --git a/src/node.rs b/src/node.rs index 362eaa9d..43d58298 100644 --- a/src/node.rs +++ b/src/node.rs @@ -2,8 +2,7 @@ use crate::ant_protocol::CHUNK_PROTOCOL_ID; use crate::config::{ - default_nodes_dir, default_root_dir, EvmNetworkConfig, NetworkMode, NodeConfig, - NODE_IDENTITY_FILENAME, + default_nodes_dir, default_root_dir, NetworkMode, NodeConfig, NODE_IDENTITY_FILENAME, }; use crate::error::{Error, Result}; use crate::event::{create_event_channel, NodeEvent, NodeEventsChannel, NodeEventsSender}; @@ -18,7 +17,6 @@ use crate::storage::{AntProtocol, LmdbStorage, LmdbStorageConfig}; use crate::upgrade::{ upgrade_cache_dir, AutoApplyUpgrader, BinaryCache, ReleaseCache, UpgradeMonitor, UpgradeResult, }; -use evmlib::Network as EvmNetwork; use rand::Rng; use saorsa_core::identity::NodeIdentity; use saorsa_core::{ @@ -375,15 +373,7 @@ impl NodeBuilder { }; // Create payment verifier - let evm_network = match &config.payment.evm_network { - EvmNetworkConfig::ArbitrumOne => EvmNetwork::ArbitrumOne, - EvmNetworkConfig::ArbitrumSepolia => EvmNetwork::ArbitrumSepoliaTest, - EvmNetworkConfig::Custom { - rpc_url, - payment_token_address, - payment_vault_address, - } => EvmNetwork::new_custom(rpc_url, payment_token_address, payment_vault_address), - }; + let evm_network = config.payment.evm_network.clone().into_evm_network(); let payment_config = PaymentVerifierConfig { evm: EvmVerifierConfig { network: evm_network, diff --git a/src/payment/wallet.rs b/src/payment/wallet.rs index 5592d52c..756aedf5 100644 --- a/src/payment/wallet.rs +++ b/src/payment/wallet.rs @@ -28,18 +28,9 @@ impl WalletConfig { /// # Errors /// /// Returns an error if the address string is invalid. - pub fn new(rewards_address: Option<&str>, evm_network: &EvmNetworkConfig) -> Result { + pub fn new(rewards_address: Option<&str>, evm_network: EvmNetworkConfig) -> Result { let rewards_address = rewards_address.map(parse_rewards_address).transpose()?; - - let network = match evm_network { - EvmNetworkConfig::ArbitrumOne => EvmNetwork::ArbitrumOne, - EvmNetworkConfig::ArbitrumSepolia => EvmNetwork::ArbitrumSepoliaTest, - EvmNetworkConfig::Custom { - rpc_url, - payment_token_address, - payment_vault_address, - } => EvmNetwork::new_custom(rpc_url, payment_token_address, payment_vault_address), - }; + let network = evm_network.into_evm_network(); Ok(Self { rewards_address, @@ -175,7 +166,7 @@ mod tests { fn test_wallet_config_new() { let config = WalletConfig::new( Some("0x742d35Cc6634C0532925a3b844Bc9e7595916Da2"), - &EvmNetworkConfig::ArbitrumSepolia, + EvmNetworkConfig::ArbitrumSepolia, ); assert!(config.is_ok()); let config = config.expect("valid config"); @@ -185,7 +176,7 @@ mod tests { #[test] fn test_wallet_config_no_address() { - let config = WalletConfig::new(None, &EvmNetworkConfig::ArbitrumOne); + let config = WalletConfig::new(None, EvmNetworkConfig::ArbitrumOne); assert!(config.is_ok()); let config = config.expect("valid config"); assert!(!config.has_rewards_address()); @@ -221,7 +212,7 @@ mod tests { #[test] fn test_get_rewards_address_none() { - let config = WalletConfig::new(None, &EvmNetworkConfig::ArbitrumOne).expect("valid config"); + let config = WalletConfig::new(None, EvmNetworkConfig::ArbitrumOne).expect("valid config"); assert!(config.get_rewards_address().is_none()); } @@ -229,7 +220,7 @@ mod tests { fn test_get_rewards_address_some() { let config = WalletConfig::new( Some("0x742d35Cc6634C0532925a3b844Bc9e7595916Da2"), - &EvmNetworkConfig::ArbitrumOne, + EvmNetworkConfig::ArbitrumOne, ) .expect("valid config"); assert!(config.get_rewards_address().is_some()); @@ -260,7 +251,7 @@ mod tests { #[test] fn test_wallet_config_invalid_address() { - let result = WalletConfig::new(Some("invalid"), &EvmNetworkConfig::ArbitrumOne); + let result = WalletConfig::new(Some("invalid"), EvmNetworkConfig::ArbitrumOne); assert!(result.is_err()); } }