From cf43283991b60b4348624243fc18c11a2d8fc1a3 Mon Sep 17 00:00:00 2001 From: Joey Kraut Date: Fri, 9 Aug 2024 11:25:06 -0700 Subject: [PATCH] funds-manager: Reorganize api types and helpers ahead of execution impl --- funds-manager/funds-manager-api/Cargo.toml | 6 + funds-manager/funds-manager-api/src/lib.rs | 202 +----------------- .../funds-manager-api/src/serialization.rs | 96 +++++++++ .../funds-manager-api/src/types/fees.rs | 38 ++++ .../funds-manager-api/src/types/gas.rs | 69 ++++++ .../src/types/hot_wallets.rs | 81 +++++++ .../funds-manager-api/src/types/mod.rs | 9 + .../funds-manager-api/src/types/quoters.rs | 109 ++++++++++ .../src/custody_client/hot_wallets.rs | 2 +- .../src/execution_client/error.rs | 4 +- .../src/execution_client/quotes.rs | 40 +--- .../src/execution_client/swap.rs | 3 +- .../funds-manager-server/src/handlers.rs | 37 +++- .../funds-manager-server/src/helpers.rs | 99 --------- .../funds-manager-server/src/main.rs | 187 +++++----------- .../funds-manager-server/src/server.rs | 142 ++++++++++++ 16 files changed, 649 insertions(+), 475 deletions(-) create mode 100644 funds-manager/funds-manager-api/src/serialization.rs create mode 100644 funds-manager/funds-manager-api/src/types/fees.rs create mode 100644 funds-manager/funds-manager-api/src/types/gas.rs create mode 100644 funds-manager/funds-manager-api/src/types/hot_wallets.rs create mode 100644 funds-manager/funds-manager-api/src/types/mod.rs create mode 100644 funds-manager/funds-manager-api/src/types/quoters.rs create mode 100644 funds-manager/funds-manager-server/src/server.rs diff --git a/funds-manager/funds-manager-api/Cargo.toml b/funds-manager/funds-manager-api/Cargo.toml index ffc6717..e227d32 100644 --- a/funds-manager/funds-manager-api/Cargo.toml +++ b/funds-manager/funds-manager-api/Cargo.toml @@ -6,6 +6,9 @@ edition = "2021" [dependencies] renegade-api = { package = "external-api", workspace = true } + +ethers = "2" +hex = "0.4.3" hmac = "0.12.1" http = "0.2.12" itertools = "0.13.0" @@ -13,3 +16,6 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha2 = "0.10.7" uuid = "1.7.1" + +[dev-dependencies] +rand = "0.8.5" diff --git a/funds-manager/funds-manager-api/src/lib.rs b/funds-manager/funds-manager-api/src/lib.rs index 74ed888..5d362cb 100644 --- a/funds-manager/funds-manager-api/src/lib.rs +++ b/funds-manager/funds-manager-api/src/lib.rs @@ -3,202 +3,6 @@ #![deny(clippy::missing_docs_in_private_items)] pub mod auth; - -use renegade_api::types::ApiWallet; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -// -------------- -// | Api Routes | -// -------------- - -/// The ping route -pub const PING_ROUTE: &str = "ping"; -/// The route through which a client may start the fee indexing process -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"; - -/// The route to withdraw gas from custody -pub const WITHDRAW_GAS_ROUTE: &str = "withdraw-gas"; -/// The route to refill gas for all active wallets -pub const REFILL_GAS_ROUTE: &str = "refill-gas"; -/// The route to register a gas wallet for a peer -pub const REGISTER_GAS_WALLET_ROUTE: &str = "register-gas-wallet"; -/// The route to report active peers -pub const REPORT_ACTIVE_PEERS_ROUTE: &str = "report-active-peers"; - -/// 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 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 | -// ------------- - -// --- 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, -} - -/// 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 { - /// The deposit address - pub address: String, -} - -/// The request body for withdrawing funds from custody -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct WithdrawFundsRequest { - /// The mint of the asset to withdraw - pub mint: String, - /// The amount of funds to withdraw - pub amount: f64, - /// The address to withdraw to - pub address: String, -} - -// --- Gas --- // - -/// The request body for withdrawing gas from custody -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct WithdrawGasRequest { - /// The amount of gas to withdraw - pub amount: f64, - /// The address to withdraw to - pub destination_address: String, -} - -/// The request body for refilling gas for all active wallets -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct RefillGasRequest { - /// The amount of gas to top up each wallet to - pub amount: f64, -} - -/// The response containing the gas wallet's address -#[derive(Debug, Serialize, Deserialize)] -pub struct CreateGasWalletResponse { - /// The address of the gas wallet - pub address: String, -} - -/// A request to allocate a gas wallet for a peer -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct RegisterGasWalletRequest { - /// The peer ID of the peer to allocate a gas wallet for - pub peer_id: String, -} - -/// The response containing an newly active gas wallet's key -/// -/// Clients will hit the corresponding endpoint to register a gas wallet with -/// the funds manager when they spin up -#[derive(Debug, Serialize, Deserialize)] -pub struct RegisterGasWalletResponse { - /// The key of the active gas wallet - pub key: String, -} - -/// A request reporting active peers in the network -/// -/// The funds manager uses such a request to mark gas wallets as active or -/// inactive -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct ReportActivePeersRequest { - /// The list of active peers - pub peers: Vec, -} - -// --- Hot Wallets --- // - -/// The request body for creating a hot wallet -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct CreateHotWalletRequest { - /// The name of the vault backing the hot wallet - pub vault: String, - /// The internal wallet ID to associate with the hot wallet - pub internal_wallet_id: Uuid, -} - -/// The response containing the hot wallet's address -#[derive(Debug, Serialize, Deserialize)] -pub struct CreateHotWalletResponse { - /// The address of the hot wallet - pub address: String, -} - -/// The response containing hot wallet balances -#[derive(Debug, Serialize, Deserialize)] -pub struct HotWalletBalancesResponse { - /// The list of hot wallets with their balances - pub wallets: Vec, -} - -/// A hot wallet with its balances -#[derive(Debug, Serialize, Deserialize)] -pub struct WalletWithBalances { - /// The address of the hot wallet - pub address: String, - /// The balances of various tokens - pub balances: Vec, -} - -/// A balance for a specific token -#[derive(Debug, Serialize, Deserialize)] -pub struct TokenBalance { - /// The mint address of the token - pub mint: String, - /// The balance amount - pub amount: u128, -} - -/// The request body for transferring funds from a hot wallet to its backing -/// vault -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct TransferToVaultRequest { - /// The address of the hot wallet - pub hot_wallet_address: String, - /// The mint of the asset to transfer - pub mint: String, - /// 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, -} +mod serialization; +mod types; +pub use types::*; diff --git a/funds-manager/funds-manager-api/src/serialization.rs b/funds-manager/funds-manager-api/src/serialization.rs new file mode 100644 index 0000000..e05c0af --- /dev/null +++ b/funds-manager/funds-manager-api/src/serialization.rs @@ -0,0 +1,96 @@ +//! Serialization helpers for the funds manager API + +/// A module for serializing and deserializing addresses as strings +pub(crate) mod address_string_serialization { + use std::str::FromStr; + + use ethers::types::Address; + use serde::{de::Error, Deserialize, Deserializer, Serializer}; + + /// Serialize an address to a string + pub fn serialize(address: &Address, s: S) -> Result { + s.serialize_str(&address.to_string()) + } + + /// Deserialize a string to an address + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { + let s = String::deserialize(d)?; + Address::from_str(&s).map_err(|_| D::Error::custom("Invalid address")) + } +} + +/// A module for serializing and deserializing U256 as strings +pub(crate) mod u256_string_serialization { + use ethers::types::U256; + use serde::{de::Error, Deserialize, Deserializer, Serializer}; + + /// Serialize a U256 to a string + pub fn serialize(value: &U256, s: S) -> Result { + s.serialize_str(&value.to_string()) + } + + /// Deserialize a string to a U256 + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { + let s = String::deserialize(d)?; + U256::from_dec_str(&s).map_err(|_| D::Error::custom("Invalid U256 value")) + } +} + +/// A module for serializing and deserializing bytes from a hex string +pub(crate) mod bytes_string_serialization { + use ethers::types::Bytes; + use hex::FromHex; + use serde::{de::Error, Deserialize, Deserializer, Serializer}; + + /// Serialize bytes to a hex string + pub fn serialize(value: &Bytes, s: S) -> Result { + let hex = format!("{value:#x}"); + s.serialize_str(&hex) + } + + /// Deserialize a hex string to bytes + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { + let s = String::deserialize(d)?; + Bytes::from_hex(s).map_err(|_| D::Error::custom("Invalid bytes value")) + } +} + +#[cfg(test)] +mod tests { + use ethers::types::{Address, Bytes, U256}; + use rand::{thread_rng, Rng}; + + /// Test serialization and deserialization of an address + #[test] + fn test_address_serialization() { + let addr = Address::random(); + let serialized = serde_json::to_string(&addr).unwrap(); + let deserialized: Address = serde_json::from_str(&serialized).unwrap(); + assert_eq!(addr, deserialized); + } + + /// Test serialization and deserialization of a U256 + #[test] + fn test_u256_serialization() { + let mut rng = thread_rng(); + let mut bytes = [0u8; 32]; + rng.fill(&mut bytes); + let value = U256::from(bytes); + + let serialized = serde_json::to_string(&value).unwrap(); + let deserialized: U256 = serde_json::from_str(&serialized).unwrap(); + assert_eq!(value, deserialized); + } + + /// Test serialization and deserialization of bytes + #[test] + fn test_bytes_serialization() { + const N: usize = 32; + let mut rng = thread_rng(); + let bytes: Bytes = (0..N).map(|_| rng.gen_range(0..=u8::MAX)).collect(); + + let serialized = serde_json::to_string(&bytes).unwrap(); + let deserialized: Bytes = serde_json::from_str(&serialized).unwrap(); + assert_eq!(bytes, deserialized); + } +} diff --git a/funds-manager/funds-manager-api/src/types/fees.rs b/funds-manager/funds-manager-api/src/types/fees.rs new file mode 100644 index 0000000..c0d38da --- /dev/null +++ b/funds-manager/funds-manager-api/src/types/fees.rs @@ -0,0 +1,38 @@ +//! API types for managing and redeeming fees + +use renegade_api::types::ApiWallet; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +// -------------- +// | Api Routes | +// -------------- + +/// The route through which a client may start the fee indexing process +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 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 | +// ------------- + +/// The response containing fee wallets +#[derive(Debug, Serialize, Deserialize)] +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-api/src/types/gas.rs b/funds-manager/funds-manager-api/src/types/gas.rs new file mode 100644 index 0000000..a93c7e5 --- /dev/null +++ b/funds-manager/funds-manager-api/src/types/gas.rs @@ -0,0 +1,69 @@ +//! API types for gas funding and gas wallet tracking +use serde::{Deserialize, Serialize}; + +// -------------- +// | Api Routes | +// -------------- + +/// The route to withdraw gas from custody +pub const WITHDRAW_GAS_ROUTE: &str = "withdraw-gas"; +/// The route to refill gas for all active wallets +pub const REFILL_GAS_ROUTE: &str = "refill-gas"; +/// The route to register a gas wallet for a peer +pub const REGISTER_GAS_WALLET_ROUTE: &str = "register-gas-wallet"; +/// The route to report active peers +pub const REPORT_ACTIVE_PEERS_ROUTE: &str = "report-active-peers"; + +// ------------- +// | Api Types | +// ------------- + +/// The request body for withdrawing gas from custody +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct WithdrawGasRequest { + /// The amount of gas to withdraw + pub amount: f64, + /// The address to withdraw to + pub destination_address: String, +} + +/// The request body for refilling gas for all active wallets +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RefillGasRequest { + /// The amount of gas to top up each wallet to + pub amount: f64, +} + +/// The response containing the gas wallet's address +#[derive(Debug, Serialize, Deserialize)] +pub struct CreateGasWalletResponse { + /// The address of the gas wallet + pub address: String, +} + +/// A request to allocate a gas wallet for a peer +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RegisterGasWalletRequest { + /// The peer ID of the peer to allocate a gas wallet for + pub peer_id: String, +} + +/// The response containing an newly active gas wallet's key +/// +/// Clients will hit the corresponding endpoint to register a gas wallet with +/// the funds manager when they spin up +#[derive(Debug, Serialize, Deserialize)] +pub struct RegisterGasWalletResponse { + /// The key of the active gas wallet + pub key: String, +} + +/// A request reporting active peers in the network +/// +/// The funds manager uses such a request to mark gas wallets as active or +/// inactive +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ReportActivePeersRequest { + /// The list of active peers + pub peers: Vec, +} diff --git a/funds-manager/funds-manager-api/src/types/hot_wallets.rs b/funds-manager/funds-manager-api/src/types/hot_wallets.rs new file mode 100644 index 0000000..3deb92b --- /dev/null +++ b/funds-manager/funds-manager-api/src/types/hot_wallets.rs @@ -0,0 +1,81 @@ +//! API types for managing hot wallets + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +// -------------- +// | Api Routes | +// -------------- + +/// 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 | +// ------------- + +/// The request body for creating a hot wallet +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CreateHotWalletRequest { + /// The name of the vault backing the hot wallet + pub vault: String, + /// The internal wallet ID to associate with the hot wallet + pub internal_wallet_id: Uuid, +} + +/// The response containing the hot wallet's address +#[derive(Debug, Serialize, Deserialize)] +pub struct CreateHotWalletResponse { + /// The address of the hot wallet + pub address: String, +} + +/// The response containing hot wallet balances +#[derive(Debug, Serialize, Deserialize)] +pub struct HotWalletBalancesResponse { + /// The list of hot wallets with their balances + pub wallets: Vec, +} + +/// A hot wallet with its balances +#[derive(Debug, Serialize, Deserialize)] +pub struct WalletWithBalances { + /// The address of the hot wallet + pub address: String, + /// The balances of various tokens + pub balances: Vec, +} + +/// A balance for a specific token +#[derive(Debug, Serialize, Deserialize)] +pub struct TokenBalance { + /// The mint address of the token + pub mint: String, + /// The balance amount + pub amount: u128, +} + +/// The request body for transferring funds from a hot wallet to its backing +/// vault +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TransferToVaultRequest { + /// The address of the hot wallet + pub hot_wallet_address: String, + /// The mint of the asset to transfer + pub mint: String, + /// 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, +} diff --git a/funds-manager/funds-manager-api/src/types/mod.rs b/funds-manager/funds-manager-api/src/types/mod.rs new file mode 100644 index 0000000..32f6405 --- /dev/null +++ b/funds-manager/funds-manager-api/src/types/mod.rs @@ -0,0 +1,9 @@ +//! API types for the funds manager + +pub mod fees; +pub mod gas; +pub mod hot_wallets; +pub mod quoters; + +/// The ping route +pub const PING_ROUTE: &str = "ping"; diff --git a/funds-manager/funds-manager-api/src/types/quoters.rs b/funds-manager/funds-manager-api/src/types/quoters.rs new file mode 100644 index 0000000..3d8e61d --- /dev/null +++ b/funds-manager/funds-manager-api/src/types/quoters.rs @@ -0,0 +1,109 @@ +//! API types for quoter management +use ethers::types::{Address, Bytes, U256}; +use serde::{Deserialize, Serialize}; + +use crate::serialization::{ + address_string_serialization, bytes_string_serialization, u256_string_serialization, +}; + +// -------------- +// | Api Routes | +// -------------- + +/// 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"; +/// The route to fetch an execution quote on the quoter hot wallet +pub const GET_EXECUTION_QUOTE_ROUTE: &str = "get-execution-quote"; +/// The route to execute a swap on the quoter hot wallet +pub const EXECUTE_SWAP_ROUTE: &str = "execute-swap"; + +// ------------- +// | 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(Clone, Debug, Serialize, Deserialize)] +pub struct WithdrawFundsRequest { + /// The mint of the asset to withdraw + pub mint: String, + /// The amount of funds to withdraw + pub amount: f64, + /// The address to withdraw to + pub address: String, +} + +// --- Execution --- // + +/// The subset of the quote response forwarded to consumers of this client +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExecutionQuote { + /// The token address we're buying + #[serde(with = "address_string_serialization")] + pub buy_token_address: Address, + /// The token address we're selling + #[serde(with = "address_string_serialization")] + pub sell_token_address: Address, + /// The amount of tokens to sell + #[serde(with = "u256_string_serialization")] + pub sell_amount: U256, + /// The quoted price + pub price: String, + /// The submitting address + #[serde(with = "address_string_serialization")] + pub from: Address, + /// The 0x swap contract address + #[serde(with = "address_string_serialization")] + pub to: Address, + /// The calldata for the swap + #[serde(with = "bytes_string_serialization")] + pub data: Bytes, + /// The value of the tx; should be zero + #[serde(with = "u256_string_serialization")] + pub value: U256, + /// The gas price used in the swap + #[serde(with = "u256_string_serialization")] + pub gas_price: U256, +} + +/// The request body for fetching a quote from the execution venue +#[derive(Debug, Serialize, Deserialize)] +pub struct GetExecutionQuoteRequest { + /// The token address we're buying + pub buy_token_address: String, + /// The token address we're selling + pub sell_token_address: String, + /// The amount of tokens to sell + pub sell_amount: u128, +} + +/// The response body for fetching a quote from the execution venue +#[derive(Debug, Serialize, Deserialize)] +pub struct GetExecutionQuoteResponse { + /// The quote, directly from the execution venue + pub quote: ExecutionQuote, +} + +/// The request body for executing a swap on the execution venue +#[derive(Debug, Serialize, Deserialize)] +pub struct ExecuteSwapRequest { + /// The quote, implicitly accepted by the caller by its presence in this + /// request + pub quote: ExecutionQuote, +} + +/// The response body for executing a swap on the execution venue +#[derive(Debug, Serialize, Deserialize)] +pub struct ExecuteSwapResponse { + /// The tx hash of the swap + pub tx_hash: String, +} diff --git a/funds-manager/funds-manager-server/src/custody_client/hot_wallets.rs b/funds-manager/funds-manager-server/src/custody_client/hot_wallets.rs index ba9158e..cac5096 100644 --- a/funds-manager/funds-manager-server/src/custody_client/hot_wallets.rs +++ b/funds-manager/funds-manager-server/src/custody_client/hot_wallets.rs @@ -11,7 +11,7 @@ use ethers::{ types::Address, utils::hex::ToHexExt, }; -use funds_manager_api::{TokenBalance, WalletWithBalances}; +use funds_manager_api::hot_wallets::{TokenBalance, WalletWithBalances}; use rand::thread_rng; use tracing::info; use uuid::Uuid; diff --git a/funds-manager/funds-manager-server/src/execution_client/error.rs b/funds-manager/funds-manager-server/src/execution_client/error.rs index 47f4f4d..8bf8490 100644 --- a/funds-manager/funds-manager-server/src/execution_client/error.rs +++ b/funds-manager/funds-manager-server/src/execution_client/error.rs @@ -1,6 +1,6 @@ //! Error types for the execution client -use std::fmt::Display; +use std::{error::Error, fmt::Display}; /// An error returned by the execution client #[derive(Debug, Clone)] @@ -45,6 +45,8 @@ impl Display for ExecutionClientError { } } +impl Error for ExecutionClientError {} + impl From for ExecutionClientError { fn from(e: reqwest::Error) -> Self { ExecutionClientError::http(e) diff --git a/funds-manager/funds-manager-server/src/execution_client/quotes.rs b/funds-manager/funds-manager-server/src/execution_client/quotes.rs index fe77625..550062a 100644 --- a/funds-manager/funds-manager-server/src/execution_client/quotes.rs +++ b/funds-manager/funds-manager-server/src/execution_client/quotes.rs @@ -1,11 +1,7 @@ //! Client methods for fetching quotes and prices from the execution venue -use ethers::types::{Address, Bytes, U256}; -use serde::{Deserialize, Serialize}; - -use crate::helpers::{ - address_string_serialization, bytes_string_serialization, u256_string_serialization, -}; +use funds_manager_api::quoters::ExecutionQuote; +use serde::Deserialize; use super::{error::ExecutionClientError, ExecutionClient}; @@ -30,38 +26,6 @@ pub struct PriceResponse { pub price: String, } -/// The subset of the quote response forwarded to consumers of this client -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ExecutionQuote { - /// The token address we're buying - #[serde(with = "address_string_serialization")] - pub buy_token_address: Address, - /// The token address we're selling - #[serde(with = "address_string_serialization")] - pub sell_token_address: Address, - /// The amount of tokens to sell - #[serde(with = "u256_string_serialization")] - pub sell_amount: U256, - /// The quoted price - pub price: String, - /// The submitting address - #[serde(with = "address_string_serialization")] - pub from: Address, - /// The 0x swap contract address - #[serde(with = "address_string_serialization")] - pub to: Address, - /// The calldata for the swap - #[serde(with = "bytes_string_serialization")] - pub data: Bytes, - /// The value of the tx; should be zero - #[serde(with = "u256_string_serialization")] - pub value: U256, - /// The gas price used in the swap - #[serde(with = "u256_string_serialization")] - pub gas_price: U256, -} - impl ExecutionClient { /// Fetch a price for an asset pub async fn get_price( diff --git a/funds-manager/funds-manager-server/src/execution_client/swap.rs b/funds-manager/funds-manager-server/src/execution_client/swap.rs index 6ef3a73..0dc683c 100644 --- a/funds-manager/funds-manager-server/src/execution_client/swap.rs +++ b/funds-manager/funds-manager-server/src/execution_client/swap.rs @@ -7,11 +7,12 @@ use ethers::{ signers::LocalWallet, types::{Address, Eip1559TransactionRequest, TransactionReceipt, U256}, }; +use funds_manager_api::quoters::ExecutionQuote; use tracing::info; use crate::helpers::ERC20; -use super::{error::ExecutionClientError, quotes::ExecutionQuote, ExecutionClient}; +use super::{error::ExecutionClientError, ExecutionClient}; impl ExecutionClient { /// Execute a quoted swap diff --git a/funds-manager/funds-manager-server/src/handlers.rs b/funds-manager/funds-manager-server/src/handlers.rs index 1e23fb9..3cc9e39 100644 --- a/funds-manager/funds-manager-server/src/handlers.rs +++ b/funds-manager/funds-manager-server/src/handlers.rs @@ -4,12 +4,17 @@ use crate::custody_client::DepositWithdrawSource; use crate::error::ApiError; use crate::Server; use bytes::Bytes; -use funds_manager_api::{ - CreateGasWalletResponse, CreateHotWalletRequest, CreateHotWalletResponse, - DepositAddressResponse, FeeWalletsResponse, HotWalletBalancesResponse, RefillGasRequest, - RegisterGasWalletRequest, RegisterGasWalletResponse, ReportActivePeersRequest, - TransferToVaultRequest, WithdrawFeeBalanceRequest, WithdrawFundsRequest, WithdrawGasRequest, - WithdrawToHotWalletRequest, +use funds_manager_api::fees::{FeeWalletsResponse, WithdrawFeeBalanceRequest}; +use funds_manager_api::gas::{ + CreateGasWalletResponse, RefillGasRequest, RegisterGasWalletRequest, RegisterGasWalletResponse, + ReportActivePeersRequest, WithdrawGasRequest, +}; +use funds_manager_api::hot_wallets::{ + CreateHotWalletRequest, CreateHotWalletResponse, HotWalletBalancesResponse, + TransferToVaultRequest, WithdrawToHotWalletRequest, +}; +use funds_manager_api::quoters::{ + DepositAddressResponse, ExecuteSwapRequest, GetExecutionQuoteRequest, WithdrawFundsRequest, }; use itertools::Itertools; use serde_json::json; @@ -110,6 +115,26 @@ pub(crate) async fn get_deposit_address_handler( Ok(warp::reply::json(&resp)) } +/// Handler for getting an execution quote +pub(crate) async fn get_execution_quote_handler( + _quote_request: GetExecutionQuoteRequest, + _server: Arc, +) -> Result { + // TODO: Implement this handler + println!("Getting execution quote"); + Ok(warp::reply::json(&"Quote fetched")) +} + +/// Handler for executing a swap +pub(crate) async fn execute_swap_handler( + _swap_request: ExecuteSwapRequest, + _server: Arc, +) -> Result { + // TODO: Implement this handler + println!("Executing swap"); + Ok(warp::reply::json(&"Swap executed")) +} + // --- Gas --- // /// Handler for withdrawing gas from custody diff --git a/funds-manager/funds-manager-server/src/helpers.rs b/funds-manager/funds-manager-server/src/helpers.rs index 6561993..51d8116 100644 --- a/funds-manager/funds-manager-server/src/helpers.rs +++ b/funds-manager/funds-manager-server/src/helpers.rs @@ -79,102 +79,3 @@ pub async fn create_secrets_manager_entry_with_description( Ok(()) } - -// ----------------- -// | Serialization | -// ----------------- - -/// A module for serializing and deserializing addresses as strings -pub(crate) mod address_string_serialization { - use std::str::FromStr; - - use ethers::types::Address; - use serde::{de::Error, Deserialize, Deserializer, Serializer}; - - /// Serialize an address to a string - pub fn serialize(address: &Address, s: S) -> Result { - s.serialize_str(&address.to_string()) - } - - /// Deserialize a string to an address - pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { - let s = String::deserialize(d)?; - Address::from_str(&s).map_err(|_| D::Error::custom("Invalid address")) - } -} - -/// A module for serializing and deserializing U256 as strings -pub(crate) mod u256_string_serialization { - use ethers::types::U256; - use serde::{de::Error, Deserialize, Deserializer, Serializer}; - - /// Serialize a U256 to a string - pub fn serialize(value: &U256, s: S) -> Result { - s.serialize_str(&value.to_string()) - } - - /// Deserialize a string to a U256 - pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { - let s = String::deserialize(d)?; - U256::from_dec_str(&s).map_err(|_| D::Error::custom("Invalid U256 value")) - } -} - -/// A module for serializing and deserializing bytes from a hex string -pub(crate) mod bytes_string_serialization { - use ethers::types::Bytes; - use hex::FromHex; - use serde::{de::Error, Deserialize, Deserializer, Serializer}; - - /// Serialize bytes to a hex string - pub fn serialize(value: &Bytes, s: S) -> Result { - let hex = format!("{value:#x}"); - s.serialize_str(&hex) - } - - /// Deserialize a hex string to bytes - pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { - let s = String::deserialize(d)?; - Bytes::from_hex(s).map_err(|_| D::Error::custom("Invalid bytes value")) - } -} - -#[cfg(test)] -mod tests { - use ethers::types::{Address, Bytes, U256}; - use rand::{thread_rng, Rng}; - - /// Test serialization and deserialization of an address - #[test] - fn test_address_serialization() { - let addr = Address::random(); - let serialized = serde_json::to_string(&addr).unwrap(); - let deserialized: Address = serde_json::from_str(&serialized).unwrap(); - assert_eq!(addr, deserialized); - } - - /// Test serialization and deserialization of a U256 - #[test] - fn test_u256_serialization() { - let mut rng = thread_rng(); - let mut bytes = [0u8; 32]; - rng.fill(&mut bytes); - let value = U256::from(bytes); - - let serialized = serde_json::to_string(&value).unwrap(); - let deserialized: U256 = serde_json::from_str(&serialized).unwrap(); - assert_eq!(value, deserialized); - } - - /// Test serialization and deserialization of bytes - #[test] - fn test_bytes_serialization() { - const N: usize = 32; - let mut rng = thread_rng(); - let bytes: Bytes = (0..N).map(|_| rng.gen_range(0..=u8::MAX)).collect(); - - let serialized = serde_json::to_string(&bytes).unwrap(); - let deserialized: Bytes = serde_json::from_str(&serialized).unwrap(); - assert_eq!(bytes, deserialized); - } -} diff --git a/funds-manager/funds-manager-server/src/main.rs b/funds-manager/funds-manager-server/src/main.rs index b71749b..b286e89 100644 --- a/funds-manager/funds-manager-server/src/main.rs +++ b/funds-manager/funds-manager-server/src/main.rs @@ -15,61 +15,48 @@ pub mod handlers; pub mod helpers; pub mod middleware; pub mod relayer_client; +pub mod server; -use aws_config::{BehaviorVersion, Region, SdkConfig}; -use db::{create_db_pool, DbPool}; -use error::FundsManagerError; -use ethers::signers::LocalWallet; use fee_indexer::Indexer; -use funds_manager_api::{ - CreateHotWalletRequest, RefillGasRequest, RegisterGasWalletRequest, ReportActivePeersRequest, - TransferToVaultRequest, WithdrawFeeBalanceRequest, WithdrawGasRequest, - WithdrawToHotWalletRequest, GET_DEPOSIT_ADDRESS_ROUTE, GET_FEE_WALLETS_ROUTE, INDEX_FEES_ROUTE, - PING_ROUTE, REDEEM_FEES_ROUTE, REFILL_GAS_ROUTE, REGISTER_GAS_WALLET_ROUTE, - REPORT_ACTIVE_PEERS_ROUTE, TRANSFER_TO_VAULT_ROUTE, WITHDRAW_CUSTODY_ROUTE, - WITHDRAW_FEE_BALANCE_ROUTE, WITHDRAW_GAS_ROUTE, WITHDRAW_TO_HOT_WALLET_ROUTE, +use funds_manager_api::fees::{ + WithdrawFeeBalanceRequest, GET_FEE_WALLETS_ROUTE, INDEX_FEES_ROUTE, REDEEM_FEES_ROUTE, + WITHDRAW_FEE_BALANCE_ROUTE, }; +use funds_manager_api::gas::{ + RefillGasRequest, RegisterGasWalletRequest, ReportActivePeersRequest, WithdrawGasRequest, + REFILL_GAS_ROUTE, REGISTER_GAS_WALLET_ROUTE, REPORT_ACTIVE_PEERS_ROUTE, WITHDRAW_GAS_ROUTE, +}; +use funds_manager_api::hot_wallets::{ + CreateHotWalletRequest, TransferToVaultRequest, WithdrawToHotWalletRequest, + TRANSFER_TO_VAULT_ROUTE, WITHDRAW_TO_HOT_WALLET_ROUTE, +}; +use funds_manager_api::quoters::{ + ExecuteSwapRequest, GetExecutionQuoteRequest, WithdrawFundsRequest, EXECUTE_SWAP_ROUTE, + GET_DEPOSIT_ADDRESS_ROUTE, GET_EXECUTION_QUOTE_ROUTE, WITHDRAW_CUSTODY_ROUTE, +}; +use funds_manager_api::PING_ROUTE; use handlers::{ - create_gas_wallet_handler, 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, refill_gas_handler, register_gas_wallet_handler, + create_gas_wallet_handler, create_hot_wallet_handler, execute_swap_handler, + get_deposit_address_handler, get_execution_quote_handler, get_fee_wallets_handler, + get_hot_wallet_balances_handler, index_fees_handler, quoter_withdraw_handler, + redeem_fees_handler, refill_gas_handler, register_gas_wallet_handler, report_active_peers_handler, transfer_to_vault_handler, withdraw_fee_balance_handler, withdraw_from_vault_handler, withdraw_gas_handler, }; use middleware::{identity, with_hmac_auth, with_json_body}; -use relayer_client::RelayerClient; -use renegade_circuit_types::elgamal::DecryptionKey; -use renegade_util::{raw_err_str, telemetry::configure_telemetry}; +use renegade_util::telemetry::configure_telemetry; +use server::Server; +use warp::Filter; -use std::{collections::HashMap, error::Error, str::FromStr, sync::Arc}; +use std::{collections::HashMap, error::Error, sync::Arc}; -use arbitrum_client::{ - client::{ArbitrumClient, ArbitrumClientConfig}, - constants::Chain, -}; +use arbitrum_client::constants::Chain; use clap::Parser; -use funds_manager_api::WithdrawFundsRequest; use tracing::{error, warn}; -use warp::Filter; use crate::custody_client::CustodyClient; use crate::error::ApiError; -// ------------- -// | Constants | -// ------------- - -/// The block polling interval for the Arbitrum client -const BLOCK_POLLING_INTERVAL_MS: u64 = 100; -/// The default region in which to provision secrets manager secrets -const DEFAULT_REGION: &str = "us-east-2"; -/// The dummy private key used to instantiate the arbitrum client -/// -/// We don't need any client functionality using a real private key, so instead -/// we use the key deployed by Arbitrum on local devnets -const DUMMY_PRIVATE_KEY: &str = - "0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659"; - // ------- // | Cli | // ------- @@ -130,6 +117,12 @@ struct Cli { /// The fireblocks api secret #[clap(long, env = "FIREBLOCKS_API_SECRET")] fireblocks_api_secret: String, + /// The execution venue api key + #[clap(long, env = "EXECUTION_VENUE_API_KEY")] + execution_venue_api_key: String, + /// The execution venue base url + #[clap(long, env = "EXECUTION_VENUE_BASE_URL")] + execution_venue_base_url: String, // --- Server Config --- // @@ -165,45 +158,6 @@ impl Cli { } } -/// The server -#[derive(Clone)] -struct Server { - /// The id of the chain this indexer targets - pub chain_id: u64, - /// The chain this indexer targets - pub chain: Chain, - /// A client for interacting with the relayer - pub relayer_client: RelayerClient, - /// The Arbitrum client - pub arbitrum_client: ArbitrumClient, - /// The decryption key - pub decryption_keys: Vec, - /// The database connection pool - pub db_pool: Arc, - /// The custody client - pub custody_client: CustodyClient, - /// The AWS config - pub aws_config: SdkConfig, - /// The HMAC key for custody endpoint authentication - pub hmac_key: Option<[u8; 32]>, -} - -impl Server { - /// Build an indexer - pub fn build_indexer(&self) -> Result { - Ok(Indexer::new( - self.chain_id, - self.chain, - self.aws_config.clone(), - self.arbitrum_client.clone(), - self.decryption_keys.clone(), - self.db_pool.clone(), - self.relayer_client.clone(), - self.custody_client.clone(), - )) - } -} - #[tokio::main] async fn main() -> Result<(), Box> { let cli = Cli::parse(); @@ -222,57 +176,8 @@ async fn main() -> Result<(), Box> { ) .expect("failed to setup telemetry"); - // Parse an AWS config - let config = aws_config::defaults(BehaviorVersion::latest()) - .region(Region::new(DEFAULT_REGION)) - .load() - .await; - - // Build an Arbitrum client - let wallet = LocalWallet::from_str(DUMMY_PRIVATE_KEY)?; - let conf = ArbitrumClientConfig { - darkpool_addr: cli.darkpool_address.clone(), - chain: cli.chain, - rpc_url: cli.rpc_url.clone(), - arb_priv_keys: vec![wallet], - block_polling_interval_ms: BLOCK_POLLING_INTERVAL_MS, - }; - let client = ArbitrumClient::new(conf).await?; - let chain_id = client.chain_id().await.map_err(raw_err_str!("Error fetching chain ID: {}"))?; - - // Build the indexer - let mut decryption_keys = vec![DecryptionKey::from_hex_str(&cli.relayer_decryption_key)?]; - if let Some(protocol_key) = &cli.protocol_decryption_key { - decryption_keys.push(DecryptionKey::from_hex_str(protocol_key)?); - } - - let hmac_key = cli.get_hmac_key(); - let relayer_client = RelayerClient::new(&cli.relayer_url, &cli.usdc_mint); - - // Create a database connection pool using bb8 - let db_pool = create_db_pool(&cli.db_url).await?; - let arc_pool = Arc::new(db_pool); - - let custody_client = CustodyClient::new( - chain_id, - cli.fireblocks_api_key, - cli.fireblocks_api_secret, - cli.rpc_url, - arc_pool.clone(), - config.clone(), - ); - - let server = Server { - chain_id, - chain: cli.chain, - relayer_client: relayer_client.clone(), - arbitrum_client: client.clone(), - decryption_keys, - db_pool: arc_pool, - custody_client, - aws_config: config, - hmac_key, - }; + let port = cli.port; // copy `cli.port` to use after moving `cli` + let server = Server::build_from_cli(cli).await.expect("failed to build server"); // ---------- // | Routes | @@ -332,6 +237,26 @@ async fn main() -> Result<(), Box> { .and(with_server(server.clone())) .and_then(get_deposit_address_handler); + let get_execution_quote = warp::post() + .and(warp::path("custody")) + .and(warp::path("quoters")) + .and(warp::path(GET_EXECUTION_QUOTE_ROUTE)) + .and(with_hmac_auth(server.clone())) + .map(with_json_body::) + .and_then(identity) + .and(with_server(server.clone())) + .and_then(get_execution_quote_handler); + + let execute_swap = warp::post() + .and(warp::path("custody")) + .and(warp::path("quoters")) + .and(warp::path(EXECUTE_SWAP_ROUTE)) + .and(with_hmac_auth(server.clone())) + .map(with_json_body::) + .and_then(identity) + .and(with_server(server.clone())) + .and_then(execute_swap_handler); + // --- Gas --- // let withdraw_gas = warp::post() @@ -425,6 +350,8 @@ async fn main() -> Result<(), Box> { .or(redeem_fees) .or(withdraw_custody) .or(get_deposit_address) + .or(get_execution_quote) + .or(execute_swap) .or(withdraw_gas) .or(refill_gas) .or(report_active_peers) @@ -437,7 +364,7 @@ async fn main() -> Result<(), Box> { .or(get_hot_wallet_balances) .or(create_hot_wallet) .recover(handle_rejection); - warp::serve(routes).run(([0, 0, 0, 0], cli.port)).await; + warp::serve(routes).run(([0, 0, 0, 0], port)).await; Ok(()) } diff --git a/funds-manager/funds-manager-server/src/server.rs b/funds-manager/funds-manager-server/src/server.rs new file mode 100644 index 0000000..1156ef2 --- /dev/null +++ b/funds-manager/funds-manager-server/src/server.rs @@ -0,0 +1,142 @@ +//! Defines the server which encapsulates all dependencies for funds manager +//! execution + +use std::{error::Error, str::FromStr, sync::Arc}; + +use arbitrum_client::{ + client::{ArbitrumClient, ArbitrumClientConfig}, + constants::Chain, +}; +use aws_config::{BehaviorVersion, Region, SdkConfig}; +use ethers::signers::LocalWallet; +use renegade_circuit_types::elgamal::DecryptionKey; +use renegade_util::raw_err_str; + +use crate::{ + custody_client::CustodyClient, + db::{create_db_pool, DbPool}, + error::FundsManagerError, + execution_client::ExecutionClient, + fee_indexer::Indexer, + relayer_client::RelayerClient, + Cli, +}; + +// ------------- +// | Constants | +// ------------- + +/// The block polling interval for the Arbitrum client +const BLOCK_POLLING_INTERVAL_MS: u64 = 100; +/// The default region in which to provision secrets manager secrets +const DEFAULT_REGION: &str = "us-east-2"; +/// The dummy private key used to instantiate the arbitrum client +/// +/// We don't need any client functionality using a real private key, so instead +/// we use the key deployed by Arbitrum on local devnets +const DUMMY_PRIVATE_KEY: &str = + "0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659"; + +/// The server +#[derive(Clone)] +pub(crate) struct Server { + /// The id of the chain this indexer targets + pub chain_id: u64, + /// The chain this indexer targets + pub chain: Chain, + /// A client for interacting with the relayer + pub relayer_client: RelayerClient, + /// The Arbitrum client + pub arbitrum_client: ArbitrumClient, + /// The decryption key + pub decryption_keys: Vec, + /// The database connection pool + pub db_pool: Arc, + /// The custody client + pub custody_client: CustodyClient, + /// The execution client + pub execution_client: ExecutionClient, + /// The AWS config + pub aws_config: SdkConfig, + /// The HMAC key for custody endpoint authentication + pub hmac_key: Option<[u8; 32]>, +} + +impl Server { + /// Build a server from the CLI + pub async fn build_from_cli(args: Cli) -> Result> { + // Parse an AWS config + let config = aws_config::defaults(BehaviorVersion::latest()) + .region(Region::new(DEFAULT_REGION)) + .load() + .await; + + // Build an Arbitrum client + let wallet = LocalWallet::from_str(DUMMY_PRIVATE_KEY)?; + let conf = ArbitrumClientConfig { + darkpool_addr: args.darkpool_address.clone(), + chain: args.chain, + rpc_url: args.rpc_url.clone(), + arb_priv_keys: vec![wallet], + block_polling_interval_ms: BLOCK_POLLING_INTERVAL_MS, + }; + let client = ArbitrumClient::new(conf).await?; + let chain_id = + client.chain_id().await.map_err(raw_err_str!("Error fetching chain ID: {}"))?; + + // Build the indexer + let mut decryption_keys = vec![DecryptionKey::from_hex_str(&args.relayer_decryption_key)?]; + if let Some(protocol_key) = &args.protocol_decryption_key { + decryption_keys.push(DecryptionKey::from_hex_str(protocol_key)?); + } + + let hmac_key = args.get_hmac_key(); + let relayer_client = RelayerClient::new(&args.relayer_url, &args.usdc_mint); + + // Create a database connection pool using bb8 + let db_pool = create_db_pool(&args.db_url).await?; + let arc_pool = Arc::new(db_pool); + + let custody_client = CustodyClient::new( + chain_id, + args.fireblocks_api_key, + args.fireblocks_api_secret, + args.rpc_url.clone(), + arc_pool.clone(), + config.clone(), + ); + + let execution_client = ExecutionClient::new( + args.execution_venue_api_key, + args.execution_venue_base_url, + &args.rpc_url, + )?; + + Ok(Self { + chain_id, + chain: args.chain, + relayer_client: relayer_client.clone(), + arbitrum_client: client.clone(), + decryption_keys, + db_pool: arc_pool, + custody_client, + execution_client, + aws_config: config, + hmac_key, + }) + } + + /// Build an indexer + pub fn build_indexer(&self) -> Result { + Ok(Indexer::new( + self.chain_id, + self.chain, + self.aws_config.clone(), + self.arbitrum_client.clone(), + self.decryption_keys.clone(), + self.db_pool.clone(), + self.relayer_client.clone(), + self.custody_client.clone(), + )) + } +}