diff --git a/funds-manager/funds-manager-api/Cargo.toml b/funds-manager/funds-manager-api/Cargo.toml index 51c15fa..7406965 100644 --- a/funds-manager/funds-manager-api/Cargo.toml +++ b/funds-manager/funds-manager-api/Cargo.toml @@ -8,3 +8,4 @@ edition = "2021" renegade-api = { package = "external-api", workspace = true } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.117" +uuid = "1.7.1" diff --git a/funds-manager/funds-manager-api/src/lib.rs b/funds-manager/funds-manager-api/src/lib.rs index cc3ac6b..e240385 100644 --- a/funds-manager/funds-manager-api/src/lib.rs +++ b/funds-manager/funds-manager-api/src/lib.rs @@ -4,6 +4,7 @@ use renegade_api::types::ApiWallet; use serde::{Deserialize, Serialize}; +use uuid::Uuid; // -------------- // | Api Routes | @@ -27,6 +28,9 @@ 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"; + // ------------- // | Api Types | // ------------- @@ -65,3 +69,12 @@ pub struct FeeWalletsResponse { /// The wallets managed by the funds manager pub wallets: Vec, } + +/// 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, +} 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 23bd9dd..5bc68bf 100644 --- a/funds-manager/funds-manager-server/src/custody_client/mod.rs +++ b/funds-manager/funds-manager-server/src/custody_client/mod.rs @@ -43,7 +43,7 @@ impl DepositWithdrawSource { pub(crate) fn get_vault_name(&self) -> &str { match self { Self::Quoter => "Quoters", - Self::FeeRedemption => unimplemented!("no vault for fee redemption yet"), + Self::FeeRedemption => "Fee Collection", Self::Gas => "Arbitrum Gas", } } diff --git a/funds-manager/funds-manager-server/src/fee_indexer/fee_balances.rs b/funds-manager/funds-manager-server/src/fee_indexer/fee_balances.rs index 1665619..0111554 100644 --- a/funds-manager/funds-manager-server/src/fee_indexer/fee_balances.rs +++ b/funds-manager/funds-manager-server/src/fee_indexer/fee_balances.rs @@ -1,13 +1,32 @@ //! Fetch the balances of redeemed fees +use crate::custody_client::DepositWithdrawSource; use crate::db::models::WalletMetadata; use crate::error::FundsManagerError; -use renegade_api::types::ApiWallet; -use renegade_common::types::wallet::derivation::derive_wallet_keychain; +use arbitrum_client::{conversion::to_contract_external_transfer, helpers::serialize_calldata}; +use ethers::{ + core::k256::ecdsa::SigningKey, + types::{Signature, U256}, + utils::keccak256, +}; +use num_bigint::BigUint; +use renegade_api::{http::wallet::WithdrawBalanceRequest, types::ApiWallet}; +use renegade_circuit_types::{ + keychain::SecretSigningKey, + transfers::{ExternalTransfer, ExternalTransferDirection}, + Amount, +}; +use renegade_common::types::wallet::{derivation::derive_wallet_keychain, Wallet}; +use renegade_util::hex::biguint_from_hex_string; +use uuid::Uuid; use super::Indexer; impl Indexer { + // ------------- + // | Interface | + // ------------- + /// Fetch fee balances for wallets managed by the funds manager pub async fn fetch_fee_wallets(&mut self) -> Result, FundsManagerError> { // Query the wallets and fetch from the relayer @@ -21,6 +40,36 @@ impl Indexer { Ok(wallets) } + /// Withdraw a fee balance for a specific wallet and mint + pub async fn withdraw_fee_balance( + &mut self, + wallet_id: Uuid, + mint: String, + ) -> Result<(), FundsManagerError> { + // Fetch the Renegade wallet + let wallet_metadata = self.get_wallet_by_id(&wallet_id).await?; + let api_wallet = self.fetch_wallet(wallet_metadata.clone()).await?; + let old_wallet = Wallet::try_from(api_wallet).map_err(FundsManagerError::custom)?; + let root_key = + old_wallet.key_chain.secret_keys.sk_root.as_ref().expect("root key not present"); + + // Get the deposit address for the fee withdrawal + let deposit_address = self + .custody_client + .get_deposit_address(&mint, DepositWithdrawSource::FeeRedemption) + .await?; + + // Send a withdrawal request to the relayer + let req = Self::build_withdrawal_request(&mint, &deposit_address, &old_wallet)?; + self.relayer_client.withdraw_balance(wallet_metadata.id, mint, req, root_key).await?; + + Ok(()) + } + + // ----------- + // | Helpers | + // ----------- + /// Fetch a wallet given its metadata /// /// This is done by: @@ -38,8 +87,86 @@ impl Indexer { derive_wallet_keychain(ð_key, self.chain_id).map_err(FundsManagerError::custom)?; let root_key = wallet_keychain.secret_keys.sk_root.clone().expect("root key not present"); - // Fetch the wallet from the relayer - let wallet = self.relayer_client.get_wallet(wallet_metadata.id, &root_key).await?; - Ok(wallet.wallet) + // Fetch the wallet from the relayer and replace the keychain so that we have + // access to the full set of secret keys + let mut wallet = + self.relayer_client.get_wallet(wallet_metadata.id, &root_key).await?.wallet; + wallet.key_chain = wallet_keychain.into(); + + Ok(wallet) + } + + /// Build a withdrawal request + fn build_withdrawal_request( + mint: &str, + to: &str, + old_wallet: &Wallet, + ) -> Result { + // Withdraw the balance from the wallet + let mut new_wallet = old_wallet.clone(); + let mint_bigint = biguint_from_hex_string(mint).map_err(FundsManagerError::custom)?; + let bal = new_wallet.get_balance(&mint_bigint).cloned().ok_or_else(|| { + FundsManagerError::custom(format!("No balance found for mint {mint}")) + })?; + + if bal.amount == 0 { + return Err(FundsManagerError::custom(format!("Balance for mint {mint} is 0"))); + } + new_wallet.withdraw(&mint_bigint, bal.amount).map_err(FundsManagerError::custom)?; + new_wallet.reblind_wallet(); + + // Sign the commitment to the new wallet and the transfer to the deposit address + let root_key = + old_wallet.key_chain.secret_keys.sk_root.as_ref().expect("root key not present"); + let commitment_sig = old_wallet + .sign_commitment(new_wallet.get_wallet_share_commitment()) + .expect("failed to sign wallet commitment"); + + let dest_bigint = biguint_from_hex_string(to).map_err(FundsManagerError::custom)?; + let transfer_sig = Self::authorize_withdrawal( + root_key, + mint_bigint.clone(), + bal.amount, + dest_bigint.clone(), + )?; + + Ok(WithdrawBalanceRequest { + destination_addr: dest_bigint, + amount: BigUint::from(bal.amount), + wallet_commitment_sig: commitment_sig.to_vec(), + external_transfer_sig: transfer_sig.to_vec(), + }) + } + + /// Authorize a withdrawal from the darkpool + fn authorize_withdrawal( + root_key: &SecretSigningKey, + mint: BigUint, + amount: Amount, + to: BigUint, + ) -> Result { + let converted_key: SigningKey = root_key.try_into().expect("key conversion failed"); + + // Construct a transfer + let transfer = ExternalTransfer { + mint, + amount, + direction: ExternalTransferDirection::Withdrawal, + account_addr: to, + }; + + // Sign the transfer with the root key + let contract_transfer = + to_contract_external_transfer(&transfer).map_err(FundsManagerError::custom)?; + let buf = serialize_calldata(&contract_transfer).map_err(FundsManagerError::custom)?; + let digest = keccak256(&buf); + let (sig, recovery_id) = + converted_key.sign_prehash_recoverable(&digest).map_err(FundsManagerError::custom)?; + + Ok(Signature { + r: U256::from_big_endian(&sig.r().to_bytes()), + s: U256::from_big_endian(&sig.s().to_bytes()), + v: recovery_id.to_byte() as u64, + }) } } diff --git a/funds-manager/funds-manager-server/src/fee_indexer/mod.rs b/funds-manager/funds-manager-server/src/fee_indexer/mod.rs index a447709..2f85e8a 100644 --- a/funds-manager/funds-manager-server/src/fee_indexer/mod.rs +++ b/funds-manager/funds-manager-server/src/fee_indexer/mod.rs @@ -6,6 +6,7 @@ use diesel_async::AsyncPgConnection; use renegade_circuit_types::elgamal::DecryptionKey; use renegade_util::hex::jubjub_from_hex_string; +use crate::custody_client::CustodyClient; use crate::relayer_client::RelayerClient; pub mod fee_balances; @@ -29,10 +30,13 @@ pub(crate) struct Indexer { pub db_conn: AsyncPgConnection, /// The AWS config pub aws_config: AwsConfig, + /// The custody client + pub custody_client: CustodyClient, } impl Indexer { /// Constructor + #[allow(clippy::too_many_arguments)] pub fn new( chain_id: u64, chain: Chain, @@ -41,6 +45,7 @@ impl Indexer { decryption_keys: Vec, db_conn: AsyncPgConnection, relayer_client: RelayerClient, + custody_client: CustodyClient, ) -> Self { Indexer { chain_id, @@ -50,6 +55,7 @@ impl Indexer { db_conn, relayer_client, aws_config, + custody_client, } } diff --git a/funds-manager/funds-manager-server/src/fee_indexer/queries.rs b/funds-manager/funds-manager-server/src/fee_indexer/queries.rs index 8f0bbf2..c05d80d 100644 --- a/funds-manager/funds-manager-server/src/fee_indexer/queries.rs +++ b/funds-manager/funds-manager-server/src/fee_indexer/queries.rs @@ -16,6 +16,7 @@ use diesel_async::RunQueryDsl; use renegade_common::types::wallet::WalletIdentifier; use renegade_constants::MAX_BALANCES; use tracing::warn; +use uuid::Uuid; use crate::db::models::WalletMetadata; use crate::db::models::{Metadata, NewFee}; @@ -26,7 +27,7 @@ use crate::db::schema::{ indexing_metadata::dsl::{ indexing_metadata as metadata_table, key as metadata_key, value as metadata_value, }, - wallets::dsl::{mints as managed_mints_col, wallets as wallet_table}, + wallets::dsl::{id as wallet_id_col, mints as managed_mints_col, wallets as wallet_table}, }; use crate::error::FundsManagerError; use crate::Indexer; @@ -208,6 +209,18 @@ impl Indexer { // | Wallets Table | // ----------------- + /// Get a wallet by its ID + pub(crate) async fn get_wallet_by_id( + &mut self, + wallet_id: &Uuid, + ) -> Result { + wallet_table + .filter(wallet_id_col.eq(wallet_id)) + .first::(&mut self.db_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, diff --git a/funds-manager/funds-manager-server/src/handlers.rs b/funds-manager/funds-manager-server/src/handlers.rs index 3997c6c..a90b705 100644 --- a/funds-manager/funds-manager-server/src/handlers.rs +++ b/funds-manager/funds-manager-server/src/handlers.rs @@ -5,7 +5,8 @@ use crate::error::ApiError; use crate::Server; use bytes::Bytes; use funds_manager_api::{ - DepositAddressResponse, FeeWalletsResponse, WithdrawFundsRequest, WithdrawGasRequest, + DepositAddressResponse, FeeWalletsResponse, WithdrawFeeBalanceRequest, WithdrawFundsRequest, + WithdrawGasRequest, }; use std::collections::HashMap; use std::sync::Arc; @@ -116,3 +117,17 @@ pub(crate) async fn get_fee_wallets_handler( let wallets = indexer.fetch_fee_wallets().await?; Ok(warp::reply::json(&FeeWalletsResponse { wallets })) } + +/// Handler for withdrawing a fee balance +pub(crate) async fn withdraw_fee_balance_handler( + req: WithdrawFeeBalanceRequest, + server: Arc, +) -> Result { + let mut indexer = server.build_indexer().await?; + indexer + .withdraw_fee_balance(req.wallet_id, req.mint) + .await + .map_err(|e| warp::reject::custom(ApiError::InternalError(e.to_string())))?; + + Ok(warp::reply::json(&"Fee withdrawal initiated...")) +} diff --git a/funds-manager/funds-manager-server/src/main.rs b/funds-manager/funds-manager-server/src/main.rs index 881de64..ef57c24 100644 --- a/funds-manager/funds-manager-server/src/main.rs +++ b/funds-manager/funds-manager-server/src/main.rs @@ -19,12 +19,14 @@ use error::FundsManagerError; use ethers::signers::LocalWallet; use fee_indexer::Indexer; use funds_manager_api::{ - WithdrawGasRequest, GET_DEPOSIT_ADDRESS_ROUTE, GET_FEE_WALLETS_ROUTE, INDEX_FEES_ROUTE, - PING_ROUTE, REDEEM_FEES_ROUTE, WITHDRAW_CUSTODY_ROUTE, WITHDRAW_GAS_ROUTE, + WithdrawFeeBalanceRequest, WithdrawGasRequest, GET_DEPOSIT_ADDRESS_ROUTE, + GET_FEE_WALLETS_ROUTE, INDEX_FEES_ROUTE, PING_ROUTE, REDEEM_FEES_ROUTE, WITHDRAW_CUSTODY_ROUTE, + WITHDRAW_FEE_BALANCE_ROUTE, WITHDRAW_GAS_ROUTE, }; use handlers::{ get_deposit_address_handler, get_fee_wallets_handler, index_fees_handler, - quoter_withdraw_handler, redeem_fees_handler, withdraw_gas_handler, + quoter_withdraw_handler, redeem_fees_handler, withdraw_fee_balance_handler, + withdraw_gas_handler, }; use middleware::{identity, with_hmac_auth, with_json_body}; use relayer_client::RelayerClient; @@ -193,6 +195,7 @@ impl Server { self.decryption_keys.clone(), db_conn, self.relayer_client.clone(), + self.custody_client.clone(), )) } } @@ -306,6 +309,15 @@ async fn main() -> Result<(), Box> { .and(with_server(server.clone())) .and_then(get_fee_wallets_handler); + let withdraw_fee_balance = warp::post() + .and(warp::path("fees")) + .and(warp::path(WITHDRAW_FEE_BALANCE_ROUTE)) + .and(with_hmac_auth(server.clone())) + .map(with_json_body::) + .and_then(identity) + .and(with_server(server.clone())) + .and_then(withdraw_fee_balance_handler); + let routes = ping .or(index_fees) .or(redeem_fees) @@ -313,6 +325,7 @@ async fn main() -> Result<(), Box> { .or(get_deposit_address) .or(withdraw_gas) .or(get_balances) + .or(withdraw_fee_balance) .recover(handle_rejection); warp::serve(routes).run(([0, 0, 0, 0], cli.port)).await; diff --git a/funds-manager/funds-manager-server/src/relayer_client.rs b/funds-manager/funds-manager-server/src/relayer_client.rs index 586bc49..fea4550 100644 --- a/funds-manager/funds-manager-server/src/relayer_client.rs +++ b/funds-manager/funds-manager-server/src/relayer_client.rs @@ -14,8 +14,9 @@ use renegade_api::{ task::{GetTaskStatusResponse, GET_TASK_STATUS_ROUTE}, wallet::{ CreateWalletRequest, CreateWalletResponse, FindWalletRequest, FindWalletResponse, - GetWalletResponse, RedeemNoteRequest, RedeemNoteResponse, CREATE_WALLET_ROUTE, - FIND_WALLET_ROUTE, GET_WALLET_ROUTE, REDEEM_NOTE_ROUTE, + GetWalletResponse, RedeemNoteRequest, RedeemNoteResponse, WithdrawBalanceRequest, + WithdrawBalanceResponse, CREATE_WALLET_ROUTE, FIND_WALLET_ROUTE, GET_WALLET_ROUTE, + REDEEM_NOTE_ROUTE, WITHDRAW_BALANCE_ROUTE, }, }, RENEGADE_AUTH_HEADER_NAME, RENEGADE_SIG_EXPIRATION_HEADER_NAME, @@ -162,6 +163,23 @@ impl RelayerClient { self.await_relayer_task(resp.task_id).await } + /// Withdraw a balance from a wallet + pub async fn withdraw_balance( + &self, + wallet_id: WalletIdentifier, + mint: String, + req: WithdrawBalanceRequest, + root_key: &SecretSigningKey, + ) -> Result<(), FundsManagerError> { + let mut path = WITHDRAW_BALANCE_ROUTE.to_string(); + path = path.replace(":wallet_id", &wallet_id.to_string()); + path = path.replace(":mint", &mint); + + let resp: WithdrawBalanceResponse = + self.post_relayer_with_auth(&path, &req, root_key).await?; + self.await_relayer_task(resp.task_id).await + } + // ----------- // | Helpers | // ----------- @@ -220,9 +238,11 @@ impl RelayerClient { // Deserialize the response if !resp.status().is_success() { + let status = resp.status(); + let body = resp.text().await.unwrap(); return Err(FundsManagerError::http(format!( - "Failed to send request: {}", - resp.status() + "Failed to send request: {}, {}", + status, body ))); }