Skip to content

Commit

Permalink
Merge pull request #19 from renegade-fi/joey/hot-wallet-create
Browse files Browse the repository at this point in the history
funds-manager: Add endpoint to create new hot wallets
  • Loading branch information
joeykraut authored Jul 30, 2024
2 parents e64f1b7 + b1cf633 commit e653139
Show file tree
Hide file tree
Showing 12 changed files with 245 additions and 57 deletions.
50 changes: 37 additions & 13 deletions funds-manager/funds-manager-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,36 @@ pub const WITHDRAW_GAS_ROUTE: &str = "withdraw-gas";

/// The route to get fee wallets
pub const GET_FEE_WALLETS_ROUTE: &str = "get-fee-wallets";

/// The route to withdraw a fee balance
pub const WITHDRAW_FEE_BALANCE_ROUTE: &str = "withdraw-fee-balance";

/// The route to create a new hot wallet
pub const CREATE_HOT_WALLET_ROUTE: &str = "create-hot-wallet";

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

// --- Fee Indexing & Management --- //

/// The response containing fee wallets
#[derive(Debug, Serialize, Deserialize)]
pub struct FeeWalletsResponse {
/// The wallets managed by the funds manager
pub wallets: Vec<ApiWallet>,
}

/// The request body for withdrawing a fee balance
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct WithdrawFeeBalanceRequest {
/// The ID of the wallet to withdraw from
pub wallet_id: Uuid,
/// The mint of the asset to withdraw
pub mint: String,
}

// --- Quoters --- //

