Skip to content

Commit

Permalink
funds-manager: custody-client: deposit: Endpoint to fetch deposit addr
Browse files Browse the repository at this point in the history
Adds an endpoint to fetch a deposit address for the quoters. This
endpoint looks up the available account asset wallets for the quoter
vault and finds the asset prefixed with the ERC20 symbol.
  • Loading branch information
joeykraut committed Jul 23, 2024
1 parent c04a549 commit fc59fa0
Show file tree
Hide file tree
Showing 8 changed files with 351 additions and 42 deletions.
3 changes: 3 additions & 0 deletions funds-manager/funds-manager-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ version = "0.1.0"
edition = "2021"

[dependencies]

serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.117"
27 changes: 27 additions & 0 deletions funds-manager/funds-manager-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#![deny(missing_docs)]
#![deny(clippy::missing_docs_in_private_items)]

use serde::{Deserialize, Serialize};

// --------------
// | Api Routes |
// --------------
Expand All @@ -12,3 +14,28 @@ pub const PING_ROUTE: &str = "ping";
pub const INDEX_FEES_ROUTE: &str = "index-fees";
/// The route through which a client may start the fee redemption process
pub const REDEEM_FEES_ROUTE: &str = "redeem-fees";

/// The route to retrieve the address to deposit custody funds to
pub const GET_DEPOSIT_ADDRESS_ROUTE: &str = "deposit-address";
/// The route to withdraw funds from custody
pub const WITHDRAW_CUSTODY_ROUTE: &str = "withdraw";

// -------------
// | Api Types |
// -------------

/// A response containing the deposit address
#[derive(Debug, Serialize, Deserialize)]
pub struct DepositAddressResponse {
/// The deposit address
pub address: String,
}

/// The request body for withdrawing funds from custody
#[derive(Debug, Serialize, Deserialize)]
pub struct WithdrawFundsRequest {
/// The mint of the asset to withdraw
pub mint: String,
/// The amount of funds to withdraw
pub amount: u128,
}
1 change: 1 addition & 0 deletions funds-manager/funds-manager-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +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"
native-tls = "0.2"
postgres-native-tls = "0.5"
tokio-postgres = "0.7.7"
Expand Down
81 changes: 81 additions & 0 deletions funds-manager/funds-manager-server/src/custody_client/deposit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//! 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};

impl CustodyClient {
/// Get the deposit address for the given mint
pub(crate) async fn get_deposit_address(
&self,
mint: &str,
source: DepositSource,
) -> 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()),
))?;

// TODO: Create an account asset if one doesn't exist
let asset = self.get_wallet_for_ticker(&deposit_vault, &symbol).ok_or(
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
)))?;

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
}
}
87 changes: 87 additions & 0 deletions funds-manager/funds-manager-server/src/custody_client/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//! Manages the custody backend for the funds manager
#![allow(missing_docs)]
pub mod deposit;

use ethers::prelude::abigen;
use ethers::providers::{Http, Provider};
use ethers::types::Address;
use fireblocks_sdk::{Client as FireblocksClient, ClientBuilder as FireblocksClientBuilder};
use renegade_util::err_str;
use std::str::FromStr;
use std::sync::Arc;

use crate::error::FundsManagerError;

abigen!(
ERC20,
r#"[
function symbol() external view returns (string memory)
]"#
);

/// The source of a deposit
pub(crate) enum DepositSource {
/// A Renegade quoter
Quoter,
/// A fee withdrawal
FeeWithdrawal,

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

View workflow job for this annotation

GitHub Actions / clippy

variant `FeeWithdrawal` is never constructed

error: variant `FeeWithdrawal` is never constructed --> funds-manager/funds-manager-server/src/custody_client/mod.rs:27:5 | 23 | pub(crate) enum DepositSource { | ------------- variant in this enum ... 27 | FeeWithdrawal, | ^^^^^^^^^^^^^ | = note: `-D dead-code` implied by `-D warnings` = help: to override `-D warnings` add `#[allow(dead_code)]`
}

impl DepositSource {
/// 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"),
}
}
}

/// The client interacting with the custody backend
#[derive(Clone)]
pub struct CustodyClient {
/// The API key for the Fireblocks API
fireblocks_api_key: String,
/// The API secret for the Fireblocks API
fireblocks_api_secret: Vec<u8>,
/// The arbitrum RPC url to use for the custody client
arbitrum_rpc_url: String,
}

impl CustodyClient {
/// Create a new CustodyClient
#[allow(clippy::needless_pass_by_value)]
pub fn new(
fireblocks_api_key: String,
fireblocks_api_secret: String,
arbitrum_rpc_url: String,
) -> Self {
let fireblocks_api_secret = fireblocks_api_secret.as_bytes().to_vec();
Self { fireblocks_api_key, fireblocks_api_secret, arbitrum_rpc_url }
}

/// Get a fireblocks client
pub fn get_fireblocks_client(&self) -> Result<FireblocksClient, FundsManagerError> {
FireblocksClientBuilder::new(&self.fireblocks_api_key, &self.fireblocks_api_secret)
// TODO: Remove the sandbox config
.with_sandbox()
.build()
.map_err(FundsManagerError::fireblocks)
}

/// Get the symbol for an ERC20 token at the given address
pub(self) async fn get_erc20_token_symbol(
&self,
token_address: &str,
) -> Result<String, FundsManagerError> {
let addr =
Address::from_str(token_address).map_err(err_str!(FundsManagerError::Arbitrum))?;
let provider = Provider::<Http>::try_from(&self.arbitrum_rpc_url)
.map_err(err_str!(FundsManagerError::Arbitrum))?;
let client = Arc::new(provider);
let erc20 = ERC20::new(addr, client);

erc20.symbol().call().await.map_err(FundsManagerError::arbitrum)
}
}
25 changes: 25 additions & 0 deletions funds-manager/funds-manager-server/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ use std::{error::Error, fmt::Display};

