Skip to content

Commit

Permalink
funds-manager: custody-client: Add withdrawal handler
Browse files Browse the repository at this point in the history
  • Loading branch information
joeykraut committed Jul 23, 2024
1 parent e42c5f2 commit 06a6747
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 73 deletions.
2 changes: 2 additions & 0 deletions funds-manager/funds-manager-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,6 @@ pub struct WithdrawFundsRequest {
pub mint: String,
/// The amount of funds to withdraw
pub amount: u128,
/// The address to withdraw to
pub address: String,
}
2 changes: 1 addition & 1 deletion funds-manager/funds-manager-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ aws-sdk-secretsmanager = "1.37"
aws-config = "1.5"
diesel = { workspace = true, features = ["postgres", "numeric", "uuid"] }
diesel-async = { workspace = true, features = ["postgres"] }
fireblocks-sdk = "0.4"
fireblocks-sdk = { git = "https://github.com/renegade-fi/fireblocks-sdk-rs" }
native-tls = "0.2"
postgres-native-tls = "0.5"
tokio-postgres = "0.7.7"
Expand Down
69 changes: 14 additions & 55 deletions funds-manager/funds-manager-server/src/custody_client/deposit.rs
Original file line number Diff line number Diff line change
@@ -1,81 +1,40 @@
//! Deposit funds into the custody backend

use fireblocks_sdk::{
types::{Account as FireblocksAccount, AccountAsset},
PagingVaultRequestBuilder,
};
use renegade_util::err_str;

use crate::error::FundsManagerError;

use super::{CustodyClient, DepositSource};
use super::{CustodyClient, DepositWithdrawSource};

impl CustodyClient {
/// Get the deposit address for the given mint
pub(crate) async fn get_deposit_address(
&self,
mint: &str,
source: DepositSource,
source: DepositWithdrawSource,
) -> Result<String, FundsManagerError> {
// Find a vault account for the asset
let symbol = self.get_erc20_token_symbol(mint).await?;
let deposit_vault =
self.get_vault_account(&source).await?.ok_or(FundsManagerError::fireblocks(
format!("no vault for deposit source: {}", source.get_vault_name()),
))?;
let deposit_vault = self.get_vault_account(&source).await?.ok_or_else(|| {
FundsManagerError::fireblocks(format!(
"no vault for deposit source: {}",
source.get_vault_name()
))
})?;

// TODO: Create an account asset if one doesn't exist
let asset = self.get_wallet_for_ticker(&deposit_vault, &symbol).ok_or(
let asset = self.get_wallet_for_ticker(&deposit_vault, &symbol).ok_or_else(|| {
FundsManagerError::fireblocks(format!(
"no wallet for deposit source: {}",
source.get_vault_name()
)),
)?;
))
})?;

// Fetch the wallet addresses for the asset
let client = self.get_fireblocks_client()?;
let (addresses, _rid) = client.addresses(deposit_vault.id, &asset.id).await?;
let addr = addresses.first().ok_or(FundsManagerError::fireblocks(format!(
"no addresses for asset: {}",
asset.id
)))?;
let addr = addresses.first().ok_or_else(|| {
FundsManagerError::fireblocks(format!("no addresses for asset: {}", asset.id))
})?;

Ok(addr.address.clone())
}

/// Get the vault account for a given asset and source
async fn get_vault_account(
&self,
source: &DepositSource,
) -> Result<Option<FireblocksAccount>, FundsManagerError> {
let client = self.get_fireblocks_client()?;
let req = PagingVaultRequestBuilder::new()
.limit(100)
.build()
.map_err(err_str!(FundsManagerError::Fireblocks))?;

let (vaults, _rid) = client.vaults(req).await?;
for vault in vaults.accounts.into_iter() {
if vault.name == source.get_vault_name() {
return Ok(Some(vault));
}
}

Ok(None)
}

/// Find the wallet in a vault account for a given symbol
fn get_wallet_for_ticker(
&self,
vault: &FireblocksAccount,
symbol: &str,
) -> Option<AccountAsset> {
for acct in vault.assets.iter() {
if acct.id.starts_with(symbol) {
return Some(acct.clone());
}
}

None
}
}
74 changes: 66 additions & 8 deletions funds-manager/funds-manager-server/src/custody_client/mod.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
//! Manages the custody backend for the funds manager
#![allow(missing_docs)]
pub mod deposit;
pub mod withdraw;

use ethers::prelude::abigen;
use ethers::providers::{Http, Provider};
use ethers::types::Address;
use fireblocks_sdk::{Client as FireblocksClient, ClientBuilder as FireblocksClientBuilder};
use fireblocks_sdk::types::Transaction;
use fireblocks_sdk::{
types::{Account as FireblocksAccount, AccountAsset},
Client as FireblocksClient, ClientBuilder as FireblocksClientBuilder,
};
use renegade_util::err_str;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use tracing::info;

