From 489872d3a188f58c260bd661c75f91f8c3a13c86 Mon Sep 17 00:00:00 2001 From: Joey Kraut Date: Tue, 30 Jul 2024 14:13:18 -0700 Subject: [PATCH] funds-manager: ndpoint to withdraw from fireblocks to hot wallet --- funds-manager/funds-manager-api/src/lib.rs | 13 ++++ .../src/custody_client/hot_wallets.rs | 76 +++++++++++++------ .../src/custody_client/mod.rs | 10 +++ .../src/custody_client/queries.rs | 13 ++++ .../funds-manager-server/src/handlers.rs | 15 +++- .../funds-manager-server/src/main.rs | 21 +++-- 6 files changed, 118 insertions(+), 30 deletions(-) diff --git a/funds-manager/funds-manager-api/src/lib.rs b/funds-manager/funds-manager-api/src/lib.rs index 2c428bb..9e664c9 100644 --- a/funds-manager/funds-manager-api/src/lib.rs +++ b/funds-manager/funds-manager-api/src/lib.rs @@ -32,6 +32,8 @@ pub const WITHDRAW_FEE_BALANCE_ROUTE: &str = "withdraw-fee-balance"; /// The route to transfer funds from a hot wallet to its backing vault pub const TRANSFER_TO_VAULT_ROUTE: &str = "transfer-to-vault"; +/// The route to withdraw funds from a hot wallet to Fireblocks +pub const WITHDRAW_TO_HOT_WALLET_ROUTE: &str = "withdraw-to-hot-wallet"; // ------------- // | Api Types | @@ -139,3 +141,14 @@ pub struct TransferToVaultRequest { /// The amount to transfer pub amount: f64, } + +/// The request body for transferring from Fireblocks to a hot wallet +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct WithdrawToHotWalletRequest { + /// The name of the vault to withdraw from + pub vault: String, + /// The mint of the asset to transfer + pub mint: String, + /// The amount to transfer + pub amount: f64, +} 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 9a2d266..058e495 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 @@ -17,11 +17,16 @@ use tracing::info; use super::{CustodyClient, ERC20}; use crate::{ + custody_client::DepositWithdrawSource, error::FundsManagerError, helpers::{create_secrets_manager_entry_with_description, get_secret}, }; impl CustodyClient { + // ------------ + // | Handlers | + // ------------ + /// Create a new hot wallet /// /// Returns the Arbitrum address of the hot wallet @@ -32,7 +37,7 @@ impl CustodyClient { let private_key = wallet.signer().to_bytes(); // Store the private key in Secrets Manager - let secret_name = format!("hot-wallet-{}", address); + let secret_name = Self::hot_wallet_secret_name(&address); let secret_value = hex::encode(private_key); let description = format!("Hot wallet for vault: {vault}"); create_secrets_manager_entry_with_description( @@ -72,29 +77,6 @@ impl CustodyClient { Ok(hot_wallet_balances) } - /// Fetch the token balance at the given address for a wallet - async fn get_token_balance( - &self, - wallet_address: &str, - token_address: &str, - provider: Arc>, - ) -> Result { - let wallet_address: Address = wallet_address.parse().map_err(|_| { - FundsManagerError::parse(format!("Invalid wallet address: {wallet_address}")) - })?; - let token_address: Address = token_address.parse().map_err(|_| { - FundsManagerError::parse(format!("Invalid token address: {token_address}")) - })?; - - let token = ERC20::new(token_address, provider); - token - .balance_of(wallet_address) - .call() - .await - .map(|balance| balance.as_u128()) - .map_err(FundsManagerError::arbitrum) - } - /// Transfer funds from a hot wallet to its backing Fireblocks vault pub async fn transfer_from_hot_wallet_to_vault( &self, @@ -122,4 +104,50 @@ impl CustodyClient { Ok(()) } + + pub async fn transfer_from_vault_to_hot_wallet( + &self, + vault: &str, + mint: &str, + amount: f64, + ) -> Result<(), FundsManagerError> { + // Fetch the wallet info, then withdraw + println!("finding wallet..."); + let wallet = self.get_hot_wallet_by_vault(vault).await?; + println!("found wallet..."); + let source = DepositWithdrawSource::from_vault_name(vault)?; + self.withdraw_with_token_addr(source, &wallet.address, mint, amount).await + } + + // ------------ + // | Handlers | + // ------------ + + /// The secret name for a hot wallet + fn hot_wallet_secret_name(address: &str) -> String { + format!("hot-wallet-{address}") + } + + /// Fetch the token balance at the given address for a wallet + async fn get_token_balance( + &self, + wallet_address: &str, + token_address: &str, + provider: Arc>, + ) -> Result { + let wallet_address: Address = wallet_address.parse().map_err(|_| { + FundsManagerError::parse(format!("Invalid wallet address: {wallet_address}")) + })?; + let token_address: Address = token_address.parse().map_err(|_| { + FundsManagerError::parse(format!("Invalid token address: {token_address}")) + })?; + + let token = ERC20::new(token_address, provider); + token + .balance_of(wallet_address) + .call() + .await + .map(|balance| balance.as_u128()) + .map_err(FundsManagerError::arbitrum) + } } 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 b7c1f06..c542c0e 100644 --- a/funds-manager/funds-manager-server/src/custody_client/mod.rs +++ b/funds-manager/funds-manager-server/src/custody_client/mod.rs @@ -56,6 +56,16 @@ impl DepositWithdrawSource { Self::Gas => "Arbitrum Gas", } } + + /// Build a `DepositWithdrawSource` from a vault name + pub fn from_vault_name(name: &str) -> Result { + match name.to_lowercase().as_str() { + "quoters" => Ok(Self::Quoter), + "fee collection" => Ok(Self::FeeRedemption), + "arbitrum gas" => Ok(Self::Gas), + _ => Err(FundsManagerError::parse(format!("invalid vault name: {name}"))), + } + } } /// The client interacting with the custody backend diff --git a/funds-manager/funds-manager-server/src/custody_client/queries.rs b/funds-manager/funds-manager-server/src/custody_client/queries.rs index e5265fe..b8994d9 100644 --- a/funds-manager/funds-manager-server/src/custody_client/queries.rs +++ b/funds-manager/funds-manager-server/src/custody_client/queries.rs @@ -51,4 +51,17 @@ impl CustodyClient { .await .map_err(err_str!(FundsManagerError::Db)) } + + /// Get a hot wallet for the given vault + pub async fn get_hot_wallet_by_vault( + &self, + vault: &str, + ) -> Result { + let mut conn = self.get_db_conn().await?; + hot_wallets::table + .filter(hot_wallets::vault.eq(vault)) + .first::(&mut conn) + .await + .map_err(err_str!(FundsManagerError::Db)) + } } diff --git a/funds-manager/funds-manager-server/src/handlers.rs b/funds-manager/funds-manager-server/src/handlers.rs index c82dd79..65ecbfc 100644 --- a/funds-manager/funds-manager-server/src/handlers.rs +++ b/funds-manager/funds-manager-server/src/handlers.rs @@ -7,7 +7,7 @@ use bytes::Bytes; use funds_manager_api::{ CreateHotWalletRequest, CreateHotWalletResponse, DepositAddressResponse, FeeWalletsResponse, HotWalletBalancesResponse, TransferToVaultRequest, WithdrawFeeBalanceRequest, - WithdrawFundsRequest, WithdrawGasRequest, + WithdrawFundsRequest, WithdrawGasRequest, WithdrawToHotWalletRequest, }; use itertools::Itertools; use std::collections::HashMap; @@ -181,3 +181,16 @@ pub(crate) async fn transfer_to_vault_handler( Ok(warp::reply::json(&"Transfer from hot wallet to vault initiated")) } + +/// Handler for withdrawing funds from a vault to its hot wallet +pub(crate) async fn withdraw_from_vault_handler( + req: WithdrawToHotWalletRequest, + server: Arc, +) -> Result { + server + .custody_client + .transfer_from_vault_to_hot_wallet(&req.vault, &req.mint, req.amount) + .await + .map_err(|e| warp::reject::custom(ApiError::InternalError(e.to_string())))?; + Ok(warp::reply::json(&"Withdrawal from vault to hot wallet initiated")) +} diff --git a/funds-manager/funds-manager-server/src/main.rs b/funds-manager/funds-manager-server/src/main.rs index 14d8050..1976af4 100644 --- a/funds-manager/funds-manager-server/src/main.rs +++ b/funds-manager/funds-manager-server/src/main.rs @@ -22,15 +22,15 @@ use ethers::signers::LocalWallet; use fee_indexer::Indexer; use funds_manager_api::{ CreateHotWalletRequest, TransferToVaultRequest, WithdrawFeeBalanceRequest, WithdrawGasRequest, - GET_DEPOSIT_ADDRESS_ROUTE, GET_FEE_WALLETS_ROUTE, INDEX_FEES_ROUTE, PING_ROUTE, - REDEEM_FEES_ROUTE, TRANSFER_TO_VAULT_ROUTE, WITHDRAW_CUSTODY_ROUTE, WITHDRAW_FEE_BALANCE_ROUTE, - WITHDRAW_GAS_ROUTE, + WithdrawToHotWalletRequest, GET_DEPOSIT_ADDRESS_ROUTE, GET_FEE_WALLETS_ROUTE, INDEX_FEES_ROUTE, + PING_ROUTE, REDEEM_FEES_ROUTE, TRANSFER_TO_VAULT_ROUTE, WITHDRAW_CUSTODY_ROUTE, + WITHDRAW_FEE_BALANCE_ROUTE, WITHDRAW_GAS_ROUTE, WITHDRAW_TO_HOT_WALLET_ROUTE, }; use handlers::{ create_hot_wallet_handler, get_deposit_address_handler, get_fee_wallets_handler, get_hot_wallet_balances_handler, index_fees_handler, quoter_withdraw_handler, redeem_fees_handler, transfer_to_vault_handler, withdraw_fee_balance_handler, - withdraw_gas_handler, + withdraw_from_vault_handler, withdraw_gas_handler, }; use middleware::{identity, with_hmac_auth, with_json_body}; use relayer_client::RelayerClient; @@ -370,6 +370,16 @@ async fn main() -> Result<(), Box> { .and(with_server(server.clone())) .and_then(transfer_to_vault_handler); + let transfer_to_hot_wallet = warp::post() + .and(warp::path("custody")) + .and(warp::path("hot-wallets")) + .and(warp::path(WITHDRAW_TO_HOT_WALLET_ROUTE)) + .and(with_hmac_auth(server.clone())) + .map(with_json_body::) + .and_then(identity) + .and(with_server(server.clone())) + .and_then(withdraw_from_vault_handler); + let routes = ping .or(index_fees) .or(redeem_fees) @@ -379,8 +389,9 @@ async fn main() -> Result<(), Box> { .or(get_balances) .or(withdraw_fee_balance) .or(transfer_to_vault) - .or(create_hot_wallet) + .or(transfer_to_hot_wallet) .or(get_hot_wallet_balances) + .or(create_hot_wallet) .recover(handle_rejection); warp::serve(routes).run(([0, 0, 0, 0], cli.port)).await;