use warp::reject::Reject;

use fireblocks_sdk::{ClientError as FireblocksClientError, FireblocksError};

/// The error type emitted by the funds manager
#[derive(Debug, Clone)]
pub enum FundsManagerError {
/// An error with the arbitrum client
Arbitrum(String),
/// An error with a database query
Db(String),
/// An error with Fireblocks operations
Fireblocks(String),
/// An error executing an HTTP request
Http(String),
/// An error parsing a value
Expand All @@ -33,6 +37,11 @@ impl FundsManagerError {
FundsManagerError::Db(msg.to_string())
}

/// Create a Fireblocks error
pub fn fireblocks<T: ToString>(msg: T) -> FundsManagerError {
FundsManagerError::Fireblocks(msg.to_string())
}

/// Create an HTTP error
pub fn http<T: ToString>(msg: T) -> FundsManagerError {
FundsManagerError::Http(msg.to_string())
Expand Down Expand Up @@ -63,12 +72,25 @@ impl Display for FundsManagerError {
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::Fireblocks(e) => write!(f, "Fireblocks error: {}", e),
}
}
}
impl Error for FundsManagerError {}
impl Reject for FundsManagerError {}

impl From<FireblocksClientError> for FundsManagerError {
fn from(error: FireblocksClientError) -> Self {
FundsManagerError::Fireblocks(error.to_string())
}
}

impl From<FireblocksError> for FundsManagerError {
fn from(error: FireblocksError) -> Self {
FundsManagerError::Fireblocks(error.to_string())
}
}

/// API-specific error type
#[derive(Debug)]
pub enum ApiError {
Expand All @@ -78,6 +100,8 @@ pub enum ApiError {
RedemptionError(String),
/// Internal server error
InternalError(String),
/// Bad request error
BadRequest(String),
}

impl Reject for ApiError {}
Expand All @@ -88,6 +112,7 @@ impl Display for ApiError {
ApiError::IndexingError(e) => write!(f, "Indexing error: {}", e),
ApiError::RedemptionError(e) => write!(f, "Redemption error: {}", e),
ApiError::InternalError(e) => write!(f, "Internal error: {}", e),
ApiError::BadRequest(e) => write!(f, "Bad request: {}", e),
}
}
}
Expand Down
62 changes: 62 additions & 0 deletions funds-manager/funds-manager-server/src/handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//! Route handlers for the funds manager

use crate::custody_client::DepositSource;
use crate::error::ApiError;
use crate::Server;
use funds_manager_api::DepositAddressResponse;
use std::collections::HashMap;
use std::sync::Arc;
use warp::reply::Json;

/// The "mint" query param
pub const MINT_QUERY_PARAM: &str = "mint";

/// Handler for indexing fees
pub(crate) async fn index_fees_handler(server: Arc<Server>) -> Result<Json, warp::Rejection> {
let mut indexer = server
.build_indexer()
.await
.map_err(|e| warp::reject::custom(ApiError::InternalError(e.to_string())))?;
indexer
.index_fees()
.await
.map_err(|e| warp::reject::custom(ApiError::IndexingError(e.to_string())))?;
Ok(warp::reply::json(&"Fees indexed successfully"))
}

/// Handler for redeeming fees
pub(crate) async fn redeem_fees_handler(server: Arc<Server>) -> Result<Json, warp::Rejection> {
let mut indexer = server
.build_indexer()
.await
.map_err(|e| warp::reject::custom(ApiError::InternalError(e.to_string())))?;
indexer
.redeem_fees()
.await
.map_err(|e| warp::reject::custom(ApiError::RedemptionError(e.to_string())))?;
Ok(warp::reply::json(&"Fees redeemed successfully"))
}

/// 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")
}

/// Handler for retrieving the address to deposit custody funds to
pub(crate) async fn get_deposit_address_handler(
query_params: HashMap<String, String>,
server: Arc<Server>,
) -> Result<Json, warp::Rejection> {
let mint = query_params.get(MINT_QUERY_PARAM).ok_or_else(|| {
warp::reject::custom(ApiError::BadRequest("Missing 'mint' query parameter".to_string()))
})?;

let address = server
.custody_client
.get_deposit_address(mint, DepositSource::Quoter)
.await
.map_err(|e| warp::reject::custom(ApiError::InternalError(e.to_string())))?;
let resp = DepositAddressResponse { address };
Ok(warp::reply::json(&resp))
}
Loading

0 comments on commit fc59fa0

Please sign in to comment.