use crate::error::FundsManagerError;

Expand All @@ -20,20 +27,24 @@ abigen!(
);

/// The source of a deposit
pub(crate) enum DepositSource {
/// A Renegade quoter
#[derive(Clone, Copy)]
pub(crate) enum DepositWithdrawSource {
/// A Renegade quoter deposit or withdrawal
Quoter,
/// A fee withdrawal
FeeWithdrawal,
/// A fee redemption deposit
FeeRedemption,

Check failure on line 35 in funds-manager/funds-manager-server/src/custody_client/mod.rs

View workflow job for this annotation

GitHub Actions / clippy

variants `FeeRedemption` and `RelayerGas` are never constructed

error: variants `FeeRedemption` and `RelayerGas` are never constructed --> funds-manager/funds-manager-server/src/custody_client/mod.rs:35:5 | 31 | pub(crate) enum DepositWithdrawSource { | --------------------- variants in this enum ... 35 | FeeRedemption, | ^^^^^^^^^^^^^ 36 | /// A gas withdrawal 37 | RelayerGas, | ^^^^^^^^^^ | = note: `DepositWithdrawSource` has a derived impl for the trait `Clone`, but this is intentionally ignored during dead code analysis = note: `-D dead-code` implied by `-D warnings` = help: to override `-D warnings` add `#[allow(dead_code)]`
/// A gas withdrawal
RelayerGas,
}

impl DepositSource {
impl DepositWithdrawSource {
/// Get the Fireblocks vault name into which the given deposit source should
/// deposit funds
pub(crate) fn get_vault_name(&self) -> &str {
match self {
DepositSource::Quoter => "Quoters",
DepositSource::FeeWithdrawal => unimplemented!("no vault for fee withdrawal yet"),
Self::Quoter => "Quoters",
Self::FeeRedemption => unimplemented!("no vault for fee redemption yet"),
Self::RelayerGas => unimplemented!("no vault for relayer gas yet"),
}
}
}
Expand Down Expand Up @@ -84,4 +95,51 @@ impl CustodyClient {

erc20.symbol().call().await.map_err(FundsManagerError::arbitrum)
}

/// Get the vault account for a given asset and source
pub(crate) async fn get_vault_account(
&self,
source: &DepositWithdrawSource,
) -> Result<Option<FireblocksAccount>, FundsManagerError> {
let client = self.get_fireblocks_client()?;
let req = fireblocks_sdk::PagingVaultRequestBuilder::new()
.limit(100)
.build()
.map_err(err_str!(FundsManagerError::Fireblocks))?;

let (vaults, _rid) = client.vaults(req).await?;
for vault in vaults.accounts.into_iter() {
if vault.name == source.get_vault_name() {
return Ok(Some(vault));
}
}

Ok(None)
}

/// Find the wallet in a vault account for a given symbol
pub(crate) fn get_wallet_for_ticker(
&self,
vault: &FireblocksAccount,
symbol: &str,
) -> Option<AccountAsset> {
vault.assets.iter().find(|acct| acct.id.starts_with(symbol)).cloned()
}

/// Poll a fireblocks transaction for completion
pub(crate) async fn poll_fireblocks_transaction(
&self,
transaction_id: &str,
) -> Result<Transaction, FundsManagerError> {
let client = self.get_fireblocks_client()?;
let timeout = Duration::from_secs(60);
let interval = Duration::from_secs(5);
client
.poll_transaction(transaction_id, timeout, interval, |tx| {
info!("tx {}: {:?}", transaction_id, tx.status);
})
.await
.map_err(FundsManagerError::fireblocks)
.map(|(tx, _rid)| tx)
}
}
60 changes: 60 additions & 0 deletions funds-manager/funds-manager-server/src/custody_client/withdraw.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use crate::error::FundsManagerError;
use bigdecimal::{BigDecimal, FromPrimitive};
use fireblocks_sdk::types::TransactionStatus;

use super::{CustodyClient, DepositWithdrawSource};

impl CustodyClient {
/// Withdraw funds from custody
pub(crate) async fn withdraw(
&self,
source: DepositWithdrawSource,
token_address: &str,
amount: u128,
destination_address: String,
) -> Result<(), FundsManagerError> {
let client = self.get_fireblocks_client()?;
let symbol = self.get_erc20_token_symbol(token_address).await?;

// Get the vault account and asset to transfer from
let vault = self
.get_vault_account(&source)
.await?
.ok_or_else(|| FundsManagerError::Custom("Vault not found".to_string()))?;

let asset = self.get_wallet_for_ticker(&vault, &symbol).ok_or_else(|| {
FundsManagerError::Custom(format!("Asset not found for symbol: {}", symbol))
})?;

// Check if the available balance is sufficient
let withdraw_amount = BigDecimal::from_u128(amount).expect("amount too large");
if asset.available < withdraw_amount {
return Err(FundsManagerError::Custom(format!(
"Insufficient balance. Available: {}, Requested: {}",
asset.available, withdraw_amount
)));
}

// Transfer
let vault_name = source.get_vault_name();
let note = format!("Withdraw {amount} {symbol} from {vault_name} to {destination_address}");

let (resp, _rid) = client
.create_transaction_external(
vault.id,
destination_address,
asset.id,
withdraw_amount,
Some(&note),
)
.await?;

let tx = self.poll_fireblocks_transaction(&resp.id).await?;
if tx.status != TransactionStatus::COMPLETED && tx.status != TransactionStatus::CONFIRMING {
let err_msg = format!("Transaction failed: {:?}", tx.status);
return Err(FundsManagerError::Custom(err_msg));
}

Ok(())
}
}
2 changes: 1 addition & 1 deletion funds-manager/funds-manager-server/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ impl Display for FundsManagerError {
FundsManagerError::Http(e) => write!(f, "HTTP error: {}", e),
FundsManagerError::Parse(e) => write!(f, "Parse error: {}", e),
FundsManagerError::SecretsManager(e) => write!(f, "Secrets manager error: {}", e),
FundsManagerError::Custom(e) => write!(f, "Custom error: {}", e),
FundsManagerError::Custom(e) => write!(f, "Uncategorized error: {}", e),
FundsManagerError::Fireblocks(e) => write!(f, "Fireblocks error: {}", e),
}
}
Expand Down
25 changes: 19 additions & 6 deletions funds-manager/funds-manager-server/src/handlers.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
//! Route handlers for the funds manager

use crate::custody_client::DepositSource;
use crate::custody_client::DepositWithdrawSource;
use crate::error::ApiError;
use crate::Server;
use funds_manager_api::DepositAddressResponse;
use funds_manager_api::{DepositAddressResponse, WithdrawFundsRequest};
use std::collections::HashMap;
use std::sync::Arc;
use warp::reply::Json;
Expand Down Expand Up @@ -38,9 +38,22 @@ pub(crate) async fn redeem_fees_handler(server: Arc<Server>) -> Result<Json, war
}

