Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

funds-manager: execution-client: Add quote execution helpers #33

Merged
merged 1 commit into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: &str,
) -> 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)
joeykraut marked this conversation as resolved.
Show resolved Hide resolved
}
}
33 changes: 26 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,35 @@ 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
#[serde(with = "u256_string_serialization")]
pub sell_amount: U256,
/// 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
80 changes: 80 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,80 @@
//! Handlers for executing swaps

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
self.approve_erc20_allowance(
quote.sell_token_address,
quote.to,
quote.sell_amount,
&wallet,
)
.await?;

// Execute the swap
let receipt = self.execute_swap_tx(quote, wallet).await?;
joeykraut marked this conversation as resolved.
Show resolved Hide resolved
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)
joeykraut marked this conversation as resolved.
Show resolved Hide resolved
.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
Loading