Skip to content

Commit

Permalink
Merge pull request #35 from renegade-fi/joey/transfer-endpoint-impl
Browse files Browse the repository at this point in the history
funds-manager: handlers: Handle quote fetch and execution
  • Loading branch information
joeykraut authored Aug 13, 2024
2 parents f045dae + 445290e commit 81c3a91
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 70 deletions.
2 changes: 1 addition & 1 deletion funds-manager/funds-manager-api/src/serialization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub(crate) mod address_string_serialization {

/// Serialize an address to a string
pub fn serialize<S: Serializer>(address: &Address, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&address.to_string())
s.serialize_str(&format!("{address:#x}"))
}

/// Deserialize a string to an address
Expand Down
8 changes: 5 additions & 3 deletions funds-manager/funds-manager-api/src/types/quoters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pub struct WithdrawFundsRequest {
// --- Execution --- //

/// The subset of the quote response forwarded to consumers of this client
#[derive(Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecutionQuote {
/// The token address we're buying
Expand Down Expand Up @@ -79,9 +79,11 @@ pub struct ExecutionQuote {
#[derive(Debug, Serialize, Deserialize)]
pub struct GetExecutionQuoteRequest {
/// The token address we're buying
pub buy_token_address: String,
#[serde(with = "address_string_serialization")]
pub buy_token_address: Address,
/// The token address we're selling
pub sell_token_address: String,
#[serde(with = "address_string_serialization")]
pub sell_token_address: Address,
/// The amount of tokens to sell
pub sell_amount: u128,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,9 @@ impl CustodyClient {
let secret_name = Self::hot_wallet_secret_name(address);
let secret_value = get_secret(&secret_name, &self.aws_config).await?;

LocalWallet::from_str(&secret_value).map_err(FundsManagerError::parse)
LocalWallet::from_str(&secret_value)
.map_err(FundsManagerError::parse)
.map(|w| w.with_chain_id(self.chain_id))
}

/// Fetch the token balance at the given address for a wallet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

use std::{error::Error, fmt::Display};

use warp::reject::Reject;

/// An error returned by the execution client
#[derive(Debug, Clone)]
pub enum ExecutionClientError {
Expand Down Expand Up @@ -44,8 +46,8 @@ impl Display for ExecutionClientError {
write!(f, "{}", msg)
}
}

impl Error for ExecutionClientError {}
impl Reject for ExecutionClientError {}

impl From<reqwest::Error> for ExecutionClientError {
fn from(e: reqwest::Error) -> Self {
Expand Down
22 changes: 14 additions & 8 deletions funds-manager/funds-manager-server/src/execution_client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ use ethers::{
providers::{Http, Provider},
signers::LocalWallet,
};
use http::StatusCode;
use reqwest::{Client, Url};
use serde::Deserialize;
use tracing::error;

use self::error::ExecutionClientError;

Expand Down Expand Up @@ -71,14 +73,18 @@ impl ExecutionClient {
params: &[(&str, &str)],
) -> Result<T, ExecutionClientError> {
let url = self.build_url(endpoint, params)?;
self.http_client
.get(url)
.header(API_KEY_HEADER, &self.api_key)
.send()
.await?
.json::<T>()
.await
.map_err(ExecutionClientError::http)
let response =
self.http_client.get(url).header(API_KEY_HEADER, &self.api_key).send().await?;

let status = response.status();
if status != StatusCode::OK {
let body = response.text().await?;
let msg = format!("Unexpected status code: {status}\nbody: {body}");
error!(msg);
return Err(ExecutionClientError::http(msg));
}

response.json::<T>().await.map_err(ExecutionClientError::http)
}

/// Get an instance of a signer middleware with the http provider attached
Expand Down
70 changes: 64 additions & 6 deletions funds-manager/funds-manager-server/src/execution_client/quotes.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
//! Client methods for fetching quotes and prices from the execution venue

use std::{str::FromStr, sync::Arc};

use ethers::{
signers::{LocalWallet, Signer},
types::{Address, U256},
};
use funds_manager_api::quoters::ExecutionQuote;
use serde::Deserialize;
use tracing::info;

use crate::helpers::ERC20;

use super::{error::ExecutionClientError, ExecutionClient};

Expand All @@ -19,6 +28,14 @@ const SELL_AMOUNT: &str = "sellAmount";
/// The taker address url param
const TAKER_ADDRESS: &str = "takerAddress";

/// The 0x exchange proxy contract address
///
/// TODO: This is the same across _most_ chains, but if we wish to support
/// one-off chains like ethereum sepolia, we should make this configurable
///
/// See: https://0x.org/docs/introduction/0x-cheat-sheet#exchange-proxy-addresses
const EXCHANGE_PROXY_ADDRESS: &str = "0xdef1c0ded9bec7f1a1670819833240f027b25eff";

/// The price response
#[derive(Debug, Deserialize)]
pub struct PriceResponse {
Expand All @@ -45,19 +62,60 @@ impl ExecutionClient {
/// Fetch a quote for an asset
pub async fn get_quote(
&self,
buy_asset: &str,
sell_asset: &str,
buy_asset: Address,
sell_asset: Address,
amount: u128,
recipient: &str,
wallet: &LocalWallet,
) -> Result<ExecutionQuote, ExecutionClientError> {
// First, set an approval for the sell token, the 0x api will not give a quote
// if its contract is not an approved spender for the requested amount
let exchange_addr = Address::from_str(EXCHANGE_PROXY_ADDRESS).unwrap();
self.approve_erc20_allowance(sell_asset, exchange_addr, U256::from(amount), wallet).await?;

let buy = format!("{buy_asset:#x}");
let sell = format!("{sell_asset:#x}");
let recipient = format!("{:#x}", wallet.address());
let amount_str = amount.to_string();
let params = [
(BUY_TOKEN, buy_asset),
(SELL_TOKEN, sell_asset),
(BUY_TOKEN, buy.as_str()),
(SELL_TOKEN, sell.as_str()),
(SELL_AMOUNT, amount_str.as_str()),
(TAKER_ADDRESS, recipient),
(TAKER_ADDRESS, recipient.as_str()),
];

self.send_get_request(QUOTE_ENDPOINT, &params).await
}

/// Approve an erc20 allowance
async fn approve_erc20_allowance(
&self,
token_address: Address,
spender: Address,
amount: U256,
wallet: &LocalWallet,
) -> Result<(), ExecutionClientError> {
let client = self.get_signer(wallet.clone());
let erc20 = ERC20::new(token_address, Arc::new(client));

// First, check if the allowance is already sufficient
let allowance = erc20
.allowance(wallet.address(), spender)
.await
.map_err(ExecutionClientError::arbitrum)?;
if allowance >= amount {
info!("Already approved erc20 allowance for {spender:#x}");
return Ok(());
}

// Otherwise, approve the allowance
let tx = erc20.approve(spender, amount);
let pending_tx = tx.send().await.map_err(ExecutionClientError::arbitrum)?;

let receipt = pending_tx
.await
.map_err(ExecutionClientError::arbitrum)?
.ok_or_else(|| ExecutionClientError::arbitrum("Transaction failed"))?;
info!("Approved erc20 allowance at: {:#x}", receipt.transaction_hash);
Ok(())
}
}
45 changes: 8 additions & 37 deletions funds-manager/funds-manager-server/src/execution_client/swap.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
//! Handlers for executing swaps

use std::sync::Arc;

use ethers::{
providers::Middleware,
signers::LocalWallet,
types::{Address, Eip1559TransactionRequest, TransactionReceipt, U256},
types::{Eip1559TransactionRequest, TransactionReceipt},
};
use funds_manager_api::quoters::ExecutionQuote;
use tracing::info;

use crate::helpers::ERC20;
use crate::helpers::TransactionHash;

use super::{error::ExecutionClientError, ExecutionClient};

Expand All @@ -19,47 +17,20 @@ impl ExecutionClient {
pub async fn execute_swap(
&self,
quote: ExecutionQuote,
wallet: LocalWallet,
) -> Result<(), ExecutionClientError> {
// Approve the necessary ERC20 allowance
self.approve_erc20_allowance(
quote.sell_token_address,
quote.to,
quote.sell_amount,
&wallet,
)
.await?;

wallet: &LocalWallet,
) -> Result<TransactionHash, ExecutionClientError> {
// Execute the swap
let receipt = self.execute_swap_tx(quote, wallet).await?;
info!("Swap executed at {}", receipt.transaction_hash);
Ok(())
}

/// Approve an erc20 allowance
async fn approve_erc20_allowance(
&self,
token_address: Address,
spender: Address,
amount: U256,
wallet: &LocalWallet,
) -> Result<TransactionReceipt, ExecutionClientError> {
let client = self.get_signer(wallet.clone());
let erc20 = ERC20::new(token_address, Arc::new(client));
let tx = erc20.approve(spender, amount);
let pending_tx = tx.send().await.map_err(ExecutionClientError::arbitrum)?;

pending_tx
.await
.map_err(ExecutionClientError::arbitrum)?
.ok_or_else(|| ExecutionClientError::arbitrum("Transaction failed"))
let tx_hash = receipt.transaction_hash;
info!("Swap executed at {tx_hash:#x}");
Ok(tx_hash)
}

/// Execute a swap
async fn execute_swap_tx(
&self,
quote: ExecutionQuote,
wallet: LocalWallet,
wallet: &LocalWallet,
) -> Result<TransactionReceipt, ExecutionClientError> {
let client = self.get_signer(wallet.clone());
let tx = Eip1559TransactionRequest::new()
Expand Down
36 changes: 25 additions & 11 deletions funds-manager/funds-manager-server/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ use funds_manager_api::hot_wallets::{
TransferToVaultRequest, WithdrawToHotWalletRequest,
};
use funds_manager_api::quoters::{
DepositAddressResponse, ExecuteSwapRequest, GetExecutionQuoteRequest, WithdrawFundsRequest,
DepositAddressResponse, ExecuteSwapRequest, ExecuteSwapResponse, GetExecutionQuoteRequest,
GetExecutionQuoteResponse, WithdrawFundsRequest,
};
use itertools::Itertools;
use serde_json::json;
Expand Down Expand Up @@ -117,22 +118,35 @@ pub(crate) async fn get_deposit_address_handler(

/// Handler for getting an execution quote
pub(crate) async fn get_execution_quote_handler(
_quote_request: GetExecutionQuoteRequest,
_server: Arc<Server>,
req: GetExecutionQuoteRequest,
server: Arc<Server>,
) -> Result<Json, warp::Rejection> {
// TODO: Implement this handler
println!("Getting execution quote");
Ok(warp::reply::json(&"Quote fetched"))
// Fetch the quoter hot wallet information
let vault = DepositWithdrawSource::Quoter.vault_name();
let hot_wallet = server.custody_client.get_hot_wallet_by_vault(vault).await?;
let wallet = server.custody_client.get_hot_wallet_private_key(&hot_wallet.address).await?;
let quote = server
.execution_client
.get_quote(req.buy_token_address, req.sell_token_address, req.sell_amount, &wallet)
.await
.map_err(|e| warp::reject::custom(ApiError::InternalError(e.to_string())))?;

let resp = GetExecutionQuoteResponse { quote };
Ok(warp::reply::json(&resp))
}

/// Handler for executing a swap
pub(crate) async fn execute_swap_handler(
_swap_request: ExecuteSwapRequest,
_server: Arc<Server>,
req: ExecuteSwapRequest,
server: Arc<Server>,
) -> Result<Json, warp::Rejection> {
// TODO: Implement this handler
println!("Executing swap");
Ok(warp::reply::json(&"Swap executed"))
let vault = DepositWithdrawSource::Quoter.vault_name();
let hot_wallet = server.custody_client.get_hot_wallet_by_vault(vault).await?;
let wallet = server.custody_client.get_hot_wallet_private_key(&hot_wallet.address).await?;

let tx = server.execution_client.execute_swap(req.quote, &wallet).await?;
let resp = ExecuteSwapResponse { tx_hash: format!("{tx:#x}") };
Ok(warp::reply::json(&resp))
}

// --- Gas --- //
Expand Down
8 changes: 6 additions & 2 deletions funds-manager/funds-manager-server/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@

use aws_config::SdkConfig;
use aws_sdk_secretsmanager::client::Client as SecretsManagerClient;
use ethers::contract::abigen;
use ethers::{contract::abigen, types::H256};
use renegade_util::err_str;

use crate::error::FundsManagerError;

/// A readable type alias for a transaction hash
pub type TransactionHash = H256;

// ---------
// | ERC20 |
// ---------
Expand All @@ -16,9 +19,10 @@ use crate::error::FundsManagerError;
abigen!(
ERC20,
r#"[
function balanceOf(address account) external view returns (uint256)
function symbol() external view returns (string memory)
function decimals() external view returns (uint8)
function balanceOf(address account) external view returns (uint256)
function allowance(address owner, address spender) external view returns (uint256)
function approve(address spender, uint256 value) external returns (bool)
function transfer(address recipient, uint256 amount) external returns (bool)
]"#
Expand Down

0 comments on commit 81c3a91

Please sign in to comment.