/// Handler for withdrawing funds from custody
pub(crate) async fn withdraw_funds_handler(_server: Arc<Server>) -> Result<Json, warp::Rejection> {
// Implement the withdrawal logic here
todo!("Implement withdrawal from custody")
pub(crate) async fn quoter_withdraw_handler(
withdraw_request: WithdrawFundsRequest,
server: Arc<Server>,
) -> Result<Json, warp::Rejection> {
server
.custody_client
.withdraw(
DepositWithdrawSource::Quoter,
&withdraw_request.mint,
withdraw_request.amount,
withdraw_request.address,
)
.await
.map_err(|e| warp::reject::custom(ApiError::InternalError(e.to_string())))?;

Ok(warp::reply::json(&"Withdrawal complete"))
}

/// Handler for retrieving the address to deposit custody funds to
Expand All @@ -54,7 +67,7 @@ pub(crate) async fn get_deposit_address_handler(

let address = server
.custody_client
.get_deposit_address(mint, DepositSource::Quoter)
.get_deposit_address(mint, DepositWithdrawSource::Quoter)
.await
.map_err(|e| warp::reject::custom(ApiError::InternalError(e.to_string())))?;
let resp = DepositAddressResponse { address };
Expand Down
5 changes: 3 additions & 2 deletions funds-manager/funds-manager-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use funds_manager_api::{
WITHDRAW_CUSTODY_ROUTE,
};
use handlers::{
get_deposit_address_handler, index_fees_handler, redeem_fees_handler, withdraw_funds_handler,
get_deposit_address_handler, index_fees_handler, quoter_withdraw_handler, redeem_fees_handler,
};
use relayer_client::RelayerClient;
use renegade_circuit_types::elgamal::DecryptionKey;
Expand Down Expand Up @@ -228,8 +228,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
.and(warp::path("custody"))
.and(warp::path("quoters"))
.and(warp::path(WITHDRAW_CUSTODY_ROUTE))
.and(warp::body::json())
.and(with_server(Arc::new(server.clone())))
.and_then(withdraw_funds_handler);
.and_then(quoter_withdraw_handler);

let get_deposit_address = warp::get()
.and(warp::path("custody"))
Expand Down

0 comments on commit 06a6747

Please sign in to comment.