diff --git a/funds-manager/funds-manager-server/src/custody_client/deposit.rs b/funds-manager/funds-manager-server/src/custody_client/deposit.rs index c495515..db7519e 100644 --- a/funds-manager/funds-manager-server/src/custody_client/deposit.rs +++ b/funds-manager/funds-manager-server/src/custody_client/deposit.rs @@ -8,15 +8,22 @@ impl CustodyClient { /// Get the deposit address for the given mint pub(crate) async fn get_deposit_address( &self, - mint: &str, source: DepositWithdrawSource, ) -> Result { let vault_name = source.vault_name(); - self.get_deposit_address_by_vault_name(mint, vault_name).await + self.get_deposit_address_by_vault_name(vault_name).await } /// Get the deposit address given a vault name pub(crate) async fn get_deposit_address_by_vault_name( + &self, + vault_name: &str, + ) -> Result { + self.get_hot_wallet_by_vault(vault_name).await.map(|w| w.address) + } + + /// Get the deposit address given a vault name + pub(crate) async fn get_fireblocks_deposit_address( &self, mint: &str, vault_name: &str, 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 880c809..572b4aa 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 @@ -92,8 +92,7 @@ impl CustodyClient { let wallet = LocalWallet::from_str(&secret_value).map_err(FundsManagerError::parse)?; // 3. Look up the vault deposit address - let deposit_address = - self.get_deposit_address_by_vault_name(mint, &hot_wallet.vault).await?; + let deposit_address = self.get_fireblocks_deposit_address(mint, &hot_wallet.vault).await?; // 4. Transfer the tokens let receipt = self.erc20_transfer(mint, &deposit_address, amount, wallet).await?; @@ -114,7 +113,8 @@ impl CustodyClient { // Fetch the wallet info, then withdraw let wallet = self.get_hot_wallet_by_vault(vault).await?; let source = DepositWithdrawSource::from_vault_name(vault)?; - self.withdraw_with_token_addr(source, &wallet.address, mint, amount).await + let symbol = self.get_erc20_token_symbol(mint).await?; + self.withdraw_from_fireblocks(source, &wallet.address, &symbol, amount).await } // ------------ @@ -122,7 +122,7 @@ impl CustodyClient { // ------------ /// The secret name for a hot wallet - fn hot_wallet_secret_name(address: &str) -> String { + pub(crate) fn hot_wallet_secret_name(address: &str) -> String { format!("hot-wallet-{address}") } 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 c542c0e..fe4dbb3 100644 --- a/funds-manager/funds-manager-server/src/custody_client/mod.rs +++ b/funds-manager/funds-manager-server/src/custody_client/mod.rs @@ -8,9 +8,10 @@ pub mod withdraw; use aws_config::SdkConfig as AwsConfig; use ethers::middleware::SignerMiddleware; use ethers::prelude::abigen; -use ethers::providers::{Http, Provider}; +use ethers::providers::{Http, Middleware, Provider}; use ethers::signers::{LocalWallet, Signer}; -use ethers::types::{Address, TransactionReceipt}; +use ethers::types::{Address, TransactionReceipt, TransactionRequest, U256}; +use ethers::utils::format_units; use fireblocks_sdk::types::Transaction; use fireblocks_sdk::{ types::{Account as FireblocksAccount, AccountAsset}, @@ -178,6 +179,43 @@ impl CustodyClient { .map_err(err_str!(FundsManagerError::Arbitrum)) } + /// Get the native token balance of an address + pub(crate) async fn get_ether_balance(&self, address: &str) -> Result { + let provider = self.get_rpc_provider()?; + let client = Arc::new(provider); + let address = Address::from_str(address).map_err(FundsManagerError::parse)?; + let balance = + client.get_balance(address, None).await.map_err(FundsManagerError::arbitrum)?; + + // Convert U256 to f64 + let balance_str = format_units(balance, "ether").map_err(FundsManagerError::parse)?; + balance_str.parse::().map_err(FundsManagerError::parse) + } + + /// Transfer ether from the given wallet + pub(crate) async fn transfer_ether( + &self, + to: &str, + amount: f64, + wallet: LocalWallet, + ) -> Result { + let wallet = wallet.with_chain_id(self.chain_id); + let provider = self.get_rpc_provider()?; + let client = SignerMiddleware::new(provider, wallet); + + let to = Address::from_str(to).map_err(FundsManagerError::parse)?; + let amount = ethers::utils::parse_units(amount.to_string(), "ether") + .map_err(FundsManagerError::parse)?; + + let tx = TransactionRequest::new().to(to).value(U256::from(amount)); + let pending_tx = + client.send_transaction(tx, None).await.map_err(FundsManagerError::arbitrum)?; + pending_tx + .await + .map_err(FundsManagerError::arbitrum)? + .ok_or_else(|| FundsManagerError::arbitrum("Transaction failed".to_string())) + } + /// Get the symbol for an ERC20 token at the given address pub(self) async fn get_erc20_token_symbol( &self, @@ -192,6 +230,29 @@ impl CustodyClient { erc20.symbol().call().await.map_err(FundsManagerError::arbitrum) } + /// Get the erc20 balance of an address + pub(crate) async fn get_erc20_balance( + &self, + token_address: &str, + address: &str, + ) -> Result { + // Setup the provider + let token_address = Address::from_str(token_address).map_err(FundsManagerError::parse)?; + let address = Address::from_str(address).map_err(FundsManagerError::parse)?; + let provider = self.get_rpc_provider()?; + let client = Arc::new(provider); + let erc20 = ERC20::new(token_address, client); + + // Fetch the balance and correct for the ERC20 decimal precision + let decimals = erc20.decimals().call().await.map_err(FundsManagerError::arbitrum)? as u32; + let balance = + erc20.balance_of(address).call().await.map_err(FundsManagerError::arbitrum)?; + let bal_str = format_units(balance, decimals).map_err(FundsManagerError::parse)?; + let bal_f64 = bal_str.parse::().map_err(FundsManagerError::parse)?; + + Ok(bal_f64) + } + /// Perform an erc20 transfer pub(crate) async fn erc20_transfer( &self, 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 e8220d3..846b400 100644 --- a/funds-manager/funds-manager-server/src/custody_client/withdraw.rs +++ b/funds-manager/funds-manager-server/src/custody_client/withdraw.rs @@ -1,24 +1,47 @@ -use crate::error::FundsManagerError; +use std::str::FromStr; + +use crate::{error::FundsManagerError, helpers::get_secret}; use bigdecimal::{BigDecimal, FromPrimitive}; +use ethers::signers::LocalWallet; use fireblocks_sdk::types::TransactionStatus; +use tracing::info; use super::{CustodyClient, DepositWithdrawSource}; impl CustodyClient { - /// Withdraw gas from custody - pub(crate) async fn withdraw_with_token_addr( + /// Withdraw from hot wallet custody with a provided token address + pub(crate) async fn withdraw_from_hot_wallet( &self, source: DepositWithdrawSource, destination_address: &str, token_address: &str, amount: f64, ) -> Result<(), FundsManagerError> { - let symbol = self.get_erc20_token_symbol(token_address).await?; - self.withdraw(source, destination_address, &symbol, amount).await + // Find the wallet for the given destination and check its balance + let wallet = self.get_hot_wallet_by_vault(source.vault_name()).await?; + let bal = self.get_erc20_balance(token_address, &wallet.address).await?; + if bal < amount { + return Err(FundsManagerError::Custom("Insufficient balance".to_string())); + } + + // Fetch the wallet private key + let secret_name = Self::hot_wallet_secret_name(&wallet.address); + let private_key = get_secret(&secret_name, &self.aws_config).await?; + let wallet = + LocalWallet::from_str(private_key.as_str()).map_err(FundsManagerError::parse)?; + + // Execute the erc20 transfer + let tx = self.erc20_transfer(token_address, destination_address, amount, wallet).await?; + info!( + "Withdrew {amount} {token_address} from hot wallet to {destination_address}. Tx: {:#}", + tx.transaction_hash + ); + + Ok(()) } /// Withdraw funds from custody - pub(crate) async fn withdraw( + pub(crate) async fn withdraw_from_fireblocks( &self, source: DepositWithdrawSource, destination_address: &str, @@ -69,4 +92,31 @@ impl CustodyClient { Ok(()) } + + /// Withdraw gas + pub(crate) async fn withdraw_gas( + &self, + amount: f64, + to: &str, + ) -> Result<(), FundsManagerError> { + // Check the gas wallet's balance + let gas_vault_name = DepositWithdrawSource::Gas.vault_name(); + let gas_wallet = self.get_hot_wallet_by_vault(gas_vault_name).await?; + let bal = self.get_ether_balance(&gas_wallet.address).await?; + if bal < amount { + return Err(FundsManagerError::custom("Insufficient balance")); + } + + // Fetch the gas wallet's private key + let secret_name = Self::hot_wallet_secret_name(&gas_wallet.address); + let private_key = get_secret(&secret_name, &self.aws_config).await?; + let wallet = + LocalWallet::from_str(private_key.as_str()).map_err(FundsManagerError::parse)?; + + // Execute the transfer + let tx = self.transfer_ether(to, amount, wallet).await?; + info!("Withdrew {amount} ETH from gas wallet to {to}. Tx: {:#}", tx.transaction_hash); + + Ok(()) + } } diff --git a/funds-manager/funds-manager-server/src/fee_indexer/fee_balances.rs b/funds-manager/funds-manager-server/src/fee_indexer/fee_balances.rs index 39dc167..7692c66 100644 --- a/funds-manager/funds-manager-server/src/fee_indexer/fee_balances.rs +++ b/funds-manager/funds-manager-server/src/fee_indexer/fee_balances.rs @@ -54,10 +54,8 @@ impl Indexer { old_wallet.key_chain.secret_keys.sk_root.as_ref().expect("root key not present"); // Get the deposit address for the fee withdrawal - let deposit_address = self - .custody_client - .get_deposit_address(&mint, DepositWithdrawSource::FeeRedemption) - .await?; + let deposit_address = + self.custody_client.get_deposit_address(DepositWithdrawSource::FeeRedemption).await?; // Send a withdrawal request to the relayer let req = Self::build_withdrawal_request(&mint, &deposit_address, &old_wallet)?; diff --git a/funds-manager/funds-manager-server/src/handlers.rs b/funds-manager/funds-manager-server/src/handlers.rs index 65ecbfc..c87f3c8 100644 --- a/funds-manager/funds-manager-server/src/handlers.rs +++ b/funds-manager/funds-manager-server/src/handlers.rs @@ -52,7 +52,7 @@ pub(crate) async fn quoter_withdraw_handler( ) -> Result { server .custody_client - .withdraw_with_token_addr( + .withdraw_from_hot_wallet( DepositWithdrawSource::Quoter, &withdraw_request.address, &withdraw_request.mint, @@ -66,16 +66,11 @@ pub(crate) async fn quoter_withdraw_handler( /// Handler for retrieving the address to deposit custody funds to pub(crate) async fn get_deposit_address_handler( - query_params: HashMap, server: Arc, ) -> Result { - let mint = query_params.get(MINTS_QUERY_PARAM).ok_or_else(|| { - warp::reject::custom(ApiError::BadRequest("Missing 'mints' query parameter".to_string())) - })?; - let address = server .custody_client - .get_deposit_address(mint, DepositWithdrawSource::Quoter) + .get_deposit_address(DepositWithdrawSource::Quoter) .await .map_err(|e| warp::reject::custom(ApiError::InternalError(e.to_string())))?; let resp = DepositAddressResponse { address }; @@ -96,12 +91,7 @@ pub(crate) async fn withdraw_gas_handler( server .custody_client - .withdraw( - DepositWithdrawSource::Gas, - &withdraw_request.destination_address, - GAS_ASSET_NAME, - withdraw_request.amount, - ) + .withdraw_gas(withdraw_request.amount, &withdraw_request.destination_address) .await .map_err(|e| warp::reject::custom(ApiError::InternalError(e.to_string())))?; diff --git a/funds-manager/funds-manager-server/src/main.rs b/funds-manager/funds-manager-server/src/main.rs index 1976af4..d691721 100644 --- a/funds-manager/funds-manager-server/src/main.rs +++ b/funds-manager/funds-manager-server/src/main.rs @@ -46,7 +46,7 @@ use arbitrum_client::{ use clap::Parser; use funds_manager_api::WithdrawFundsRequest; use tracing::{error, warn}; -use warp::{filters::query::query, Filter}; +use warp::Filter; use crate::custody_client::CustodyClient; use crate::error::ApiError; @@ -325,7 +325,6 @@ async fn main() -> Result<(), Box> { .and(warp::path("custody")) .and(warp::path("quoters")) .and(warp::path(GET_DEPOSIT_ADDRESS_ROUTE)) - .and(query::>()) .and(with_server(server.clone())) .and_then(get_deposit_address_handler);