Skip to content

Commit

Permalink
funds-manager: ndpoint to withdraw from fireblocks to hot wallet
Browse files Browse the repository at this point in the history
  • Loading branch information
joeykraut committed Jul 30, 2024
1 parent 0ce3163 commit 489872d
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 30 deletions.
13 changes: 13 additions & 0 deletions funds-manager/funds-manager-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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,
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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<Provider<Http>>,
) -> Result<u128, FundsManagerError> {
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,
Expand Down Expand Up @@ -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<Provider<Http>>,
) -> Result<u128, FundsManagerError> {
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)
}
}
10 changes: 10 additions & 0 deletions funds-manager/funds-manager-server/src/custody_client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self, FundsManagerError> {
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
Expand Down
13 changes: 13 additions & 0 deletions funds-manager/funds-manager-server/src/custody_client/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<HotWallet, FundsManagerError> {
let mut conn = self.get_db_conn().await?;
hot_wallets::table
.filter(hot_wallets::vault.eq(vault))
.first::<HotWallet>(&mut conn)
.await
.map_err(err_str!(FundsManagerError::Db))
}
}
15 changes: 14 additions & 1 deletion funds-manager/funds-manager-server/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Server>,
) -> Result<Json, warp::Rejection> {
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"))
}
21 changes: 16 additions & 5 deletions funds-manager/funds-manager-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -370,6 +370,16 @@ async fn main() -> Result<(), Box<dyn Error>> {
.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::<WithdrawToHotWalletRequest>)
.and_then(identity)
.and(with_server(server.clone()))
.and_then(withdraw_from_vault_handler);

let routes = ping
.or(index_fees)
.or(redeem_fees)
Expand All @@ -379,8 +389,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
.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;

Expand Down

0 comments on commit 489872d

Please sign in to comment.