From 8c7a5472c31f3d3c2f575cb3c5d9558b13ec6bbf Mon Sep 17 00:00:00 2001 From: Koh Wei Jie Date: Fri, 20 Feb 2026 22:58:11 +0000 Subject: [PATCH 1/5] Ethereum Sepolia support for auth server and funds manager - Add Ethereum gas oracle and Sepolia gas estimation - Fix CachedSponsorshipInfo f64 serialization for HMAC round-trip - Add configurable gas refill amounts, tolerance, and limits - Add Ethereum/Sepolia withdrawal and hot wallet support - Add funds manager gas refill and wallet management scripts - Add Ethereum Sepolia deploy config Co-Authored-By: Claude Opus 4.6 --- auth/auth-server/Cargo.toml | 1 + .../gas_estimation/gas_oracles/ethereum.rs | 27 ++++ .../server/gas_estimation/gas_oracles/mod.rs | 5 + .../src/server/gas_sponsorship/mod.rs | 125 ++++++++++++++++++ funds-manager/funds-manager-server/src/cli.rs | 48 +++++++ .../src/custody_client/gas_sponsor.rs | 5 +- .../src/custody_client/gas_wallets.rs | 26 +++- .../src/custody_client/hot_wallets.rs | 5 +- .../src/custody_client/mod.rs | 53 +++++++- .../src/custody_client/withdraw.rs | 4 +- .../funds-manager-server/src/handlers/gas.rs | 26 ++-- .../funds-manager-server/src/main.rs | 71 ++++++---- .../funds-manager-server/src/server.rs | 1 + funds-manager/scripts/create_gas_wallets.py | 55 ++++++++ .../scripts/fireblocks_list_vault_accounts.py | 57 ++++++++ funds-manager/scripts/refill_gas.py | 58 ++++++++ funds-manager/scripts/requirements.txt | 10 ++ renegade-deploy-config.toml | 10 ++ 18 files changed, 528 insertions(+), 59 deletions(-) create mode 100644 auth/auth-server/src/server/gas_estimation/gas_oracles/ethereum.rs create mode 100644 funds-manager/scripts/create_gas_wallets.py create mode 100644 funds-manager/scripts/fireblocks_list_vault_accounts.py create mode 100644 funds-manager/scripts/refill_gas.py create mode 100644 funds-manager/scripts/requirements.txt diff --git a/auth/auth-server/Cargo.toml b/auth/auth-server/Cargo.toml index 93fa195c..fd682f38 100644 --- a/auth/auth-server/Cargo.toml +++ b/auth/auth-server/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [features] arbitrum = [] base = [] +ethereum = [] [dependencies] # === HTTP Server === # diff --git a/auth/auth-server/src/server/gas_estimation/gas_oracles/ethereum.rs b/auth/auth-server/src/server/gas_estimation/gas_oracles/ethereum.rs new file mode 100644 index 00000000..b0cfcec5 --- /dev/null +++ b/auth/auth-server/src/server/gas_estimation/gas_oracles/ethereum.rs @@ -0,0 +1,27 @@ +//! Ethereum L1 gas estimation +//! +//! On Ethereum L1, there is no separate L1 data posting cost (unlike L2s). +//! The total gas cost is simply execution gas * gas price. + +use alloy::primitives::{Address, U256}; +use alloy::providers::{DynProvider, Provider}; + +use super::GasPriceEstimation; + +/// Estimate the gas cost for a transaction on Ethereum L1 +/// +/// Unlike L2s (Arbitrum/Base), there is no separate L1 data posting cost. +/// The total cost is simply execution gas * gas price. +pub async fn estimate_l1_gas_component( + provider: DynProvider, + _to: Address, + _data: Vec, +) -> Result { + let gas_price = provider.get_gas_price().await.map_err(|e| e.to_string())?; + + Ok(GasPriceEstimation { + gas_estimate_for_l1: U256::ZERO, + l2_base_fee: U256::from(gas_price), + l1_data_fee: U256::ZERO, + }) +} diff --git a/auth/auth-server/src/server/gas_estimation/gas_oracles/mod.rs b/auth/auth-server/src/server/gas_estimation/gas_oracles/mod.rs index 67f6946a..f0edb78e 100644 --- a/auth/auth-server/src/server/gas_estimation/gas_oracles/mod.rs +++ b/auth/auth-server/src/server/gas_estimation/gas_oracles/mod.rs @@ -11,6 +11,11 @@ pub use arbitrum::estimate_l1_gas_component; #[cfg(feature = "base")] pub use base::estimate_l1_gas_component; +#[cfg(feature = "ethereum")] +mod ethereum; +#[cfg(feature = "ethereum")] +pub use ethereum::estimate_l1_gas_component; + /// Result of the gas price estimation pub struct GasPriceEstimation { /// The L1 gas estimate in L2 gas units diff --git a/auth/auth-server/src/server/gas_sponsorship/mod.rs b/auth/auth-server/src/server/gas_sponsorship/mod.rs index 8f5e12be..eb39ca8d 100644 --- a/auth/auth-server/src/server/gas_sponsorship/mod.rs +++ b/auth/auth-server/src/server/gas_sponsorship/mod.rs @@ -41,6 +41,7 @@ pub(crate) struct CachedSponsorshipInfo { pub gas_sponsorship_info: GasSponsorshipInfo, /// The original price from the relayer's signed quote, /// needed to restore the quote for signature verification + #[serde(with = "renegade_external_api::serde_helpers::f64_as_string")] pub original_price: f64, } @@ -160,3 +161,127 @@ impl Server { self.write_sponsorship_info_to_redis(redis_key, &cached_info).await } } + +#[cfg(test)] +mod tests { + use serde::{Deserialize, Serialize}; + + /// Simulates the UNFIXED CachedSponsorshipInfo — bare f64, serialized + /// as a JSON number (e.g. `1234.5678`). + #[derive(Clone, Debug, Serialize, Deserialize)] + struct BareF64Cache { + original_price: f64, + } + + /// Simulates the FIXED CachedSponsorshipInfo — f64 serialized as a + /// JSON string (e.g. `"1234.5678"`), matching ApiTimestampedPrice. + #[derive(Clone, Debug, Serialize, Deserialize)] + struct StringF64Cache { + #[serde(with = "renegade_external_api::serde_helpers::f64_as_string")] + original_price: f64, + } + + /// Proof of concept: demonstrate that the JSON format of the bare f64 + /// (number) differs from the f64_as_string format (string). The HMAC + /// is computed over the quote's JSON bytes, where the price field uses + /// f64_as_string. If the cached price round-trips through a different + /// JSON format, it could produce a different f64 bit pattern, causing + /// the re-serialized quote to differ → HMAC mismatch. + #[test] + fn test_f64_serialization_format_mismatch() { + // A realistic price value from a quote + let price: f64 = 0.000009483294637281045; + + // --- Bare f64 (the bug) --- + let bare = BareF64Cache { original_price: price }; + let bare_json = serde_json::to_string(&bare).unwrap(); + + // --- f64_as_string (the fix) --- + let string = StringF64Cache { original_price: price }; + let string_json = serde_json::to_string(&string).unwrap(); + + println!("Original f64 bits: {:064b}", price.to_bits()); + println!("Original to_string(): {}", price); + println!(); + println!("Bare f64 JSON: {bare_json}"); + println!("f64_as_string JSON: {string_json}"); + + // The JSON formats are structurally different: + // bare: {"original_price":0.000009483294637281045} + // string: {"original_price":"0.000009483294637281045"} + // This is the root cause — different serialization paths for the + // same logical value. + + // Round-trip through bare JSON (simulating Redis write/read) + let bare_restored: BareF64Cache = serde_json::from_str(&bare_json).unwrap(); + let bare_restored_bits = bare_restored.original_price.to_bits(); + + // Round-trip through string JSON + let string_restored: StringF64Cache = serde_json::from_str(&string_json).unwrap(); + let string_restored_bits = string_restored.original_price.to_bits(); + + println!(); + println!("Bare round-trip bits: {:064b}", bare_restored_bits); + println!("String round-trip bits: {:064b}", string_restored_bits); + println!( + "Bits match original: bare={}, string={}", + bare_restored_bits == price.to_bits(), + string_restored_bits == price.to_bits(), + ); + + // The critical check: after restoring the price from cache and + // re-serializing the quote (which uses f64_as_string), do we get + // the same string the relayer signed? + let original_display = price.to_string(); + let bare_restored_display = bare_restored.original_price.to_string(); + let string_restored_display = string_restored.original_price.to_string(); + + println!(); + println!("Original Display: {original_display}"); + println!("Bare restored Display: {bare_restored_display}"); + println!("String restored Display: {string_restored_display}"); + + // The f64_as_string path is guaranteed to round-trip correctly + // because it uses the same serialization format (Display/to_string) + // on both sides. The bare path uses a *different* format (JSON + // number via ryu) which is not guaranteed to produce the same + // Display output after round-tripping. + assert_eq!( + string_restored_display, original_display, + "f64_as_string round-trip must preserve Display output exactly" + ); + } + + /// Demonstrate that serde_json's bare number format and f64::to_string() + /// can produce different representations for the same value, which is + /// the mechanism by which HMAC verification can fail. + #[test] + fn test_bare_vs_string_serialization_difference() { + // Try a range of realistic price values to find divergences + let test_prices: Vec = vec![ + 1.0 / 3.0, // repeating decimal + 0.1 + 0.2, // classic floating point + std::f64::consts::PI, // irrational + 1e-15, // very small + 1.7976931348623157e+308, // near f64::MAX + 0.000009483294637281045, // realistic crypto price + 2999.4800000000005, // ETH-like price with rounding artifact + ]; + + println!( + "{:<35} | {:<30} | {:<30} | match?", + "value", "serde_json number", "f64::to_string()" + ); + println!("{}", "-".repeat(105)); + + for price in &test_prices { + // What serde_json produces for a bare f64 (JSON number) + let serde_repr = serde_json::to_string(price).unwrap(); + // What f64_as_string would produce (Display trait) + let display_repr = price.to_string(); + + let matches = serde_repr == display_repr; + println!("{price:<35e} | {serde_repr:<30} | {display_repr:<30} | {matches}"); + } + } +} diff --git a/funds-manager/funds-manager-server/src/cli.rs b/funds-manager/funds-manager-server/src/cli.rs index a648ae3a..81aa1059 100644 --- a/funds-manager/funds-manager-server/src/cli.rs +++ b/funds-manager/funds-manager-server/src/cli.rs @@ -57,6 +57,15 @@ pub enum Environment { Testnet, } +impl std::fmt::Display for Environment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Environment::Mainnet => write!(f, "mainnet"), + Environment::Testnet => write!(f, "testnet"), + } + } +} + /// The cli for the fee sweeper #[rustfmt::skip] #[derive(Parser)] @@ -110,6 +119,15 @@ pub struct Cli { #[clap(long, default_value = "3000")] pub port: u16, + // --- Gas Limits --- // + + /// The maximum amount of gas (in ETH) that can be withdrawn at a given time + #[clap(long, env = "MAX_GAS_WITHDRAWAL_AMOUNT", default_value = "1.0")] + pub max_gas_withdrawal_amount: f64, + /// The maximum amount (in ETH) that a request may refill gas to + #[clap(long, env = "MAX_GAS_REFILL_AMOUNT", default_value = "0.1")] + pub max_gas_refill_amount: f64, + // --- Telemetry --- // /// Whether or not Datadog is in use. Controls log format & telemetry export. @@ -220,6 +238,20 @@ pub struct ChainConfig { // --- Contract Addresses --- // /// The Permit2 contract address for the chain pub permit2_addr: Address, + + // --- Gas Wallet Params --- // + /// The maximum amount (in ETH) that a request may refill gas to. + /// Falls back to the server-wide CLI default if not specified. + pub max_gas_refill_amount: Option, + /// The maximum amount of gas (in ETH) that can be withdrawn at a given + /// time. Falls back to the server-wide CLI default if not specified. + pub max_gas_withdrawal_amount: Option, + /// The amount of ETH to top up gas wallets to. + /// Defaults to 0.01 ETH if not specified. + pub gas_top_up_amount: Option, + /// The refill tolerance as a fraction (0.0–1.0). Refill is skipped if + /// balance exceeds target * tolerance. + pub gas_refill_tolerance: Option, } impl ChainConfig { @@ -232,6 +264,7 @@ impl ChainConfig { db_pool: Arc, aws_config: SdkConfig, price_reporter: PriceReporterClient, + cli_args: &Cli, ) -> Result { // Build a relayer client // let relayer_client = RelayerClient::new(&self.relayer_url, chain); @@ -258,6 +291,17 @@ impl ChainConfig { let gas_sponsor_address = get_gas_sponsor_address(chain); let gas_sponsor_address_v2 = get_gas_sponsor_address_v2(chain); + use crate::custody_client::gas_wallets::{ + DEFAULT_GAS_REFILL_TOLERANCE, DEFAULT_TOP_UP_AMOUNT, + }; + let max_gas_refill_amount = + self.max_gas_refill_amount.unwrap_or(cli_args.max_gas_refill_amount); + let max_gas_withdrawal_amount = + self.max_gas_withdrawal_amount.unwrap_or(cli_args.max_gas_withdrawal_amount); + let gas_top_up_amount = self.gas_top_up_amount.unwrap_or_else(|| DEFAULT_TOP_UP_AMOUNT); + let gas_refill_tolerance = + self.gas_refill_tolerance.unwrap_or_else(|| DEFAULT_GAS_REFILL_TOLERANCE); + let custody_client = CustodyClient::new( chain, chain_id, @@ -269,6 +313,10 @@ impl ChainConfig { gas_sponsor_address, gas_sponsor_address_v2, price_reporter.clone(), + max_gas_refill_amount, + max_gas_withdrawal_amount, + gas_top_up_amount, + gas_refill_tolerance, )?; let quoter_hot_wallet = diff --git a/funds-manager/funds-manager-server/src/custody_client/gas_sponsor.rs b/funds-manager/funds-manager-server/src/custody_client/gas_sponsor.rs index 68d88686..a635c506 100644 --- a/funds-manager/funds-manager-server/src/custody_client/gas_sponsor.rs +++ b/funds-manager/funds-manager-server/src/custody_client/gas_sponsor.rs @@ -224,7 +224,10 @@ impl CustodyClient { Chain::ArbitrumSepolia | Chain::ArbitrumOne => sol::receiveEthCall {}.abi_encode(), // Solidity implementations use the `receive` fallback function to receive ETH // No calldata is needed here - Chain::BaseSepolia | Chain::BaseMainnet => Vec::new(), + Chain::BaseSepolia + | Chain::BaseMainnet + | Chain::EthereumSepolia + | Chain::EthereumMainnet => Vec::new(), _ => { panic!("transferring eth is not supported on {:?}", self.chain); }, diff --git a/funds-manager/funds-manager-server/src/custody_client/gas_wallets.rs b/funds-manager/funds-manager-server/src/custody_client/gas_wallets.rs index ea8d2776..9660c929 100644 --- a/funds-manager/funds-manager-server/src/custody_client/gas_wallets.rs +++ b/funds-manager/funds-manager-server/src/custody_client/gas_wallets.rs @@ -75,7 +75,7 @@ impl CustodyClient { // Update the gas wallet to be active, top up wallets, and return the key self.mark_gas_wallet_active(&gas_wallet.address, peer_id).await?; - self.refill_gas_wallets(DEFAULT_TOP_UP_AMOUNT).await?; + self.refill_gas_wallets(self.gas_top_up_amount).await?; Ok(secret_value) } @@ -120,7 +120,7 @@ impl CustodyClient { /// Get the secret name for a gas wallet's private key fn gas_wallet_secret_name(address: &str) -> String { - format!("gas-wallet-{}", address) + format!("gas-wallet-{}", address.to_lowercase()) } /// Refill gas for a set of wallets @@ -144,11 +144,15 @@ impl CustodyClient { // If the gas wallet has insufficient funds, top up each wallet as much as // possible - let target = - if my_balance < total_amount { my_balance / wallets.len() as f64 } else { fill_to }; + let (target, amount_desc) = if my_balance < total_amount { + let t = my_balance / wallets.len() as f64; + (t, format!("(hot wallet balance / {} wallets = {})", wallets.len(), t)) + } else { + (fill_to, format!("fill_to amount {fill_to}")) + }; for wallet in wallets.iter() { - self.top_up_gas(&wallet.address, target).await?; + self.top_up_gas(&wallet.address, "ETH", target, &amount_desc).await?; } Ok(()) } @@ -157,9 +161,12 @@ impl CustodyClient { pub(crate) async fn top_up_gas( &self, addr: &str, + symbol: &str, amount: f64, + amount_desc: &str, ) -> Result<(), FundsManagerError> { - self.top_up_gas_with_tolerance(addr, amount, DEFAULT_GAS_REFILL_TOLERANCE).await + self.top_up_gas_with_tolerance(addr, symbol, amount, self.gas_refill_tolerance, amount_desc) + .await } /// Refill gas for a wallet up to a given amount @@ -169,12 +176,17 @@ impl CustodyClient { pub(crate) async fn top_up_gas_with_tolerance( &self, addr: &str, + symbol: &str, amount: f64, tolerance: f64, + amount_desc: &str, ) -> Result<(), FundsManagerError> { let bal = self.get_ether_balance(addr).await?; if bal > amount * tolerance { - info!("Skipping gas refill for {addr} because balance is within tolerance"); + info!( + "Skipping gas refill for 0x{addr} ({symbol}) because balance is within tolerance [{tolerance} of {amount_desc}] (has {bal}, above {})", + amount * tolerance, + ); return Ok(()); } diff --git a/funds-manager/funds-manager-server/src/custody_client/hot_wallets.rs b/funds-manager/funds-manager-server/src/custody_client/hot_wallets.rs index dde5c1db..fc38e18a 100644 --- a/funds-manager/funds-manager-server/src/custody_client/hot_wallets.rs +++ b/funds-manager/funds-manager-server/src/custody_client/hot_wallets.rs @@ -131,7 +131,7 @@ impl CustodyClient { /// The secret name for a hot wallet pub(crate) fn hot_wallet_secret_name(address: &str) -> String { - format!("hot-wallet-{address}") + format!("hot-wallet-{}", address.to_lowercase()) } /// Get the hot wallet private key for a vault @@ -170,6 +170,7 @@ impl CustodyClient { /// Top up the gas (ether) balance on the quoter hot wallet pub(crate) async fn top_up_quoter_hot_wallet_gas(&self) -> Result<(), FundsManagerError> { let hot_wallet = self.get_quoter_hot_wallet().await?; - self.top_up_gas(&hot_wallet.address, DEFAULT_QUOTER_GAS_TOP_UP_AMOUNT).await + let desc = format!("quoter top-up amount {DEFAULT_QUOTER_GAS_TOP_UP_AMOUNT}"); + self.top_up_gas(&hot_wallet.address, "ETH", DEFAULT_QUOTER_GAS_TOP_UP_AMOUNT, &desc).await } } diff --git a/funds-manager/funds-manager-server/src/custody_client/mod.rs b/funds-manager/funds-manager-server/src/custody_client/mod.rs index e6ca774e..0bc1635a 100644 --- a/funds-manager/funds-manager-server/src/custody_client/mod.rs +++ b/funds-manager/funds-manager-server/src/custody_client/mod.rs @@ -59,14 +59,18 @@ const ARB_SEPOLIA_ETH_ASSET_ID: &str = "ETH-AETH_SEPOLIA"; const BASE_MAINNET_ETH_ASSET_ID: &str = "BASECHAIN_ETH"; /// The Fireblocks asset ID for ETH on Base Sepolia const BASE_SEPOLIA_ETH_ASSET_ID: &str = "BASECHAIN_ETH_TEST5"; +/// The Fireblocks asset ID for ETH on Ethereum mainnet +const ETHEREUM_MAINNET_ETH_ASSET_ID: &str = "ETH"; +/// The Fireblocks asset ID for ETH on Ethereum Sepolia +const ETHEREUM_SEPOLIA_ETH_ASSET_ID: &str = "ETH_TEST5"; /// The Fireblocks asset IDs for native assets on testnets pub const TESTNET_NATIVE_ASSET_IDS: &[&str] = - &[ARB_SEPOLIA_ETH_ASSET_ID, BASE_SEPOLIA_ETH_ASSET_ID, "ETH_TEST5"]; + &[ARB_SEPOLIA_ETH_ASSET_ID, BASE_SEPOLIA_ETH_ASSET_ID, ETHEREUM_SEPOLIA_ETH_ASSET_ID]; /// The Fireblocks asset IDs for native assets on mainnets pub const MAINNET_NATIVE_ASSET_IDS: &[&str] = - &[ARB_ONE_ETH_ASSET_ID, BASE_MAINNET_ETH_ASSET_ID, "ETH"]; + &[ARB_ONE_ETH_ASSET_ID, BASE_MAINNET_ETH_ASSET_ID, ETHEREUM_MAINNET_ETH_ASSET_ID]; /// The number of confirmations Fireblocks requires to consider a contract call /// final @@ -111,7 +115,9 @@ impl DepositWithdrawSource { let full_name = format!("{env_name} {name}").to_lowercase(); match full_name.to_lowercase().as_str() { "arbitrum quoters" | "base quoters" | "ethereum quoters" => Ok(Self::Quoter), - "arbitrum fee collection" | "base fee collection" => Ok(Self::FeeRedemption), + "arbitrum fee collection" | "base fee collection" | "ethereum fee collection" => { + Ok(Self::FeeRedemption) + }, "arbitrum gas" | "base gas" | "ethereum gas" => Ok(Self::Gas), _ => Err(FundsManagerError::parse(format!("invalid vault name: {name}"))), } @@ -140,6 +146,14 @@ pub struct CustodyClient { gas_sponsor_address_v2: Address, /// The price reporter client price_reporter: PriceReporterClient, + /// The maximum amount (in ETH) that a request may refill gas to + max_gas_refill_amount: f64, + /// The maximum amount of gas (in ETH) that can be withdrawn at a given time + max_gas_withdrawal_amount: f64, + /// The amount of ETH to fill gas wallets to on registration + gas_top_up_amount: f64, + /// The tolerance for gas refills (fraction of target balance) + gas_refill_tolerance: f64, } impl CustodyClient { @@ -157,6 +171,10 @@ impl CustodyClient { gas_sponsor_address: Address, gas_sponsor_address_v2: Address, price_reporter: PriceReporterClient, + max_gas_refill_amount: f64, + max_gas_withdrawal_amount: f64, + gas_top_up_amount: f64, + gas_refill_tolerance: f64, ) -> Result { let fireblocks_client = Arc::new(FireblocksClient::new(&fireblocks_api_key, &fireblocks_api_secret)?); @@ -171,9 +189,23 @@ impl CustodyClient { gas_sponsor_address, gas_sponsor_address_v2, price_reporter, + max_gas_refill_amount, + max_gas_withdrawal_amount, + gas_top_up_amount, + gas_refill_tolerance, }) } + /// Get the maximum gas refill amount for this chain + pub fn max_gas_refill_amount(&self) -> f64 { + self.max_gas_refill_amount + } + + /// Get the maximum gas withdrawal amount for this chain + pub fn max_gas_withdrawal_amount(&self) -> f64 { + self.max_gas_withdrawal_amount + } + /// Get a database connection from the pool pub async fn get_db_conn(&self) -> Result, FundsManagerError> { self.db_pool.get().await.map_err(|e| FundsManagerError::Db(e.to_string())) @@ -233,6 +265,8 @@ impl CustodyClient { Chain::ArbitrumSepolia => Ok(ARB_SEPOLIA_ETH_ASSET_ID.to_string()), Chain::BaseMainnet => Ok(BASE_MAINNET_ETH_ASSET_ID.to_string()), Chain::BaseSepolia => Ok(BASE_SEPOLIA_ETH_ASSET_ID.to_string()), + Chain::EthereumMainnet => Ok(ETHEREUM_MAINNET_ETH_ASSET_ID.to_string()), + Chain::EthereumSepolia => Ok(ETHEREUM_SEPOLIA_ETH_ASSET_ID.to_string()), _ => Err(FundsManagerError::custom(ERR_UNSUPPORTED_CHAIN)), } } @@ -240,8 +274,12 @@ impl CustodyClient { /// Get the Fireblocks asset IDs for native assets on the current chain pub(crate) fn get_current_env_native_asset_ids(&self) -> Result<&[&str], FundsManagerError> { match self.chain { - Chain::ArbitrumOne | Chain::BaseMainnet => Ok(MAINNET_NATIVE_ASSET_IDS), - Chain::ArbitrumSepolia | Chain::BaseSepolia => Ok(TESTNET_NATIVE_ASSET_IDS), + Chain::ArbitrumOne | Chain::BaseMainnet | Chain::EthereumMainnet => { + Ok(MAINNET_NATIVE_ASSET_IDS) + }, + Chain::ArbitrumSepolia | Chain::BaseSepolia | Chain::EthereumSepolia => { + Ok(TESTNET_NATIVE_ASSET_IDS) + }, _ => Err(FundsManagerError::custom(ERR_UNSUPPORTED_CHAIN)), } } @@ -249,7 +287,10 @@ impl CustodyClient { /// Get the Fireblocks blockchain ID for the current chain async fn get_current_blockchain_id(&self) -> Result { let list_blockchains_params = ListBlockchainsParams::builder() - .test(matches!(self.chain, Chain::ArbitrumSepolia | Chain::BaseSepolia)) + .test(matches!( + self.chain, + Chain::ArbitrumSepolia | Chain::BaseSepolia | Chain::EthereumSepolia + )) .deprecated(false) .build(); diff --git a/funds-manager/funds-manager-server/src/custody_client/withdraw.rs b/funds-manager/funds-manager-server/src/custody_client/withdraw.rs index 7512998d..6c9ca07b 100644 --- a/funds-manager/funds-manager-server/src/custody_client/withdraw.rs +++ b/funds-manager/funds-manager-server/src/custody_client/withdraw.rs @@ -67,9 +67,11 @@ impl CustodyClient { let wallet = self.get_hot_wallet_private_key(&wallet.address).await?; // Execute the erc20 transfer + let token = Token::from_addr_on_chain(token_address, self.chain); + let ticker = token.get_ticker().unwrap_or(token_address.to_string()); let tx = self.erc20_transfer(token_address, destination_address, amount, wallet).await?; info!( - "Withdrew {amount} {token_address} from hot wallet to {destination_address}. Tx: {:?}", + "Withdrew {amount} {ticker} ({token_address}) from hot wallet to {destination_address}. Tx: {:?}", tx.transaction_hash ); diff --git a/funds-manager/funds-manager-server/src/handlers/gas.rs b/funds-manager/funds-manager-server/src/handlers/gas.rs index 0f8e4b92..61e1872c 100644 --- a/funds-manager/funds-manager-server/src/handlers/gas.rs +++ b/funds-manager/funds-manager-server/src/handlers/gas.rs @@ -21,29 +21,20 @@ use crate::{ server::Server, }; -// ------------- -// | Constants | -// ------------- - -/// The maximum amount of gas that can be withdrawn at a given time -const MAX_GAS_WITHDRAWAL_AMOUNT: f64 = 1.; // ETH -/// The maximum amount that a request may refill gas to -const MAX_GAS_REFILL_AMOUNT: f64 = 0.1; // ETH - /// Handler for withdrawing gas from custody pub(crate) async fn withdraw_gas_handler( chain: Chain, withdraw_request: WithdrawGasRequest, server: Arc, ) -> Result { - if withdraw_request.amount > MAX_GAS_WITHDRAWAL_AMOUNT { + let custody_client = server.get_custody_client(&chain)?; + let max = custody_client.max_gas_withdrawal_amount(); + if withdraw_request.amount > max { return Err(warp::reject::custom(ApiError::BadRequest(format!( "Requested amount {} ETH exceeds maximum allowed withdrawal of {} ETH", - withdraw_request.amount, MAX_GAS_WITHDRAWAL_AMOUNT + withdraw_request.amount, max )))); } - - let custody_client = server.get_custody_client(&chain)?; custody_client .withdraw_gas(withdraw_request.amount, &withdraw_request.destination_address) .await @@ -58,15 +49,16 @@ pub(crate) async fn refill_gas_handler( req: RefillGasRequest, server: Arc, ) -> Result { + let custody_client = server.get_custody_client(&chain)?; + // Check that the refill amount is less than the max - if req.amount > MAX_GAS_REFILL_AMOUNT { + let max = custody_client.max_gas_refill_amount(); + if req.amount > max { return Err(warp::reject::custom(ApiError::BadRequest(format!( "Requested amount {} ETH exceeds maximum allowed refill of {} ETH", - req.amount, MAX_GAS_REFILL_AMOUNT + req.amount, max )))); } - - let custody_client = server.get_custody_client(&chain)?; custody_client.refill_gas_wallets(req.amount).await?; let resp = json!({}); diff --git a/funds-manager/funds-manager-server/src/main.rs b/funds-manager/funds-manager-server/src/main.rs index 9323aa15..c6322ec0 100644 --- a/funds-manager/funds-manager-server/src/main.rs +++ b/funds-manager/funds-manager-server/src/main.rs @@ -50,7 +50,7 @@ use renegade_types_core::Chain; use server::Server; use std::{collections::HashMap, error::Error, sync::Arc}; -use tracing::{error, warn}; +use tracing::{error, info_span, warn}; use warp::Filter; use crate::custody_client::CustodyClient; @@ -103,6 +103,9 @@ async fn async_main() -> Result<(), Box> { cli.configure_telemetry()?; + let chain_env = cli.environment.to_string(); + let _root_span = info_span!("funds-manager", chain_env = %chain_env).entered(); + let port = cli.port; // copy `cli.port` to use after moving `cli` let server = Server::build_from_cli(cli).await.expect("failed to build server"); @@ -119,21 +122,21 @@ async fn async_main() -> Result<(), Box> { let index_fees = warp::post() .and(warp::path("fees")) - .and(warp::path::param::()) + .and(with_chain_param()) .and(warp::path(INDEX_FEES_ROUTE)) .and(with_server(server.clone())) .and_then(index_fees_handler); let redeem_fees = warp::post() .and(warp::path("fees")) - .and(warp::path::param::()) + .and(with_chain_param()) .and(warp::path(REDEEM_FEES_ROUTE)) .and(with_server(server.clone())) .and_then(redeem_fees_handler); let get_balances = warp::get() .and(warp::path("fees")) - .and(warp::path::param::()) + .and(with_chain_param()) .and(warp::path(GET_FEE_WALLETS_ROUTE)) .and(with_hmac_auth(server.clone())) .and(with_server(server.clone())) @@ -141,7 +144,7 @@ async fn async_main() -> Result<(), Box> { let withdraw_fee_balance = warp::post() .and(warp::path("fees")) - .and(warp::path::param::()) + .and(with_chain_param()) .and(warp::path(WITHDRAW_FEE_BALANCE_ROUTE)) .and(with_hmac_auth(server.clone())) .map(with_chain_and_json_body::) @@ -152,14 +155,14 @@ async fn async_main() -> Result<(), Box> { let get_fee_hot_wallet_address = warp::get() .and(warp::path("fees")) - .and(warp::path::param::()) + .and(with_chain_param()) .and(warp::path(GET_FEE_HOT_WALLET_ADDRESS_ROUTE)) .and(with_server(server.clone())) .and_then(get_fee_hot_wallet_address_handler); let get_unredeemed_fee_totals = warp::get() .and(warp::path("fees")) - .and(warp::path::param::()) + .and(with_chain_param()) .and(warp::path(GET_UNREDEEMED_FEE_TOTALS_ROUTE)) .and(with_server(server.clone())) .and_then(get_unredeemed_fee_totals_handler); @@ -179,7 +182,7 @@ async fn async_main() -> Result<(), Box> { let withdraw_custody = warp::post() .and(warp::path("custody")) - .and(warp::path::param::()) + .and(with_chain_param()) .and(warp::path("quoters")) .and(warp::path(WITHDRAW_CUSTODY_ROUTE)) .and(with_hmac_auth(server.clone())) @@ -191,7 +194,7 @@ async fn async_main() -> Result<(), Box> { let get_deposit_address = warp::get() .and(warp::path("custody")) - .and(warp::path::param::()) + .and(with_chain_param()) .and(warp::path("quoters")) .and(warp::path(GET_DEPOSIT_ADDRESS_ROUTE)) .and(with_server(server.clone())) @@ -199,7 +202,7 @@ async fn async_main() -> Result<(), Box> { let swap_immediate = warp::post() .and(warp::path("custody")) - .and(warp::path::param::()) + .and(with_chain_param()) .and(warp::path("quoters")) .and(warp::path(SWAP_IMMEDIATE_ROUTE)) .and(with_hmac_auth(server.clone())) @@ -211,7 +214,7 @@ async fn async_main() -> Result<(), Box> { let swap_into_target_token = warp::post() .and(warp::path("custody")) - .and(warp::path::param::()) + .and(with_chain_param()) .and(warp::path("quoters")) .and(warp::path(SWAP_INTO_TARGET_TOKEN_ROUTE)) .and(with_hmac_auth(server.clone())) @@ -235,7 +238,7 @@ async fn async_main() -> Result<(), Box> { let withdraw_gas = warp::post() .and(warp::path("custody")) - .and(warp::path::param::()) + .and(with_chain_param()) .and(warp::path("gas")) .and(warp::path(WITHDRAW_GAS_ROUTE)) .and(with_hmac_auth(server.clone())) @@ -247,7 +250,7 @@ async fn async_main() -> Result<(), Box> { let refill_gas = warp::post() .and(warp::path("custody")) - .and(warp::path::param::()) + .and(with_chain_param()) .and(warp::path("gas")) .and(warp::path(REFILL_GAS_ROUTE)) .and(with_hmac_auth(server.clone())) @@ -259,7 +262,7 @@ async fn async_main() -> Result<(), Box> { let set_gas_wallet_status = warp::post() .and(warp::path("custody")) - .and(warp::path::param::()) + .and(with_chain_param()) .and(warp::path("gas-wallets")) .and(warp::path(SET_GAS_WALLET_STATUS_ROUTE)) .and(with_hmac_auth(server.clone())) @@ -271,15 +274,16 @@ async fn async_main() -> Result<(), Box> { let add_gas_wallet = warp::post() .and(warp::path("custody")) - .and(warp::path::param::()) + .and(with_chain_param()) .and(warp::path("gas-wallets")) + .and(warp::path::end()) .and(with_hmac_auth(server.clone())) .and(with_server(server.clone())) .and_then(create_gas_wallet_handler); let register_gas_wallet = warp::post() .and(warp::path("custody")) - .and(warp::path::param::()) + .and(with_chain_param()) .and(warp::path("gas-wallets")) .and(warp::path(REGISTER_GAS_WALLET_ROUTE)) .and(with_hmac_auth(server.clone())) @@ -291,7 +295,7 @@ async fn async_main() -> Result<(), Box> { let report_active_peers = warp::post() .and(warp::path("custody")) - .and(warp::path::param::()) + .and(with_chain_param()) .and(warp::path("gas-wallets")) .and(warp::path(REPORT_ACTIVE_PEERS_ROUTE)) .and(with_hmac_auth(server.clone())) @@ -303,7 +307,7 @@ async fn async_main() -> Result<(), Box> { let refill_gas_sponsor = warp::post() .and(warp::path("custody")) - .and(warp::path::param::()) + .and(with_chain_param()) .and(warp::path("gas")) .and(warp::path(REFILL_GAS_SPONSOR_ROUTE)) .and(with_hmac_auth(server.clone())) @@ -312,15 +316,16 @@ async fn async_main() -> Result<(), Box> { let get_gas_wallets = warp::get() .and(warp::path("custody")) - .and(warp::path::param::()) + .and(with_chain_param()) .and(warp::path("gas-wallets")) + .and(warp::path::end()) .and(with_hmac_auth(server.clone())) .and(with_server(server.clone())) .and_then(get_gas_wallets_handler); let get_gas_hot_wallet_address = warp::get() .and(warp::path("custody")) - .and(warp::path::param::()) + .and(with_chain_param()) .and(warp::path("gas")) .and(warp::path(GET_GAS_HOT_WALLET_ADDRESS_ROUTE)) .and(with_server(server.clone())) @@ -330,8 +335,9 @@ async fn async_main() -> Result<(), Box> { let create_hot_wallet = warp::post() .and(warp::path("custody")) - .and(warp::path::param::()) + .and(with_chain_param()) .and(warp::path("hot-wallets")) + .and(warp::path::end()) .and(with_hmac_auth(server.clone())) .map(with_chain_and_json_body::) .and_then(identity) @@ -341,7 +347,7 @@ async fn async_main() -> Result<(), Box> { let get_hot_wallet_balances = warp::get() .and(warp::path("custody")) - .and(warp::path::param::()) + .and(with_chain_param()) .and(warp::path("hot-wallets")) .and(with_hmac_auth(server.clone())) .and(warp::query::>()) @@ -350,7 +356,7 @@ async fn async_main() -> Result<(), Box> { let transfer_to_vault = warp::post() .and(warp::path("custody")) - .and(warp::path::param::()) + .and(with_chain_param()) .and(warp::path("hot-wallets")) .and(warp::path(TRANSFER_TO_VAULT_ROUTE)) .and(with_hmac_auth(server.clone())) @@ -362,7 +368,7 @@ async fn async_main() -> Result<(), Box> { let transfer_to_hot_wallet = warp::post() .and(warp::path("custody")) - .and(warp::path::param::()) + .and(with_chain_param()) .and(warp::path("hot-wallets")) .and(warp::path(WITHDRAW_TO_HOT_WALLET_ROUTE)) .and(with_hmac_auth(server.clone())) @@ -409,7 +415,14 @@ async fn async_main() -> Result<(), Box> { .or(create_hot_wallet) .or(rpc) .boxed() - .with(warp::trace::request()) + .with(warp::trace(|info: warp::trace::Info| { + tracing::info_span!( + "request", + method = %info.method(), + path = %info.path(), + chain = tracing::field::Empty, + ) + })) .recover(handle_rejection); warp::serve(routes).run(([0, 0, 0, 0], port)).await; @@ -444,3 +457,11 @@ fn with_server( ) -> impl Filter,), Error = std::convert::Infallible> + Clone { warp::any().map(move || server.clone()) } + +/// Extract a `Chain` path parameter and record it on the current tracing span +fn with_chain_param() -> impl Filter + Clone { + warp::path::param::().map(|chain: Chain| { + tracing::Span::current().record("chain", tracing::field::display(&chain)); + chain + }) +} diff --git a/funds-manager/funds-manager-server/src/server.rs b/funds-manager/funds-manager-server/src/server.rs index edf9deb6..4e6a574b 100644 --- a/funds-manager/funds-manager-server/src/server.rs +++ b/funds-manager/funds-manager-server/src/server.rs @@ -79,6 +79,7 @@ impl Server { arc_pool.clone(), aws_config.clone(), price_reporter.clone(), + &args, ) .await?; diff --git a/funds-manager/scripts/create_gas_wallets.py b/funds-manager/scripts/create_gas_wallets.py new file mode 100644 index 00000000..78940cdd --- /dev/null +++ b/funds-manager/scripts/create_gas_wallets.py @@ -0,0 +1,55 @@ +"""Create gas wallets via the funds manager API.""" + +import argparse +import hmac +import hashlib +import json + +import requests + + +def create_gas_wallet( + host: str, + chain: str, + hmac_key_hex: str | None = None, +) -> dict: + """Create a single gas wallet via the funds manager API.""" + path = f"/custody/{chain}/gas-wallets" + body = "" + + headers = {"Content-Type": "application/json"} + + if hmac_key_hex: + hmac_key = bytes.fromhex(hmac_key_hex) + message = b"POST" + path.encode() + body.encode() + signature = hmac.new(hmac_key, message, hashlib.sha256).hexdigest() + headers["X-Signature"] = signature + + resp = requests.post(host + path, headers=headers, data=body) + if not resp.ok: + print(f"Error {resp.status_code}: {resp.text}") + resp.raise_for_status() + return resp.json() + + +def main(): + parser = argparse.ArgumentParser(description="Create gas wallets") + parser.add_argument("--host", required=True, help="Funds manager host, e.g. http://localhost:3000") + parser.add_argument("--chain", default="ethereum-sepolia", help="Chain name (default: ethereum-sepolia)") + parser.add_argument("-n", "--count", type=int, default=1, help="Number of gas wallets to create (default: 1)") + parser.add_argument("--hmac-key", default=None, help="HMAC key as hex string (omit if auth is disabled)") + args = parser.parse_args() + + for i in range(args.count): + result = create_gas_wallet( + host=args.host, + chain=args.chain, + hmac_key_hex=args.hmac_key, + ) + print(f"[{i + 1}/{args.count}] Created gas wallet: {result['address']}") + + print(f"Done. Created {args.count} gas wallet(s).") + + +if __name__ == "__main__": + main() diff --git a/funds-manager/scripts/fireblocks_list_vault_accounts.py b/funds-manager/scripts/fireblocks_list_vault_accounts.py new file mode 100644 index 00000000..b57ebd93 --- /dev/null +++ b/funds-manager/scripts/fireblocks_list_vault_accounts.py @@ -0,0 +1,57 @@ +"""List Fireblocks vault accounts using JWT authentication.""" + +import argparse +import hashlib +import json +import time +import uuid + +import jwt +import requests + + +def sign_jwt(api_key: str, private_key: str, path: str, body: str = "") -> str: + """Create a signed JWT for Fireblocks API authentication.""" + now = int(time.time()) + body_hash = hashlib.sha256(body.encode("utf-8")).hexdigest() + + payload = { + "uri": path, + "nonce": str(uuid.uuid4()), + "iat": now, + "exp": now + 30, + "sub": api_key, + "bodyHash": body_hash, + } + + return jwt.encode(payload, private_key, algorithm="RS256") + + +def main(): + parser = argparse.ArgumentParser(description="List Fireblocks vault accounts") + parser.add_argument("--api-key", required=True, help="Fireblocks API key") + parser.add_argument("--secret-key-path", required=True, help="Path to the RSA private key file") + args = parser.parse_args() + + with open(args.secret_key_path) as f: + private_key = f.read() + + path = "/v1/vault/accounts_paged" + token = sign_jwt(args.api_key, private_key, path) + + resp = requests.get( + f"https://api.fireblocks.io{path}", + headers={ + "X-API-Key": args.api_key, + "Authorization": f"Bearer {token}", + }, + ) + + if not resp.ok: + print(f"Error {resp.status_code}: {resp.text}") + else: + print(json.dumps(resp.json(), indent=2)) + + +if __name__ == "__main__": + main() diff --git a/funds-manager/scripts/refill_gas.py b/funds-manager/scripts/refill_gas.py new file mode 100644 index 00000000..c6be92d0 --- /dev/null +++ b/funds-manager/scripts/refill_gas.py @@ -0,0 +1,58 @@ +"""Refill gas for all active gas wallets via the funds manager API.""" + +import argparse +import hmac +import hashlib +import json + +import requests + + +def refill_gas( + host: str, + chain: str, + fill_to: float, + hmac_key_hex: str | None = None, +) -> dict: + """Refill gas for all active gas wallets.""" + path = f"/custody/{chain}/gas/refill-gas" + body = json.dumps( + {"amount": fill_to}, + separators=(",", ":"), + ) + + headers = {"Content-Type": "application/json"} + + if hmac_key_hex: + hmac_key = bytes.fromhex(hmac_key_hex) + message = b"POST" + path.encode() + body.encode() + signature = hmac.new(hmac_key, message, hashlib.sha256).hexdigest() + headers["X-Signature"] = signature + + resp = requests.post(host + path, headers=headers, data=body, timeout=300) + if not resp.ok: + print(f"Error {resp.status_code}: {resp.text}") + resp.raise_for_status() + return resp.json() + + +def main(): + parser = argparse.ArgumentParser(description="Refill gas for all active gas wallets") + parser.add_argument("--host", required=True, help="Funds manager host, e.g. http://localhost:3000") + parser.add_argument("--chain", default="ethereum-sepolia", help="Chain name (default: ethereum-sepolia)") + parser.add_argument("--fill-to", type=float, default=0.1, help="Target ETH balance to fill each wallet to (default: 0.1)") + parser.add_argument("--hmac-key", default=None, help="HMAC key as hex string (omit if auth is disabled)") + args = parser.parse_args() + + print(f"Refilling gas wallets on {args.chain} to {args.fill_to} ETH each...") + result = refill_gas( + host=args.host, + chain=args.chain, + fill_to=args.fill_to, + hmac_key_hex=args.hmac_key, + ) + print(json.dumps(result, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/funds-manager/scripts/requirements.txt b/funds-manager/scripts/requirements.txt new file mode 100644 index 00000000..92153702 --- /dev/null +++ b/funds-manager/scripts/requirements.txt @@ -0,0 +1,10 @@ +certifi==2026.1.4 +cffi==2.0.0 +charset-normalizer==3.4.4 +cryptography==46.0.4 +idna==3.11 +pycparser==3.0 +PyJWT==2.11.0 +requests==2.32.5 +urllib3==2.6.3 +uuid==1.30 diff --git a/renegade-deploy-config.toml b/renegade-deploy-config.toml index 10a3fcdd..7c5d6d36 100644 --- a/renegade-deploy-config.toml +++ b/renegade-deploy-config.toml @@ -43,6 +43,16 @@ cargo_features = "base" environment = "base-mainnet" resource = "auth-server-v2" +[services.ethereum-sepolia-auth-server] +[services.ethereum-sepolia-auth-server.build] +dockerfile = "auth/Dockerfile" +ecr_repo = "auth-server-ethereum-sepolia-v2" +cargo_features = "ethereum" + +[services.ethereum-sepolia-auth-server.deploy] +environment = "ethereum-sepolia" +resource = "auth-server-v2" + ##################### ### Funds Manager ### ##################### From 39be6cdccae64578b95691374711cd337fd7a7bd Mon Sep 17 00:00:00 2001 From: Koh Wei Jie Date: Fri, 20 Feb 2026 15:44:27 -0800 Subject: [PATCH 2/5] removed funds-manager testing scripts; use ETHEREUM_MAINNET_ETH_ASSET_ID as the gas symbol --- .../src/custody_client/hot_wallets.rs | 4 +- funds-manager/scripts/create_gas_wallets.py | 55 ------------------ .../scripts/fireblocks_list_vault_accounts.py | 57 ------------------ funds-manager/scripts/refill_gas.py | 58 ------------------- funds-manager/scripts/requirements.txt | 10 ---- 5 files changed, 2 insertions(+), 182 deletions(-) delete mode 100644 funds-manager/scripts/create_gas_wallets.py delete mode 100644 funds-manager/scripts/fireblocks_list_vault_accounts.py delete mode 100644 funds-manager/scripts/refill_gas.py delete mode 100644 funds-manager/scripts/requirements.txt diff --git a/funds-manager/funds-manager-server/src/custody_client/hot_wallets.rs b/funds-manager/funds-manager-server/src/custody_client/hot_wallets.rs index fc38e18a..b8f87a7b 100644 --- a/funds-manager/funds-manager-server/src/custody_client/hot_wallets.rs +++ b/funds-manager/funds-manager-server/src/custody_client/hot_wallets.rs @@ -16,7 +16,7 @@ use uuid::Uuid; use super::CustodyClient; use crate::{ - custody_client::DepositWithdrawSource, + custody_client::{DepositWithdrawSource, ETHEREUM_MAINNET_ETH_ASSET_ID}, error::FundsManagerError, helpers::{IERC20, create_secrets_manager_entry_with_description, get_secret}, }; @@ -171,6 +171,6 @@ impl CustodyClient { pub(crate) async fn top_up_quoter_hot_wallet_gas(&self) -> Result<(), FundsManagerError> { let hot_wallet = self.get_quoter_hot_wallet().await?; let desc = format!("quoter top-up amount {DEFAULT_QUOTER_GAS_TOP_UP_AMOUNT}"); - self.top_up_gas(&hot_wallet.address, "ETH", DEFAULT_QUOTER_GAS_TOP_UP_AMOUNT, &desc).await + self.top_up_gas(&hot_wallet.address, ETHEREUM_MAINNET_ETH_ASSET_ID, DEFAULT_QUOTER_GAS_TOP_UP_AMOUNT, &desc).await } } diff --git a/funds-manager/scripts/create_gas_wallets.py b/funds-manager/scripts/create_gas_wallets.py deleted file mode 100644 index 78940cdd..00000000 --- a/funds-manager/scripts/create_gas_wallets.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Create gas wallets via the funds manager API.""" - -import argparse -import hmac -import hashlib -import json - -import requests - - -def create_gas_wallet( - host: str, - chain: str, - hmac_key_hex: str | None = None, -) -> dict: - """Create a single gas wallet via the funds manager API.""" - path = f"/custody/{chain}/gas-wallets" - body = "" - - headers = {"Content-Type": "application/json"} - - if hmac_key_hex: - hmac_key = bytes.fromhex(hmac_key_hex) - message = b"POST" + path.encode() + body.encode() - signature = hmac.new(hmac_key, message, hashlib.sha256).hexdigest() - headers["X-Signature"] = signature - - resp = requests.post(host + path, headers=headers, data=body) - if not resp.ok: - print(f"Error {resp.status_code}: {resp.text}") - resp.raise_for_status() - return resp.json() - - -def main(): - parser = argparse.ArgumentParser(description="Create gas wallets") - parser.add_argument("--host", required=True, help="Funds manager host, e.g. http://localhost:3000") - parser.add_argument("--chain", default="ethereum-sepolia", help="Chain name (default: ethereum-sepolia)") - parser.add_argument("-n", "--count", type=int, default=1, help="Number of gas wallets to create (default: 1)") - parser.add_argument("--hmac-key", default=None, help="HMAC key as hex string (omit if auth is disabled)") - args = parser.parse_args() - - for i in range(args.count): - result = create_gas_wallet( - host=args.host, - chain=args.chain, - hmac_key_hex=args.hmac_key, - ) - print(f"[{i + 1}/{args.count}] Created gas wallet: {result['address']}") - - print(f"Done. Created {args.count} gas wallet(s).") - - -if __name__ == "__main__": - main() diff --git a/funds-manager/scripts/fireblocks_list_vault_accounts.py b/funds-manager/scripts/fireblocks_list_vault_accounts.py deleted file mode 100644 index b57ebd93..00000000 --- a/funds-manager/scripts/fireblocks_list_vault_accounts.py +++ /dev/null @@ -1,57 +0,0 @@ -"""List Fireblocks vault accounts using JWT authentication.""" - -import argparse -import hashlib -import json -import time -import uuid - -import jwt -import requests - - -def sign_jwt(api_key: str, private_key: str, path: str, body: str = "") -> str: - """Create a signed JWT for Fireblocks API authentication.""" - now = int(time.time()) - body_hash = hashlib.sha256(body.encode("utf-8")).hexdigest() - - payload = { - "uri": path, - "nonce": str(uuid.uuid4()), - "iat": now, - "exp": now + 30, - "sub": api_key, - "bodyHash": body_hash, - } - - return jwt.encode(payload, private_key, algorithm="RS256") - - -def main(): - parser = argparse.ArgumentParser(description="List Fireblocks vault accounts") - parser.add_argument("--api-key", required=True, help="Fireblocks API key") - parser.add_argument("--secret-key-path", required=True, help="Path to the RSA private key file") - args = parser.parse_args() - - with open(args.secret_key_path) as f: - private_key = f.read() - - path = "/v1/vault/accounts_paged" - token = sign_jwt(args.api_key, private_key, path) - - resp = requests.get( - f"https://api.fireblocks.io{path}", - headers={ - "X-API-Key": args.api_key, - "Authorization": f"Bearer {token}", - }, - ) - - if not resp.ok: - print(f"Error {resp.status_code}: {resp.text}") - else: - print(json.dumps(resp.json(), indent=2)) - - -if __name__ == "__main__": - main() diff --git a/funds-manager/scripts/refill_gas.py b/funds-manager/scripts/refill_gas.py deleted file mode 100644 index c6be92d0..00000000 --- a/funds-manager/scripts/refill_gas.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Refill gas for all active gas wallets via the funds manager API.""" - -import argparse -import hmac -import hashlib -import json - -import requests - - -def refill_gas( - host: str, - chain: str, - fill_to: float, - hmac_key_hex: str | None = None, -) -> dict: - """Refill gas for all active gas wallets.""" - path = f"/custody/{chain}/gas/refill-gas" - body = json.dumps( - {"amount": fill_to}, - separators=(",", ":"), - ) - - headers = {"Content-Type": "application/json"} - - if hmac_key_hex: - hmac_key = bytes.fromhex(hmac_key_hex) - message = b"POST" + path.encode() + body.encode() - signature = hmac.new(hmac_key, message, hashlib.sha256).hexdigest() - headers["X-Signature"] = signature - - resp = requests.post(host + path, headers=headers, data=body, timeout=300) - if not resp.ok: - print(f"Error {resp.status_code}: {resp.text}") - resp.raise_for_status() - return resp.json() - - -def main(): - parser = argparse.ArgumentParser(description="Refill gas for all active gas wallets") - parser.add_argument("--host", required=True, help="Funds manager host, e.g. http://localhost:3000") - parser.add_argument("--chain", default="ethereum-sepolia", help="Chain name (default: ethereum-sepolia)") - parser.add_argument("--fill-to", type=float, default=0.1, help="Target ETH balance to fill each wallet to (default: 0.1)") - parser.add_argument("--hmac-key", default=None, help="HMAC key as hex string (omit if auth is disabled)") - args = parser.parse_args() - - print(f"Refilling gas wallets on {args.chain} to {args.fill_to} ETH each...") - result = refill_gas( - host=args.host, - chain=args.chain, - fill_to=args.fill_to, - hmac_key_hex=args.hmac_key, - ) - print(json.dumps(result, indent=2)) - - -if __name__ == "__main__": - main() diff --git a/funds-manager/scripts/requirements.txt b/funds-manager/scripts/requirements.txt deleted file mode 100644 index 92153702..00000000 --- a/funds-manager/scripts/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -certifi==2026.1.4 -cffi==2.0.0 -charset-normalizer==3.4.4 -cryptography==46.0.4 -idna==3.11 -pycparser==3.0 -PyJWT==2.11.0 -requests==2.32.5 -urllib3==2.6.3 -uuid==1.30 From ed4ae3d67597c590180f1eddde11635dd08a1fbd Mon Sep 17 00:00:00 2001 From: Koh Wei Jie Date: Mon, 23 Feb 2026 12:42:33 -0800 Subject: [PATCH 3/5] ethereum mainnet (and sepolia) in renegade-deploy-config.toml; clippy fix --- funds-manager/funds-manager-server/src/cli.rs | 5 +++-- renegade-deploy-config.toml | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/funds-manager/funds-manager-server/src/cli.rs b/funds-manager/funds-manager-server/src/cli.rs index 81aa1059..97d7c948 100644 --- a/funds-manager/funds-manager-server/src/cli.rs +++ b/funds-manager/funds-manager-server/src/cli.rs @@ -256,6 +256,7 @@ pub struct ChainConfig { impl ChainConfig { /// Build chain-specific clients from the given config + #[allow(clippy::too_many_arguments)] pub async fn build_clients( &self, chain: Chain, @@ -298,9 +299,9 @@ impl ChainConfig { self.max_gas_refill_amount.unwrap_or(cli_args.max_gas_refill_amount); let max_gas_withdrawal_amount = self.max_gas_withdrawal_amount.unwrap_or(cli_args.max_gas_withdrawal_amount); - let gas_top_up_amount = self.gas_top_up_amount.unwrap_or_else(|| DEFAULT_TOP_UP_AMOUNT); + let gas_top_up_amount = self.gas_top_up_amount.unwrap_or(DEFAULT_TOP_UP_AMOUNT); let gas_refill_tolerance = - self.gas_refill_tolerance.unwrap_or_else(|| DEFAULT_GAS_REFILL_TOLERANCE); + self.gas_refill_tolerance.unwrap_or(DEFAULT_GAS_REFILL_TOLERANCE); let custody_client = CustodyClient::new( chain, diff --git a/renegade-deploy-config.toml b/renegade-deploy-config.toml index 12698e85..d946d98c 100644 --- a/renegade-deploy-config.toml +++ b/renegade-deploy-config.toml @@ -63,6 +63,16 @@ cargo_features = "ethereum" environment = "ethereum-sepolia" resource = "auth-server-v2" +[services.ethereum-mainnet-auth-server] +[services.ethereum-mainnet-auth-server.build] +dockerfile = "auth/Dockerfile" +ecr_repo = "auth-server-ethereum-mainnet-v2" +cargo_features = "ethereum" + +[services.ethereum-mainnet-auth-server.deploy] +environment = "ethereum-mainnet" +resource = "auth-server-v2" + ##################### ### Funds Manager ### ##################### From 947a310b872f62dc64bcc9d260fc0b4a495d1109 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:43:23 +0000 Subject: [PATCH 4/5] Format Rust code using rustfmt --- .../src/custody_client/hot_wallets.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/funds-manager/funds-manager-server/src/custody_client/hot_wallets.rs b/funds-manager/funds-manager-server/src/custody_client/hot_wallets.rs index b8f87a7b..8aeee871 100644 --- a/funds-manager/funds-manager-server/src/custody_client/hot_wallets.rs +++ b/funds-manager/funds-manager-server/src/custody_client/hot_wallets.rs @@ -171,6 +171,12 @@ impl CustodyClient { pub(crate) async fn top_up_quoter_hot_wallet_gas(&self) -> Result<(), FundsManagerError> { let hot_wallet = self.get_quoter_hot_wallet().await?; let desc = format!("quoter top-up amount {DEFAULT_QUOTER_GAS_TOP_UP_AMOUNT}"); - self.top_up_gas(&hot_wallet.address, ETHEREUM_MAINNET_ETH_ASSET_ID, DEFAULT_QUOTER_GAS_TOP_UP_AMOUNT, &desc).await + self.top_up_gas( + &hot_wallet.address, + ETHEREUM_MAINNET_ETH_ASSET_ID, + DEFAULT_QUOTER_GAS_TOP_UP_AMOUNT, + &desc, + ) + .await } } From d3f1f018116352eb86286119293cb5140e73c956 Mon Sep 17 00:00:00 2001 From: Koh Wei Jie Date: Mon, 23 Feb 2026 13:12:00 -0800 Subject: [PATCH 5/5] ethereum mainnet - wip --- .../src/server/api_handlers/connectors/rfqt/helpers.rs | 2 ++ .../src/execution_client/venues/bebop/mod.rs | 1 + .../src/execution_client/venues/cowswap/mod.rs | 1 + 3 files changed, 4 insertions(+) diff --git a/auth/auth-server/src/server/api_handlers/connectors/rfqt/helpers.rs b/auth/auth-server/src/server/api_handlers/connectors/rfqt/helpers.rs index 95f78655..77de4138 100644 --- a/auth/auth-server/src/server/api_handlers/connectors/rfqt/helpers.rs +++ b/auth/auth-server/src/server/api_handlers/connectors/rfqt/helpers.rs @@ -88,6 +88,8 @@ fn chain_to_chain_id(chain: Chain) -> u64 { Chain::ArbitrumSepolia => 421614, Chain::BaseMainnet => 8453, Chain::BaseSepolia => 84532, + Chain::EthereumMainnet => 1, + Chain::EthereumSepolia => 11155111, Chain::Devnet => 0, } } diff --git a/funds-manager/funds-manager-server/src/execution_client/venues/bebop/mod.rs b/funds-manager/funds-manager-server/src/execution_client/venues/bebop/mod.rs index e634f374..85ca6ab8 100644 --- a/funds-manager/funds-manager-server/src/execution_client/venues/bebop/mod.rs +++ b/funds-manager/funds-manager-server/src/execution_client/venues/bebop/mod.rs @@ -345,6 +345,7 @@ fn to_bebop_chain(chain: Chain) -> Result { match chain { Chain::ArbitrumOne => Ok("arbitrum".to_string()), Chain::BaseMainnet => Ok("base".to_string()), + Chain::EthereumMainnet => Ok("ethereum".to_string()), _ => Err(ExecutionClientError::onchain(format!("Bebop does not support chain: {chain}"))), } } diff --git a/funds-manager/funds-manager-server/src/execution_client/venues/cowswap/mod.rs b/funds-manager/funds-manager-server/src/execution_client/venues/cowswap/mod.rs index b02048d4..37ce324d 100644 --- a/funds-manager/funds-manager-server/src/execution_client/venues/cowswap/mod.rs +++ b/funds-manager/funds-manager-server/src/execution_client/venues/cowswap/mod.rs @@ -479,6 +479,7 @@ fn to_cowswap_chain(chain: Chain) -> Result { match chain { Chain::ArbitrumOne => Ok("arbitrum_one".to_string()), Chain::BaseMainnet => Ok("base".to_string()), + Chain::EthereumMainnet => Ok("mainnet".to_string()), _ => Err(ExecutionClientError::onchain(format!("Cowswap does not support chain: {chain}"))), } }