/// A response containing the deposit address
#[derive(Debug, Serialize, Deserialize)]
pub struct DepositAddressResponse {
Expand All @@ -53,6 +75,8 @@ pub struct WithdrawFundsRequest {
pub address: String,
}

// --- Gas --- //

// Update request body name and documentation
/// The request body for withdrawing gas from custody
#[derive(Clone, Debug, Serialize, Deserialize)]
Expand All @@ -63,18 +87,18 @@ pub struct WithdrawGasRequest {
pub destination_address: String,
}

/// The response containing fee wallets
#[derive(Debug, Serialize, Deserialize)]
pub struct FeeWalletsResponse {
/// The wallets managed by the funds manager
pub wallets: Vec<ApiWallet>,
}
// --- Hot Wallets --- //

/// The request body for withdrawing a fee balance
/// The request body for creating a hot wallet
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct WithdrawFeeBalanceRequest {
/// The ID of the wallet to withdraw from
pub wallet_id: Uuid,
/// The mint of the asset to withdraw
pub mint: String,
pub struct CreateHotWalletRequest {
/// The name of the vault backing the hot wallet
pub vault: String,
}

/// The response containing the hot wallet's address
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateHotWalletResponse {
/// The address of the hot wallet
pub address: String,
}
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 @@ -54,6 +54,7 @@ futures = "0.3"
http = "1.1"
itertools = "0.13"
num-bigint = "0.4"
rand = "0.8"
reqwest = { version = "0.12", features = ["json"] }
serde = "1.0"
serde_json = "1.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//! Handlers for managing hot wallets
//!
//! We store funds in hot wallets to prevent excessive in/out-flow from
//! Fireblocks

use ethers::{
signers::{LocalWallet, Signer},
utils::hex::ToHexExt,
};
use rand::thread_rng;
use tracing::info;

use super::CustodyClient;
use crate::{error::FundsManagerError, helpers::create_secrets_manager_entry_with_description};

impl CustodyClient {
/// Create a new hot wallet
///
/// Returns the Arbitrum address of the hot wallet
pub async fn create_hot_wallet(&self, vault: String) -> Result<String, FundsManagerError> {
// Generate a new Ethereum keypair
let wallet = LocalWallet::new(&mut thread_rng());
let address = wallet.address().encode_hex();
let private_key = wallet.signer().to_bytes();

// Store the private key in Secrets Manager
let secret_name = format!("hot-wallet-{}", address);
let secret_value = hex::encode(private_key);
let description = format!("Hot wallet for vault: {vault}");
create_secrets_manager_entry_with_description(
&secret_name,
&secret_value,
&self.aws_config,
&description,
)
.await?;

// Insert the wallet metadata into the database
self.insert_hot_wallet(&address, &vault, &secret_name).await?;
info!("Created hot wallet with address: {} for vault: {}", address, vault);
Ok(address)
}
}
8 changes: 7 additions & 1 deletion funds-manager/funds-manager-server/src/custody_client/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
//! Manages the custody backend for the funds manager
#![allow(missing_docs)]
pub mod deposit;
mod hot_wallets;
mod queries;
pub mod withdraw;

use aws_config::SdkConfig as AwsConfig;
use ethers::prelude::abigen;
use ethers::providers::{Http, Provider};
use ethers::types::Address;
Expand Down Expand Up @@ -61,6 +64,8 @@ pub struct CustodyClient {
arbitrum_rpc_url: String,
/// The database connection pool
db_pool: Arc<DbPool>,
/// The AWS config
aws_config: AwsConfig,
}

impl CustodyClient {
Expand All @@ -71,9 +76,10 @@ impl CustodyClient {
fireblocks_api_secret: String,
arbitrum_rpc_url: String,
db_pool: Arc<DbPool>,
aws_config: AwsConfig,
) -> Self {
let fireblocks_api_secret = fireblocks_api_secret.as_bytes().to_vec();
Self { fireblocks_api_key, fireblocks_api_secret, arbitrum_rpc_url, db_pool }
Self { fireblocks_api_key, fireblocks_api_secret, arbitrum_rpc_url, db_pool, aws_config }
}

/// Get a fireblocks client
Expand Down
29 changes: 29 additions & 0 deletions funds-manager/funds-manager-server/src/custody_client/queries.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//! Queries for managing custody data

use diesel_async::RunQueryDsl;
use renegade_util::err_str;

use crate::db::models::HotWallet;
use crate::db::schema::hot_wallets;
use crate::error::FundsManagerError;
use crate::CustodyClient;

impl CustodyClient {
/// Insert a new hot wallet into the database
pub async fn insert_hot_wallet(
&self,
address: &str,
vault: &str,
secret_id: &str,
) -> Result<(), FundsManagerError> {
let mut conn = self.get_db_conn().await?;
let entry = HotWallet::new(secret_id.to_string(), vault.to_string(), address.to_string());
diesel::insert_into(hot_wallets::table)
.values(entry)
.execute(&mut conn)
.await
.map_err(err_str!(FundsManagerError::Db))?;

Ok(())
}
}
24 changes: 21 additions & 3 deletions funds-manager/funds-manager-server/src/db/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,33 @@ pub struct Metadata {
#[diesel(table_name = crate::db::schema::renegade_wallets)]
#[diesel(check_for_backend(diesel::pg::Pg))]
#[allow(missing_docs, clippy::missing_docs_in_private_items)]
pub struct WalletMetadata {
pub struct RenegadeWalletMetadata {
pub id: Uuid,
pub mints: Vec<Option<String>>,
pub secret_id: String,
}

impl WalletMetadata {
impl RenegadeWalletMetadata {
/// Construct a new wallet metadata entry
pub fn empty(id: Uuid, secret_id: String) -> Self {
WalletMetadata { id, mints: vec![], secret_id }
RenegadeWalletMetadata { id, mints: vec![], secret_id }
}
}

/// A hot wallet managed by the custody client
#[derive(Clone, Queryable, Selectable, Insertable)]
#[diesel(table_name = crate::db::schema::hot_wallets)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct HotWallet {
pub id: Uuid,
pub secret_id: String,
pub vault: String,
pub address: String,
}

impl HotWallet {
/// Construct a new hot wallet entry
pub fn new(secret_id: String, vault: String, address: String) -> Self {
HotWallet { id: Uuid::new_v4(), secret_id, vault, address }
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Fetch the balances of redeemed fees

use crate::custody_client::DepositWithdrawSource;
use crate::db::models::WalletMetadata;
use crate::db::models::RenegadeWalletMetadata;
use crate::error::FundsManagerError;
use arbitrum_client::{conversion::to_contract_external_transfer, helpers::serialize_calldata};
use ethers::{
Expand Down Expand Up @@ -77,7 +77,7 @@ impl Indexer {
/// 2. Use the key to fetch the wallet from the relayer
async fn fetch_wallet(
&mut self,
wallet_metadata: WalletMetadata,
wallet_metadata: RenegadeWalletMetadata,
) -> Result<ApiWallet, FundsManagerError> {
// Get the wallet's private key from secrets manager
let eth_key = self.get_wallet_private_key(&wallet_metadata).await?;
Expand Down
22 changes: 11 additions & 11 deletions funds-manager/funds-manager-server/src/fee_indexer/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use renegade_constants::MAX_BALANCES;
use tracing::warn;
use uuid::Uuid;

use crate::db::models::WalletMetadata;
use crate::db::models::RenegadeWalletMetadata;
use crate::db::models::{Metadata, NewFee};
use crate::db::schema::{
fees::dsl::{
Expand Down Expand Up @@ -222,22 +222,22 @@ impl Indexer {
pub(crate) async fn get_wallet_by_id(
&mut self,
wallet_id: &Uuid,
) -> Result<WalletMetadata, FundsManagerError> {
) -> Result<RenegadeWalletMetadata, FundsManagerError> {
let mut conn = self.get_conn().await?;
renegade_wallet_table
.filter(wallet_id_col.eq(wallet_id))
.first::<WalletMetadata>(&mut conn)
.first::<RenegadeWalletMetadata>(&mut conn)
.await
.map_err(|e| FundsManagerError::db(format!("failed to get wallet by ID: {}", e)))
}

/// Get all wallets in the table
pub(crate) async fn get_all_wallets(
&mut self,
) -> Result<Vec<WalletMetadata>, FundsManagerError> {
) -> Result<Vec<RenegadeWalletMetadata>, FundsManagerError> {
let mut conn = self.get_conn().await?;
let wallets = renegade_wallet_table
.load::<WalletMetadata>(&mut conn)
.load::<RenegadeWalletMetadata>(&mut conn)
.await
.map_err(|e| FundsManagerError::db(format!("failed to load wallets: {}", e)))?;
Ok(wallets)
Expand All @@ -247,11 +247,11 @@ impl Indexer {
pub(crate) async fn get_wallet_for_mint(
&mut self,
mint: &str,
) -> Result<Option<WalletMetadata>, FundsManagerError> {
) -> Result<Option<RenegadeWalletMetadata>, FundsManagerError> {
let mut conn = self.get_conn().await?;
let wallets: Vec<WalletMetadata> = renegade_wallet_table
let wallets: Vec<RenegadeWalletMetadata> = renegade_wallet_table
.filter(managed_mints_col.contains(vec![mint]))
.load::<WalletMetadata>(&mut conn)
.load::<RenegadeWalletMetadata>(&mut conn)
.await
.map_err(|_| FundsManagerError::db("failed to query wallet for mint"))?;

Expand All @@ -261,12 +261,12 @@ impl Indexer {
/// Find a wallet with an empty balance slot, if one exists
pub(crate) async fn find_wallet_with_empty_balance(
&mut self,
) -> Result<Option<WalletMetadata>, FundsManagerError> {
) -> Result<Option<RenegadeWalletMetadata>, FundsManagerError> {
let mut conn = self.get_conn().await?;
let n_mints = coalesce(array_length(managed_mints_col, 1 /* dim */), 0);
let wallets = renegade_wallet_table
.filter(n_mints.lt(MAX_BALANCES as i32))
.load::<WalletMetadata>(&mut conn)
.load::<RenegadeWalletMetadata>(&mut conn)
.await
.map_err(|_| FundsManagerError::db("failed to query wallets with empty balances"))?;

Expand All @@ -276,7 +276,7 @@ impl Indexer {
/// Insert a new wallet into the wallets table
pub(crate) async fn insert_wallet(
&mut self,
wallet: WalletMetadata,
wallet: RenegadeWalletMetadata,
) -> Result<(), FundsManagerError> {
let mut conn = self.get_conn().await?;
diesel::insert_into(renegade_wallet_table)
Expand Down
Loading

0 comments on commit e653139

Please sign in to comment.