Skip to content

Commit

Permalink
funds-manager: execution-client: Add quote execution helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
joeykraut committed Aug 8, 2024
1 parent a078091 commit 7782935
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ use rand::thread_rng;
use tracing::info;
use uuid::Uuid;

use super::{CustodyClient, ERC20};
use super::CustodyClient;
use crate::{
custody_client::DepositWithdrawSource,
error::FundsManagerError,
helpers::{create_secrets_manager_entry_with_description, get_secret},
helpers::{create_secrets_manager_entry_with_description, get_secret, ERC20},
};

impl CustodyClient {
Expand Down Expand Up @@ -109,6 +109,7 @@ impl CustodyClient {
Ok(())
}

/// Transfer funds from a vault to a hot wallet
pub async fn transfer_from_vault_to_hot_wallet(
&self,
vault: &str,
Expand Down
13 changes: 1 addition & 12 deletions funds-manager/funds-manager-server/src/custody_client/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
//! Manages the custody backend for the funds manager
#![allow(missing_docs)]
pub mod deposit;
pub mod gas_wallets;
mod hot_wallets;
Expand All @@ -8,7 +7,6 @@ pub mod withdraw;

use aws_config::SdkConfig as AwsConfig;
use ethers::middleware::SignerMiddleware;
use ethers::prelude::abigen;
use ethers::providers::{Http, Middleware, Provider};
use ethers::signers::{LocalWallet, Signer};
use ethers::types::{Address, TransactionReceipt, TransactionRequest};
Expand All @@ -26,16 +24,7 @@ use tracing::info;

use crate::db::{DbConn, DbPool};
use crate::error::FundsManagerError;

abigen!(
ERC20,
r#"[
function balanceOf(address account) external view returns (uint256)
function transfer(address recipient, uint256 amount) external returns (bool)
function symbol() external view returns (string memory)
function decimals() external view returns (uint8)
]"#
);
use crate::helpers::ERC20;

/// The source of a deposit
#[derive(Clone, Copy)]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//! Withdrawal methods for custodied funds
use std::str::FromStr;

use crate::{error::FundsManagerError, helpers::get_secret};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,21 @@ use std::fmt::Display;
/// An error returned by the execution client
#[derive(Debug, Clone)]
pub enum ExecutionClientError {
/// An error interacting with Arbitrum
Arbitrum(String),
/// An error returned by the execution client
Http(String),
/// An error parsing a value
Parse(String),
}

impl ExecutionClientError {
/// Create a new arbitrum error
#[allow(clippy::needless_pass_by_value)]
pub fn arbitrum<T: ToString>(e: T) -> Self {
ExecutionClientError::Arbitrum(e.to_string())
}

/// Create a new http error
#[allow(clippy::needless_pass_by_value)]
pub fn http<T: ToString>(e: T) -> Self {
Expand All @@ -28,6 +36,7 @@ impl ExecutionClientError {
impl Display for ExecutionClientError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let msg = match self {
ExecutionClientError::Arbitrum(e) => format!("Arbitrum error: {e}"),
ExecutionClientError::Http(e) => format!("HTTP error: {e}"),
ExecutionClientError::Parse(e) => format!("Parse error: {e}"),
};
Expand Down
31 changes: 29 additions & 2 deletions funds-manager/funds-manager-server/src/execution_client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@
//! API
pub mod error;
pub mod quotes;
pub mod swap;

use std::sync::Arc;

use ethers::{
middleware::SignerMiddleware,
providers::{Http, Provider},
signers::LocalWallet,
};
use reqwest::{Client, Url};
use serde::Deserialize;

Expand All @@ -22,12 +28,25 @@ pub struct ExecutionClient {
base_url: String,
/// The underlying HTTP client
http_client: Arc<Client>,
/// The RPC provider
rpc_provider: Arc<Provider<Http>>,
}

impl ExecutionClient {
/// Create a new client
pub fn new(api_key: String, base_url: String) -> Self {
Self { api_key, base_url, http_client: Arc::new(Client::new()) }
pub fn new(
api_key: String,
base_url: String,
rpc_url: String,

Check failure on line 40 in funds-manager/funds-manager-server/src/execution_client/mod.rs

View workflow job for this annotation

GitHub Actions / clippy

this argument is passed by value, but not consumed in the function body

error: this argument is passed by value, but not consumed in the function body --> funds-manager/funds-manager-server/src/execution_client/mod.rs:40:18 | 40 | rpc_url: String, | ^^^^^^ help: consider changing the type to: `&str` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_pass_by_value = note: requested on the command line with `-D clippy::needless-pass-by-value`
) -> Result<Self, ExecutionClientError> {
let provider =
Provider::<Http>::try_from(&rpc_url).map_err(ExecutionClientError::arbitrum)?;
Ok(Self {
api_key,
base_url,
http_client: Arc::new(Client::new()),
rpc_provider: Arc::new(provider),
})
}

/// Get a full URL for a given endpoint
Expand Down Expand Up @@ -61,4 +80,12 @@ impl ExecutionClient {
.await
.map_err(ExecutionClientError::http)
}

/// Get an instance of a signer middleware with the http provider attached
fn get_signer(
&self,
wallet: LocalWallet,
) -> SignerMiddleware<Arc<Provider<Http>>, LocalWallet> {
SignerMiddleware::new(self.rpc_provider.clone(), wallet)
}
}
32 changes: 25 additions & 7 deletions funds-manager/funds-manager-server/src/execution_client/quotes.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
//! Client methods for fetching quotes and prices from the execution venue

use serde::Deserialize;
use ethers::types::{Address, Bytes, U256};
use serde::{Deserialize, Serialize};

use crate::helpers::{
address_string_serialization, bytes_string_serialization, u256_string_serialization,
};

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

Expand All @@ -26,21 +31,34 @@ pub struct PriceResponse {
}

/// The subset of the quote response forwarded to consumers of this client
#[derive(Debug, Deserialize)]
#[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
pub sell_amount: String,
/// The quoted price
pub price: String,
/// The submitting address
pub from: String,
#[serde(with = "address_string_serialization")]
pub from: Address,
/// The 0x swap contract address
pub to: String,
#[serde(with = "address_string_serialization")]
pub to: Address,
/// The calldata for the swap
pub data: String,
#[serde(with = "bytes_string_serialization")]
pub data: Bytes,
/// The value of the tx; should be zero
pub value: String,
#[serde(with = "u256_string_serialization")]
pub value: U256,
/// The gas price used in the swap
pub gas_price: String,
#[serde(with = "u256_string_serialization")]
pub gas_price: U256,
}

impl ExecutionClient {
Expand Down
76 changes: 76 additions & 0 deletions funds-manager/funds-manager-server/src/execution_client/swap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//! Handlers for executing swaps

use std::str::FromStr;
use std::sync::Arc;

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

use crate::helpers::ERC20;

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

impl ExecutionClient {
/// Execute a quoted swap
pub async fn execute_swap(
&self,
quote: ExecutionQuote,
wallet: LocalWallet,
) -> Result<(), ExecutionClientError> {
// Approve the necessary ERC20 allowance
let amount = U256::from_str(&quote.sell_amount).map_err(ExecutionClientError::parse)?;
self.approve_erc20_allowance(quote.sell_token_address, quote.to, amount, &wallet).await?;

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

/// Execute a swap
async fn execute_swap_tx(
&self,
quote: ExecutionQuote,
wallet: LocalWallet,
) -> Result<TransactionReceipt, ExecutionClientError> {
let client = self.get_signer(wallet.clone());
let tx = Eip1559TransactionRequest::new()
.to(quote.to)
.from(quote.from)
.value(quote.value)
.data(quote.data);

// Send the transaction
let pending_tx = client
.send_transaction(tx, None /* block */)
.await
.map_err(ExecutionClientError::arbitrum)?;
pending_tx
.await
.map_err(ExecutionClientError::arbitrum)?
.ok_or_else(|| ExecutionClientError::arbitrum("Transaction failed"))
}
}
Loading

0 comments on commit 7782935

Please sign in to comment.