diff --git a/crates/uniswapx-rs/src/order.rs b/crates/uniswapx-rs/src/order.rs index 9b8e9e1..e611479 100644 --- a/crates/uniswapx-rs/src/order.rs +++ b/crates/uniswapx-rs/src/order.rs @@ -1,5 +1,8 @@ +use std::error::Error; + +use alloy_dyn_abi::SolType; use alloy_primitives::Uint; -use alloy_sol_types::{sol, SolType}; +use alloy_sol_types::sol; use anyhow::Result; sol! { @@ -29,30 +32,73 @@ sol! { } #[derive(Debug)] - struct ExclusiveDutchOrder { - OrderInfo info; + struct CosignerData { uint256 decayStartTime; uint256 decayEndTime; address exclusiveFiller; uint256 exclusivityOverrideBps; - DutchInput input; - DutchOutput[] outputs; + uint256 inputAmount; + uint256[] outputAmounts; + } + + #[derive(Debug)] + struct V2DutchOrder { + OrderInfo info; + address cosigner; + DutchInput baseInput; + DutchOutput[] baseOutputs; + CosignerData cosignerData; + bytes cosignature; + } + + #[derive(Debug)] + struct PriorityInput { + address token; + uint256 amount; + uint256 mpsPerPriorityFeeWei; + } + + #[derive(Debug)] + struct PriorityOutput { + address token; + uint256 amount; + uint256 mpsPerPriorityFeeWei; + address recipient; + } + + #[derive(Debug)] + struct PriorityCosignerData { + uint256 auctionTargetBlock; + } + + #[derive(Debug)] + struct PriorityOrder { + OrderInfo info; + address cosigner; + uint256 auctionStartBlock; + uint256 baselinePriorityFeeWei; + PriorityInput input; + PriorityOutput[] outputs; + PriorityCosignerData cosignerData; + bytes cosignature; } } -pub fn decode_order(encoded_order: &str) -> Result { - let encoded_order = if encoded_order.starts_with("0x") { - &encoded_order[2..] - } else { - encoded_order - }; - let order_hex = hex::decode(encoded_order)?; +pub const MPS: u64 = 1e7 as u64; - Ok(ExclusiveDutchOrder::decode(&order_hex, false)?) +#[derive(Debug, Clone)] +pub enum Order { + V2DutchOrder(V2DutchOrder), + PriorityOrder(PriorityOrder), } -pub fn encode_order(order: &ExclusiveDutchOrder) -> Vec { - ExclusiveDutchOrder::encode(order) +impl Order { + pub fn encode(&self) -> Vec { + match self { + Order::V2DutchOrder(order) => order.encode_inner(), + Order::PriorityOrder(order) => order.encode_inner(), + } + } } #[derive(Debug, Clone)] @@ -79,9 +125,18 @@ pub enum OrderResolution { Resolved(ResolvedOrder), Expired, Invalid, + NotFillableYet } -impl ExclusiveDutchOrder { +impl V2DutchOrder { + pub fn decode_inner(order_hex: &[u8], validate: bool) -> Result> { + Ok(V2DutchOrder::decode_single(order_hex, validate)?) + } + + pub fn encode_inner(&self) -> Vec { + V2DutchOrder::encode_single(self) + } + pub fn resolve(&self, timestamp: u64) -> OrderResolution { let timestamp = Uint::from(timestamp); @@ -90,33 +145,34 @@ impl ExclusiveDutchOrder { }; // resolve over the decay curve + // TODO: apply cosigner logic let input = ResolvedInput { - token: self.input.token.to_string(), + token: self.baseInput.token.to_string(), amount: resolve_decay( timestamp, - self.decayStartTime, - self.decayEndTime, - self.input.startAmount, - self.input.endAmount, + self.cosignerData.decayStartTime, + self.cosignerData.decayEndTime, + self.baseInput.startAmount, + self.baseInput.endAmount, ), }; let outputs = self - .outputs + .baseOutputs .iter() .map(|output| { let mut amount = resolve_decay( timestamp, - self.decayStartTime, - self.decayEndTime, + self.cosignerData.decayStartTime, + self.cosignerData.decayEndTime, output.startAmount, output.endAmount, ); // add exclusivity override to amount - if self.decayStartTime.gt(×tamp) && !self.exclusiveFiller.is_zero() { - let exclusivity = self.exclusivityOverrideBps.wrapping_add(Uint::from(10000)); + if self.cosignerData.decayStartTime.gt(×tamp) && !self.cosignerData.exclusiveFiller.is_zero() { + let exclusivity = self.cosignerData.exclusivityOverrideBps.wrapping_add(Uint::from(10000)); let exclusivity = exclusivity.wrapping_mul(amount); amount = exclusivity.wrapping_div(Uint::from(10000)); }; @@ -133,6 +189,59 @@ impl ExclusiveDutchOrder { } } +impl PriorityOrder { + pub fn decode_inner(order_hex: &[u8], validate: bool) -> Result> { + Ok(PriorityOrder::decode_single(order_hex, validate)?) + } + + pub fn encode_inner(&self) -> Vec { + PriorityOrder::encode_single(self) + } + + pub fn resolve(&self, block_number: u64, timestamp: u64, priority_fee: Uint<256, 4>) -> OrderResolution { + let timestamp = Uint::from(timestamp); + + if self.info.deadline.lt(×tamp) { + return OrderResolution::Expired; + }; + + let input = self.input.scale(priority_fee); + let outputs = self + .outputs + .iter() + .map(|output| output.scale(priority_fee)) + .collect(); + + if Uint::from(block_number).lt(&self.cosignerData.auctionTargetBlock.saturating_sub(Uint::from(2))) { + return OrderResolution::NotFillableYet; + }; + + OrderResolution::Resolved(ResolvedOrder { input, outputs }) + } +} + +impl PriorityInput { + pub fn scale(&self, priority_fee: Uint<256, 4>) -> ResolvedInput { + let amount = self.amount.wrapping_mul(Uint::from(MPS).wrapping_add(priority_fee.wrapping_mul(self.mpsPerPriorityFeeWei))).wrapping_div(Uint::from(MPS)); + ResolvedInput { + token: self.token.to_string(), + amount, + } + } +} + +impl PriorityOutput { + pub fn scale(&self, priority_fee: Uint<256, 4>) -> ResolvedOutput { + let amount = self.amount.wrapping_mul(Uint::from(MPS).saturating_sub(priority_fee.wrapping_mul(self.mpsPerPriorityFeeWei))).wrapping_div(Uint::from(MPS)); + ResolvedOutput { + token: self.token.to_string(), + amount, + recipient: self.recipient.to_string(), + } + } + +} + fn resolve_decay( at_time: Uint<256, 4>, start_time: Uint<256, 4>, diff --git a/src/collectors/uniswapx_order_collector.rs b/src/collectors/uniswapx_order_collector.rs index 3587135..6828b3b 100644 --- a/src/collectors/uniswapx_order_collector.rs +++ b/src/collectors/uniswapx_order_collector.rs @@ -4,12 +4,60 @@ use async_trait::async_trait; use futures::{stream, StreamExt}; use reqwest::Client; use serde::Deserialize; +use std::fmt; +use std::str::FromStr; +use std::string::ToString; use tokio::time::Duration; use tokio_stream::wrappers::IntervalStream; static UNISWAPX_API_URL: &str = "https://api.uniswap.org/v2"; -static POLL_INTERVAL_SECS: u64 = 5; -pub const CHAIN_ID: u64 = 1; +static POLL_INTERVAL_SECS: u64 = 1; + +#[derive(Debug)] +pub enum OrderTypeError { + InvalidOrderType, +} + +impl fmt::Display for OrderTypeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Invalid order type") + } +} + +impl std::error::Error for OrderTypeError {} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum OrderType { + DutchV2, + Priority, +} + +impl FromStr for OrderType { + type Err = OrderTypeError; + + fn from_str(s: &str) -> Result { + match s { + "Dutch_V2" => Ok(OrderType::DutchV2), + "Priority" => Ok(OrderType::Priority), + _ => Err(OrderTypeError::InvalidOrderType), + } + } +} + +impl ToString for OrderType { + fn to_string(&self) -> String { + match self { + OrderType::DutchV2 => "Dutch_V2".to_string(), + OrderType::Priority => "Priority".to_string(), + } + } +} + +impl Default for OrderType { + fn default() -> Self { + OrderType::DutchV2 + } +} #[derive(Debug, Clone, Deserialize)] pub struct UniswapXOrder { @@ -38,13 +86,17 @@ pub struct UniswapXOrderResponse { pub struct UniswapXOrderCollector { pub client: Client, pub base_url: String, + pub chain_id: u64, + pub order_type: OrderType, } impl UniswapXOrderCollector { - pub fn new() -> Self { + pub fn new(chain_id: u64, order_type: OrderType) -> Self { Self { client: Client::new(), base_url: UNISWAPX_API_URL.to_string(), + chain_id, + order_type, } } } @@ -56,8 +108,10 @@ impl UniswapXOrderCollector { impl Collector for UniswapXOrderCollector { async fn get_event_stream(&self) -> Result> { let url = format!( - "{}/orders?orderStatus=open&chainId={}", - self.base_url, CHAIN_ID + "{}/orders?orderStatus=open&chainId={}&orderType={}", + self.base_url, + self.chain_id, + self.order_type.to_string() ); // stream that polls the UniswapX API every 5 seconds @@ -94,8 +148,10 @@ impl Collector for UniswapXOrderCollector { mod tests { use crate::collectors::uniswapx_order_collector::UniswapXOrderCollector; use artemis_core::types::Collector; + use ethers::utils::hex; use futures::StreamExt; use mockito::{Mock, Server, ServerGuard}; + use uniswapx_rs::order::V2DutchOrder; async fn get_collector(mock_response: &str) -> (UniswapXOrderCollector, ServerGuard, Mock) { let mut server = Server::new_async().await; @@ -112,6 +168,8 @@ mod tests { let res = UniswapXOrderCollector { client: reqwest::Client::new(), base_url: url.clone(), + chain_id: 1, + order_type: super::OrderType::DutchV2, }; (res, server, mock) @@ -140,4 +198,32 @@ mod tests { ); mock.assert_async().await; } + + #[tokio::test] + async fn decodes_v2_order() { + let response = r#" +{"orders":[{"type":"Dutch_V2","orderStatus":"open","signature":"0x6eb32e7912d333e9c1ab162db02ed1656cdc8fbea2e21e70cd3634e8a3bd85d0582b46cacb584412ef3e035837b005b70f67897969426f9795128ea52de3a8cf1b","encodedOrder":"0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001000000000000000000000000004449cd34d1eb1fedcf02a1be3834ffde8e6a61800000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000000422ca8b0a00a4250000000000000000000000000000000000000000000000000422ca8b0a00a42500000000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000011f84b9aa48e5f8aa8b9897600006289be000000000000000000000000c9838bbf85ad068136e8da07021e9e131201901904683298fe8b71446644eba514e387688690bde85b7bcaf8de44455a6aaf7a3000000000000000000000000000000000000000000000000000000000669adac5000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003c330a127f1ec70000000000000000000000000000000000000000000000000034be9ca1484989000000000000000000000000c9838bbf85ad068136e8da07021e9e131201901900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000269fc8de5047000000000000000000000000000000000000000000000000000021d754744fbe000000000000000000000000000000fee13a103a10d593b9ae06b3e05f2e7e1c00000000000000000000000000000000000000000000000000000000669ad9b600000000000000000000000000000000000000000000000000000000669ad9f20000000000000000000000006f1cdbbb4d53d226cf4b917bf768b94acbab61680000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000003c64146542c1fd00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041d90e87f6f9e84487bfbb5170e856a332769359664c72f90250ee8917baf3a5920e87d331fcf97456e5d4d88761c552a9115569861aa96120b56d882339bbaac91c00000000000000000000000000000000000000000000000000000000000000","chainId":1,"nonce":"1993352701105935839386570705396248068916924096291549856616269381900329515568","orderHash":"0x382f612930c2121ed91fcdc00972f76b4adbef8d111830e1d135ac944a144876","swapper":"0xC9838Bbf85Ad068136E8DA07021E9e1312019019","input":{"token":"0x6982508145454Ce325dDbE47a25d4ec3d2311933","startAmount":"5000000000000000000000000","endAmount":"5000000000000000000000000"},"outputs":[{"token":"0x0000000000000000000000000000000000000000","startAmount":"16944616955649735","endAmount":"14846278718998921","recipient":"0xC9838Bbf85Ad068136E8DA07021E9e1312019019"},{"token":"0x0000000000000000000000000000000000000000","startAmount":"42467711668295","endAmount":"37208718593982","recipient":"0x000000fee13a103A10D593b9AE06b3e05F2E7E1c"}],"cosignerData":{"decayStartTime":1721424310,"decayEndTime":1721424370,"exclusiveFiller":"0x6F1cDbBb4d53d226CF4B917bF768B94acbAB6168","inputOverride":"0","outputOverrides":["16998537363636733","0"]},"cosignature":"0xd90e87f6f9e84487bfbb5170e856a332769359664c72f90250ee8917baf3a5920e87d331fcf97456e5d4d88761c552a9115569861aa96120b56d882339bbaac91c","quoteId":"221f421a-455d-4358-8376-6b4fb0ffb0f1","requestId":"775eea31-3173-4f1c-b7d2-bcd6fbcf2301","createdAt":1721424286}]} "#; + let (collector, _server, _) = get_collector(response).await; + // get event stream and parse events + let stream = collector.get_event_stream().await.unwrap(); + let (first_order, _) = stream.into_future().await; + assert!(first_order.is_some()); + assert_eq!( + first_order.clone().unwrap().order_hash, + "0x382f612930c2121ed91fcdc00972f76b4adbef8d111830e1d135ac944a144876" + ); + let encoded_order = &first_order.unwrap().encoded_order; + let encoded_order = if encoded_order.starts_with("0x") { + &encoded_order[2..] + } else { + encoded_order + }; + let order_hex: Vec = hex::decode(encoded_order).unwrap(); + + let result = V2DutchOrder::decode_inner(&order_hex, false); + match result { + Err(e) => panic!("Error decoding order: {:?}", e), + _ => (), + } + } } diff --git a/src/collectors/uniswapx_route_collector.rs b/src/collectors/uniswapx_route_collector.rs index be6721a..2247fa3 100644 --- a/src/collectors/uniswapx_route_collector.rs +++ b/src/collectors/uniswapx_route_collector.rs @@ -1,13 +1,11 @@ -use crate::collectors::uniswapx_order_collector::CHAIN_ID; use alloy_primitives::Uint; -use anyhow::Result; +use anyhow::{Context, Result}; use reqwest::header::ORIGIN; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc::{Receiver, Sender}; use tracing::info; -use uniswapx_rs::order::{ExclusiveDutchOrder, ResolvedOrder}; +use uniswapx_rs::order::{Order, ResolvedOrder}; -use crate::strategies::uniswapx_strategy::EXECUTOR_ADDRESS; use artemis_core::types::{Collector, CollectorStream}; use async_trait::async_trait; use futures::lock::Mutex; @@ -20,7 +18,7 @@ const DEADLINE: u64 = 1000; #[derive(Debug, Clone)] pub struct OrderData { - pub order: ExclusiveDutchOrder, + pub order: Order, pub hash: String, pub signature: String, pub resolved: ResolvedOrder, @@ -35,7 +33,7 @@ pub struct OrderBatchData { pub token_out: String, } -#[derive(Serialize)] +#[derive(Serialize, Debug)] #[allow(dead_code)] enum TradeType { #[serde(rename = "exactIn")] @@ -44,7 +42,7 @@ enum TradeType { ExactOut, } -#[derive(Serialize)] +#[derive(Serialize, Debug)] #[serde(rename_all = "camelCase")] struct RoutingApiQuery { token_in_address: String, @@ -57,6 +55,8 @@ struct RoutingApiQuery { recipient: String, slippage_tolerance: String, deadline: u64, + #[serde(rename = "enableUniversalRouter")] + enable_universal_router: bool, } #[derive(Clone, Debug, Deserialize)] @@ -109,6 +109,7 @@ pub struct MethodParameters { #[serde(rename_all = "camelCase")] pub struct OrderRoute { pub quote: String, + pub quote_gas_adjusted: String, pub gas_price_wei: String, pub gas_use_estimate_quote: String, pub gas_use_estimate: String, @@ -117,9 +118,11 @@ pub struct OrderRoute { } pub struct RouteOrderParams { + pub chain_id: u64, pub token_in: String, pub token_out: String, pub amount: String, + pub recipient: String, } #[derive(Clone, Debug)] @@ -138,19 +141,25 @@ pub struct RouteResponse { /// [events](Route) which contain the order. pub struct UniswapXRouteCollector { pub client: Client, + pub chain_id: u64, pub route_request_receiver: Mutex>>, pub route_sender: Sender, + pub executor_address: String, } impl UniswapXRouteCollector { pub fn new( + chain_id: u64, route_request_receiver: Receiver>, route_sender: Sender, + executor_address: String, ) -> Self { Self { client: Client::new(), + chain_id, route_request_receiver: Mutex::new(route_request_receiver), route_sender, + executor_address, } } } @@ -174,20 +183,27 @@ impl Collector for UniswapXRouteCollector { async move { (batch, route_order(RouteOrderParams { + chain_id: self.chain_id, token_in: token_in.clone(), token_out: token_out.clone(), amount: amount_in.to_string(), + recipient: self.executor_address.clone(), }).await) } }).collect(); let routes: Vec<_> = tasks.collect().await; for (batch, route_result) in routes { - if let Ok(route) = route_result { - yield RoutedOrder { - request: batch.clone(), - route: route, - }; + match route_result { + Ok(route) => { + yield RoutedOrder { + request: batch.clone(), + route: route, + }; + } + Err(e) => { + info!("Failed to route order: {}", e); + } } } } @@ -200,14 +216,15 @@ impl Collector for UniswapXRouteCollector { pub async fn route_order(params: RouteOrderParams) -> Result { // TODO: support exactOutput let query = RoutingApiQuery { - token_in_address: params.token_in, - token_out_address: params.token_out, - token_in_chain_id: CHAIN_ID, - token_out_chain_id: CHAIN_ID, + token_in_address: resolve_address(params.token_in), + token_out_address: resolve_address(params.token_out), + token_in_chain_id: params.chain_id, + token_out_chain_id: params.chain_id, trade_type: TradeType::ExactIn, amount: params.amount, - recipient: EXECUTOR_ADDRESS.to_string(), + recipient: params.recipient, slippage_tolerance: SLIPPAGE_TOLERANCE.to_string(), + enable_universal_router: false, deadline: DEADLINE, }; @@ -218,8 +235,19 @@ pub async fn route_order(params: RouteOrderParams) -> Result { Ok(client .get(format!("{}?{}", ROUTING_API, query_string)) .header(ORIGIN, "https://app.uniswap.org") + .header("x-request-source", "uniswap-web") .send() - .await? + .await + .context("Quote request failed with {}")? .json::() - .await?) + .await + .context("Failed to parse response: {}")?) +} + +// The Uniswap routing API requires that "ETH" be used instead of the zero address +fn resolve_address(token: String) -> String { + if token == "0x0000000000000000000000000000000000000000" { + return "ETH".to_string(); + } + return token; } diff --git a/src/executors/mod.rs b/src/executors/mod.rs index 280b1af..17706c8 100644 --- a/src/executors/mod.rs +++ b/src/executors/mod.rs @@ -1 +1,2 @@ pub mod protect_executor; +pub mod public_1559_executor; diff --git a/src/executors/protect_executor.rs b/src/executors/protect_executor.rs index 03552e8..172c7a2 100644 --- a/src/executors/protect_executor.rs +++ b/src/executors/protect_executor.rs @@ -35,7 +35,6 @@ where { /// Send a transaction to the mempool. async fn execute(&self, mut action: SubmitTxToMempool) -> Result<()> { - info!("Executing tx {:?}", action.tx); let gas_usage_result = self .client .estimate_gas(&action.tx, None) @@ -60,6 +59,8 @@ where .context("Error getting gas price: {}")?; } action.tx.set_gas_price(bid_gas_price); + + info!("Executing tx {:?}", action.tx); self.sender_client.send_transaction(action.tx, None).await?; Ok(()) } diff --git a/src/executors/public_1559_executor.rs b/src/executors/public_1559_executor.rs new file mode 100644 index 0000000..ac39e67 --- /dev/null +++ b/src/executors/public_1559_executor.rs @@ -0,0 +1,79 @@ +use std::sync::Arc; +use tracing::info; + +use anyhow::{Context, Result}; +use artemis_core::types::Executor; +use async_trait::async_trait; +use ethers::{providers::Middleware, types::U256}; + +use crate::strategies::types::SubmitTxToMempoolWithExecutionMetadata; + +/// An executor that sends transactions to the public mempool. +pub struct Public1559Executor { + client: Arc, + sender_client: Arc, +} + +impl Public1559Executor { + pub fn new(client: Arc, sender_client: Arc) -> Self { + Self { + client, + sender_client, + } + } +} + +#[async_trait] +impl Executor for Public1559Executor +where + M: Middleware, + M::Error: 'static, + N: Middleware, + N::Error: 'static, +{ + /// Send a transaction to the mempool. + async fn execute(&self, mut action: SubmitTxToMempoolWithExecutionMetadata) -> Result<()> { + let gas_usage_result = self + .client + .estimate_gas(&action.execution.tx, None) + .await + .unwrap_or_else(|err| { + info!("Error estimating gas: {}", err); + U256::from(1_000_000) + }); + info!("Gas Usage {:?}", gas_usage_result); + + let bid_priority_fee; + let base_fee: U256 = self + .client + .get_gas_price() + .await + .context("Error getting gas price: {}")?; + + if let Some(gas_bid_info) = action.execution.gas_bid_info { + // priority fee at which we'd break even, meaning 100% of profit goes to user in the form of price improvement + // TODO: use gas estimate here + bid_priority_fee = action + .metadata + .calculate_priority_fee(gas_bid_info.bid_percentage) + } else { + bid_priority_fee = Some(U256::from(50)); + } + + let eip1559_tx = action.execution.tx.as_eip1559_mut(); + if let Some(eip1559_tx) = eip1559_tx { + eip1559_tx.max_fee_per_gas = Some(base_fee); + eip1559_tx.max_priority_fee_per_gas = bid_priority_fee; + } else { + return Err(anyhow::anyhow!("Transaction is not EIP1559")); + } + + action.execution.tx.set_gas(gas_usage_result); + + info!("Executing tx {:?}", action.execution.tx); + self.sender_client + .send_transaction(action.execution.tx, None) + .await?; + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index d021a1d..ccf06b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,9 +3,9 @@ use clap::Parser; use artemis_core::engine::Engine; use artemis_core::types::{CollectorMap, ExecutorMap}; +use collectors::uniswapx_order_collector::OrderType; use collectors::{ - block_collector::BlockCollector, - uniswapx_order_collector::{UniswapXOrderCollector, CHAIN_ID}, + block_collector::BlockCollector, uniswapx_order_collector::UniswapXOrderCollector, uniswapx_route_collector::UniswapXRouteCollector, }; use ethers::{ @@ -14,7 +14,9 @@ use ethers::{ signers::{LocalWallet, Signer}, }; use executors::protect_executor::ProtectExecutor; +use executors::public_1559_executor::Public1559Executor; use std::sync::Arc; +use strategies::priority_strategy::UniswapXPriorityFill; use strategies::{ types::{Action, Config, Event}, uniswapx_strategy::UniswapXUniswapFill, @@ -43,6 +45,18 @@ pub struct Args { /// Percentage of profit to pay in gas. #[arg(long)] pub bid_percentage: u64, + + /// Private key for sending txs. + #[arg(long)] + pub executor_address: String, + + /// Order type to use. + #[arg(long)] + pub order_type: OrderType, + + /// chain id + #[arg(long)] + pub chain_id: u64, } #[tokio::main] @@ -62,6 +76,7 @@ async fn main() -> Result<()> { // Set up ethers provider. let ws = Ws::connect(args.wss).await?; let provider = Provider::new(ws); + let chain_id = args.chain_id; let mevblocker_provider = Provider::::try_from(MEV_BLOCKER).expect("could not instantiate HTTP Provider"); @@ -70,7 +85,7 @@ async fn main() -> Result<()> { .private_key .parse::() .unwrap() - .with_chain_id(CHAIN_ID); + .with_chain_id(chain_id); let address = wallet.address(); let provider = Arc::new(provider.nonce_manager(address).with_signer(wallet.clone())); @@ -91,13 +106,21 @@ async fn main() -> Result<()> { let (batch_sender, batch_receiver) = channel(512); let (route_sender, route_receiver) = channel(512); - let uniswapx_collector = Box::new(UniswapXOrderCollector::new()); - let uniswapx_collector = - CollectorMap::new(uniswapx_collector, |e| Event::UniswapXOrder(Box::new(e))); - engine.add_collector(Box::new(uniswapx_collector)); + let uniswapx_order_collector = Box::new(UniswapXOrderCollector::new( + chain_id, + args.order_type.clone(), + )); + let uniswapx_order_collector = CollectorMap::new(uniswapx_order_collector, |e| { + Event::UniswapXOrder(Box::new(e)) + }); + engine.add_collector(Box::new(uniswapx_order_collector)); - let uniswapx_route_collector = - Box::new(UniswapXRouteCollector::new(batch_receiver, route_sender)); + let uniswapx_route_collector = Box::new(UniswapXRouteCollector::new( + chain_id, + batch_receiver, + route_sender, + args.executor_address.clone(), + )); let uniswapx_route_collector = CollectorMap::new(uniswapx_route_collector, |e| { Event::UniswapXRoute(Box::new(e)) }); @@ -105,26 +128,52 @@ async fn main() -> Result<()> { let config = Config { bid_percentage: args.bid_percentage, + executor_address: args.executor_address, }; - let strategy = UniswapXUniswapFill::new( - Arc::new(provider.clone()), - config, - batch_sender, - route_receiver, - ); - engine.add_strategy(Box::new(strategy)); + match &args.order_type { + OrderType::DutchV2 => { + let uniswapx_strategy = UniswapXUniswapFill::new( + Arc::new(provider.clone()), + config.clone(), + batch_sender, + route_receiver, + ); + engine.add_strategy(Box::new(uniswapx_strategy)); + } + OrderType::Priority => { + let priority_strategy = UniswapXPriorityFill::new( + Arc::new(provider.clone()), + config.clone(), + batch_sender, + route_receiver, + ); + + engine.add_strategy(Box::new(priority_strategy)); + } + } - let executor = Box::new(ProtectExecutor::new( + let protect_executor = Box::new(ProtectExecutor::new( provider.clone(), mevblocker_provider.clone(), )); - let executor = ExecutorMap::new(executor, |action| match action { + let public_tx_executor = Box::new(Public1559Executor::new(provider.clone(), provider.clone())); + + let protect_executor = ExecutorMap::new(protect_executor, |action| match action { Action::SubmitTx(tx) => Some(tx), + // No op for public transactions + _ => None, + }); + + let public_tx_executor = ExecutorMap::new(public_tx_executor, |action| match action { + Action::SubmitPublicTx(execution) => Some(execution), + // No op for protected transactions + _ => None, }); - engine.add_executor(Box::new(executor)); + engine.add_executor(Box::new(protect_executor)); + engine.add_executor(Box::new(public_tx_executor)); // Start engine. if let Ok(mut set) = engine.run().await { while let Some(res) = set.join_next().await { diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index 7ef947a..850329d 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -1,2 +1,4 @@ +pub mod priority_strategy; +pub mod shared; pub mod types; pub mod uniswapx_strategy; diff --git a/src/strategies/priority_strategy.rs b/src/strategies/priority_strategy.rs new file mode 100644 index 0000000..97065b6 --- /dev/null +++ b/src/strategies/priority_strategy.rs @@ -0,0 +1,421 @@ +use super::{ + shared::UniswapXStrategy, + types::{Config, OrderStatus}, +}; +use crate::{ + collectors::{ + block_collector::NewBlock, + uniswapx_order_collector::UniswapXOrder, + uniswapx_route_collector::{OrderBatchData, OrderData, RoutedOrder}, + }, + strategies::types::SubmitTxToMempoolWithExecutionMetadata, +}; +use alloy_primitives::Uint; +use anyhow::Result; +use artemis_core::executors::mempool_executor::{GasBidInfo, SubmitTxToMempool}; +use artemis_core::types::Strategy; +use async_trait::async_trait; +use bindings_uniswapx::shared_types::SignedOrder; +use ethers::{ + providers::Middleware, + types::{Address, Bytes, Filter, U256}, + utils::hex, +}; +use std::collections::HashMap; +use std::error::Error; +use std::str::FromStr; +use std::sync::Arc; +use tokio::sync::mpsc::{Receiver, Sender}; +use tracing::{error, info}; +use uniswapx_rs::order::{Order, OrderResolution, PriorityOrder, MPS}; + +use super::types::{Action, Event}; + +const BLOCK_TIME: u64 = 2; +const DONE_EXPIRY: u64 = 300; +// Base addresses +const REACTOR_ADDRESS: &str = "0x000000001Ec5656dcdB24D90DFa42742738De729"; +pub const WETH_ADDRESS: &str = "0x4200000000000000000000000000000000000006"; + +#[derive(Debug, Clone)] +pub struct ExecutionMetadata { + // amount of quote token we can get + quote: U256, + // amount of quote token needed to fill the order + amount_out_required: U256, +} + +impl ExecutionMetadata { + pub fn new(quote: U256, amount_out_required: U256) -> Self { + Self { + quote, + amount_out_required, + } + } + + pub fn calculate_priority_fee(&self, bid_percentage: u64) -> Option { + if self.quote.le(&self.amount_out_required) { + return None; + } + + let profit_quote = self.quote.saturating_sub(self.amount_out_required); + + let mps_of_improvement = profit_quote + .saturating_mul(U256::from(MPS)) + .checked_div(self.amount_out_required)?; + info!("mps_of_improvement: {}", mps_of_improvement); + let priority_fee = mps_of_improvement + .checked_mul(U256::from(bid_percentage))? + .checked_div(U256::from(100))?; + return Some(priority_fee); + } +} + +#[derive(Debug)] +#[allow(dead_code)] +pub struct UniswapXPriorityFill { + /// Ethers client. + client: Arc, + /// executor address + executor_address: String, + /// Amount of profits to bid in gas + bid_percentage: u64, + last_block_number: u64, + last_block_timestamp: u64, + // map of open order hashes to order data + open_orders: HashMap, + // map of done order hashes to time at which we can safely prune them + done_orders: HashMap, + batch_sender: Sender>, + route_receiver: Receiver, +} + +impl UniswapXPriorityFill { + pub fn new( + client: Arc, + config: Config, + sender: Sender>, + receiver: Receiver, + ) -> Self { + info!("syncing state"); + + Self { + client, + executor_address: config.executor_address, + bid_percentage: config.bid_percentage, + last_block_number: 0, + last_block_timestamp: 0, + open_orders: HashMap::new(), + done_orders: HashMap::new(), + batch_sender: sender, + route_receiver: receiver, + } + } +} + +#[async_trait] +impl Strategy for UniswapXPriorityFill { + // In order to sync this strategy, we need to get the current bid for all Sudo pools. + async fn sync_state(&mut self) -> Result<()> { + info!("syncing state"); + + Ok(()) + } + + // Process incoming events, seeing if we can arb new orders, and updating the internal state on new blocks. + async fn process_event(&mut self, event: Event) -> Option { + match event { + Event::UniswapXOrder(order) => self.process_order_event(*order).await, + Event::NewBlock(block) => self.process_new_block_event(block).await, + Event::UniswapXRoute(route) => self.process_new_route(*route).await, + } + } +} + +impl UniswapXStrategy for UniswapXPriorityFill {} + +impl UniswapXPriorityFill { + fn decode_order(&self, encoded_order: &str) -> Result> { + let encoded_order = if encoded_order.starts_with("0x") { + &encoded_order[2..] + } else { + encoded_order + }; + let order_hex = hex::decode(encoded_order)?; + + Ok(PriorityOrder::decode_inner(&order_hex, false)?) + } + + async fn process_order_event(&mut self, event: UniswapXOrder) -> Option { + if self.last_block_timestamp == 0 { + return None; + } + + let order = self + .decode_order(&event.encoded_order) + .map_err(|e| error!("failed to decode: {}", e)) + .ok()?; + + self.update_order_state(order, event.signature, event.order_hash); + // try to send immediately + self.batch_sender + .send(self.get_order_batches()) + .await + .ok()?; + + None + } + + async fn process_new_route(&mut self, event: RoutedOrder) -> Option { + if event + .request + .orders + .iter() + .any(|o| self.done_orders.contains_key(&o.hash)) + { + return None; + } + + let OrderBatchData { + // orders, + orders, + amount_out_required, + .. + } = &event.request; + + if let Some(profit) = self.get_execution_metadata(&event) { + info!( + "Sending trade: num trades: {} routed quote: {}, batch needs: {}", + orders.len(), + event.route.quote, + amount_out_required, + ); + + let signed_orders = self.get_signed_orders(orders.clone()).ok()?; + return Some(Action::SubmitPublicTx( + SubmitTxToMempoolWithExecutionMetadata { + execution: SubmitTxToMempool { + tx: self + .build_fill( + self.client.clone(), + &self.executor_address, + signed_orders, + event, + ) + .await + .ok()?, + gas_bid_info: Some(GasBidInfo { + bid_percentage: self.bid_percentage, + // this field is not used for priority orders + total_profit: U256::from(0), + }), + }, + metadata: profit, + }, + )); + } + + None + } + + /// Process new block events, updating the internal state. + async fn process_new_block_event(&mut self, event: NewBlock) -> Option { + self.last_block_number = event.number.as_u64(); + self.last_block_timestamp = event.timestamp.as_u64(); + + info!( + "Processing block {} at {}, Order set sizes -- open: {}, done: {}", + event.number, + event.timestamp, + self.open_orders.len(), + self.done_orders.len() + ); + self.handle_fills() + .await + .map_err(|e| error!("Error handling fills {}", e)) + .ok()?; + self.update_open_orders(); + self.prune_done_orders(); + + self.batch_sender + .send(self.get_order_batches()) + .await + .ok()?; + + None + } + + /// encode orders into generic signed orders + fn get_signed_orders(&self, orders: Vec) -> Result> { + let mut signed_orders: Vec = Vec::new(); + for batch in orders.iter() { + match &batch.order { + Order::PriorityOrder(order) => { + signed_orders.push(SignedOrder { + order: Bytes::from(order.encode_inner()), + sig: Bytes::from_str(&batch.signature)?, + }); + } + _ => { + return Err(anyhow::anyhow!("Invalid order type")); + } + } + } + Ok(signed_orders) + } + + /// We do not batch orders because priority fee is applied on the transaction level + fn get_order_batches(&self) -> Vec { + let mut order_batches: Vec = Vec::new(); + + // generate batches of size 1 + self.open_orders.iter().for_each(|(_, order_data)| { + let amount_in = order_data.resolved.input.amount; + let amount_out = order_data + .resolved + .outputs + .iter() + .fold(Uint::from(0), |sum, output| sum.wrapping_add(output.amount)); + + order_batches.push(OrderBatchData { + orders: vec![order_data.clone()], + amount_in, + amount_out_required: amount_out, + token_in: order_data.resolved.input.token.clone(), + token_out: order_data.resolved.outputs[0].token.clone(), + }); + }); + order_batches + } + + async fn handle_fills(&mut self) -> Result<()> { + let reactor_address = REACTOR_ADDRESS.parse::
().unwrap(); + let filter = Filter::new() + .select(self.last_block_number) + .address(reactor_address) + .event("Fill(bytes32,address,address,uint256)"); + + // early return on error + let logs = self.client.get_logs(&filter).await?; + for log in logs { + let order_hash = format!("0x{:x}", log.topics[1]); + // remove from open + info!("Removing filled order {}", order_hash); + self.open_orders.remove(&order_hash); + // add to done + self.done_orders.insert( + order_hash.to_string(), + self.current_timestamp()? + DONE_EXPIRY, + ); + } + + Ok(()) + } + + /// The profit of a priority order is calculated a bit differently + /// Rationale: + /// - we will always bid the base fee + /// - since we have to provide 1 MP (1/1000th of a bp) for every wei of priority fee + /// - we return the data needed to calculate the maximum MPS of improvement we can offer from our quote and the order specs + fn get_execution_metadata( + &self, + RoutedOrder { request, route }: &RoutedOrder, + ) -> Option { + let quote = U256::from_str_radix(&route.quote, 10).ok()?; + let amount_out_required = + U256::from_str_radix(&request.amount_out_required.to_string(), 10).ok()?; + if quote.le(&amount_out_required) { + return None; + } + + return Some({ + ExecutionMetadata { + quote, + amount_out_required, + } + }); + } + + fn update_order_state(&mut self, order: PriorityOrder, signature: String, order_hash: String) { + let resolved = order.resolve( + self.last_block_number, + self.last_block_timestamp + BLOCK_TIME, + Uint::from(0), + ); + let order_status: OrderStatus = match resolved { + OrderResolution::Expired => OrderStatus::Done, + OrderResolution::Invalid => OrderStatus::Done, + OrderResolution::NotFillableYet => OrderStatus::NotFillableYet, // TODO: gracefully handle this, currently this will cause a revert if we try to fill too earlty + OrderResolution::Resolved(resolved_order) => OrderStatus::Open(resolved_order), + }; + + match order_status { + OrderStatus::Done => { + self.mark_as_done(&order_hash); + } + OrderStatus::NotFillableYet => { + info!("Order not fillable yet, skipping: {}", order_hash); + } + OrderStatus::Open(resolved_order) => { + if self.done_orders.contains_key(&order_hash) { + info!("Order already done, skipping: {}", order_hash); + return; + } + if !self.open_orders.contains_key(&order_hash) { + info!("Adding new order {}", order_hash); + } + self.open_orders.insert( + order_hash.clone(), + OrderData { + order: Order::PriorityOrder(order), + hash: order_hash, + signature, + resolved: resolved_order, + }, + ); + } + } + } + + fn prune_done_orders(&mut self) { + let mut to_remove = Vec::new(); + for (order_hash, deadline) in self.done_orders.iter() { + if *deadline < self.last_block_timestamp { + to_remove.push(order_hash.clone()); + } + } + for order_hash in to_remove { + self.done_orders.remove(&order_hash); + } + } + + fn update_open_orders(&mut self) { + // TODO: this is nasty, plz cleanup + let binding = self.open_orders.clone(); + let order_hashes: Vec<(&String, &OrderData)> = binding.iter().collect(); + for (order_hash, order_data) in order_hashes { + match &order_data.order { + Order::PriorityOrder(order) => { + self.update_order_state( + order.clone(), + order_data.signature.clone(), + order_hash.clone().to_string(), + ); + } + _ => { + error!("Invalid order type"); + } + } + } + } + + fn mark_as_done(&mut self, order: &str) { + if self.open_orders.contains_key(order) { + self.open_orders.remove(order); + } + if !self.done_orders.contains_key(order) { + self.done_orders + .insert(order.to_string(), self.last_block_timestamp + DONE_EXPIRY); + } + } +} diff --git a/src/strategies/shared.rs b/src/strategies/shared.rs new file mode 100644 index 0000000..637945f --- /dev/null +++ b/src/strategies/shared.rs @@ -0,0 +1,123 @@ +use crate::collectors::uniswapx_route_collector::RoutedOrder; +use anyhow::Result; +use async_trait::async_trait; +use bindings_uniswapx::{ + erc20::ERC20, shared_types::SignedOrder, swap_router_02_executor::SwapRouter02Executor, +}; +use ethers::{ + abi::{ethabi, ParamType, Token}, + providers::Middleware, + types::{transaction::eip2718::TypedTransaction, Address, Bytes, H160, U256}, +}; +use std::sync::Arc; +use std::{ + str::FromStr, + time::{SystemTime, UNIX_EPOCH}, +}; + +const REACTOR_ADDRESS: &str = "0x00000011F84B9aa48e5f8aA8B9897600006289Be"; +const SWAPROUTER_02_ADDRESS: &str = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"; +pub const WETH_ADDRESS: &str = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; + +#[async_trait] +pub trait UniswapXStrategy { + // builds a transaction to fill an order + async fn build_fill( + &self, + client: Arc, + executor_address: &str, + signed_orders: Vec, + RoutedOrder { request, route }: RoutedOrder, + ) -> Result { + let chain_id: U256 = client.get_chainid().await?; + let fill_contract = + SwapRouter02Executor::new(H160::from_str(executor_address)?, client.clone()); + + let token_in: H160 = H160::from_str(&request.token_in)?; + let token_out: H160 = H160::from_str(&request.token_out)?; + + let swaprouter_02_approval = self + .get_tokens_to_approve( + client.clone(), + token_in, + &executor_address, + SWAPROUTER_02_ADDRESS, + ) + .await?; + + let reactor_approval = self + .get_tokens_to_approve( + client.clone(), + token_out, + &executor_address, + REACTOR_ADDRESS, + ) + .await?; + + // Strip off function selector + let multicall_bytes = &route.method_parameters.calldata[10..]; + + // Decode multicall into [Uint256, bytes[]] (deadline, multicallData) + let decoded_multicall_bytes = ethabi::decode( + &[ + ParamType::Uint(256), + ParamType::Array(Box::new(ParamType::Bytes)), + ], + &Bytes::from_str(multicall_bytes) + .ok() + .expect("Failed to decode multicall bytes"), + ); + + let decoded_multicall_bytes = match decoded_multicall_bytes { + Ok(data) => data[1].clone(), // already in bytes[] + Err(e) => { + return Err(anyhow::anyhow!("Failed to decode multicall bytes: {}", e)); + } + }; + + // abi encode as [tokens to approve to swap router 02, tokens to approve to reactor, multicall data] + // [address[], address[], bytes[]] + let calldata = ethabi::encode(&[ + Token::Array(swaprouter_02_approval), + Token::Array(reactor_approval), + decoded_multicall_bytes, + ]); + let mut call = fill_contract.execute_batch(signed_orders, Bytes::from(calldata)); + Ok(call.tx.set_chain_id(chain_id.as_u64()).clone()) + } + + fn current_timestamp(&self) -> Result { + let start = SystemTime::now(); + Ok(start.duration_since(UNIX_EPOCH)?.as_secs()) + } + + async fn get_tokens_to_approve( + &self, + client: Arc, + token: Address, + from: &str, + to: &str, + ) -> Result, anyhow::Error> { + if token == Address::zero() { + return Ok(vec![]); + } + let token_contract = ERC20::new(token, client.clone()); + let allowance = token_contract + .allowance( + H160::from_str(from) + .ok() + .expect("Error encoding from address"), + H160::from_str(to) + .ok() + .expect("Error encoding from address"), + ) + .await + .ok() + .expect("Failed to get allowance"); + if allowance < U256::MAX / 2 { + Ok(vec![Token::Address(token)]) + } else { + Ok(vec![]) + } + } +} diff --git a/src/strategies/types.rs b/src/strategies/types.rs index ed78d5d..3469cd9 100644 --- a/src/strategies/types.rs +++ b/src/strategies/types.rs @@ -3,6 +3,9 @@ use crate::collectors::{ uniswapx_route_collector::RoutedOrder, }; use artemis_core::executors::mempool_executor::SubmitTxToMempool; +use uniswapx_rs::order::ResolvedOrder; + +use super::priority_strategy::ExecutionMetadata; /// Core Event enum for the current strategy. #[derive(Debug, Clone)] @@ -12,14 +15,35 @@ pub enum Event { UniswapXRoute(Box), } +#[derive(Debug, Clone)] +pub struct SubmitTxToMempoolWithExecutionMetadata { + pub execution: SubmitTxToMempool, + pub metadata: ExecutionMetadata, +} + /// Core Action enum for the current strategy. #[derive(Debug, Clone)] pub enum Action { SubmitTx(SubmitTxToMempool), + SubmitPublicTx(SubmitTxToMempoolWithExecutionMetadata), } /// Configuration for variables we need to pass to the strategy. #[derive(Debug, Clone)] pub struct Config { pub bid_percentage: u64, + pub executor_address: String, +} + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct TokenInTokenOut { + pub token_in: String, + pub token_out: String, +} + +#[derive(Debug, Clone)] +pub enum OrderStatus { + Open(ResolvedOrder), + NotFillableYet, + Done, } diff --git a/src/strategies/uniswapx_strategy.rs b/src/strategies/uniswapx_strategy.rs index 97ad251..7ee53c0 100644 --- a/src/strategies/uniswapx_strategy.rs +++ b/src/strategies/uniswapx_strategy.rs @@ -1,7 +1,10 @@ -use super::types::Config; +use super::{ + shared::{UniswapXStrategy, WETH_ADDRESS}, + types::{Config, OrderStatus, TokenInTokenOut}, +}; use crate::collectors::{ block_collector::NewBlock, - uniswapx_order_collector::{UniswapXOrder, CHAIN_ID}, + uniswapx_order_collector::UniswapXOrder, uniswapx_route_collector::{OrderBatchData, OrderData, RoutedOrder}, }; use alloy_primitives::Uint; @@ -9,49 +12,33 @@ use anyhow::Result; use artemis_core::executors::mempool_executor::{GasBidInfo, SubmitTxToMempool}; use artemis_core::types::Strategy; use async_trait::async_trait; -use bindings_uniswapx::{ - exclusive_dutch_order_reactor::ExclusiveDutchOrderReactor, shared_types::SignedOrder, -}; +use bindings_uniswapx::shared_types::SignedOrder; use ethers::{ - abi::{ethabi, AbiEncode, Token}, providers::Middleware, - types::{transaction::eip2718::TypedTransaction, Address, Bytes, Filter, H160, U256}, + types::{Address, Bytes, Filter, U256}, + utils::hex, }; -use std::collections::HashMap; +use std::error::Error; use std::str::FromStr; use std::sync::Arc; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::{collections::HashMap, fmt::Debug}; use tokio::sync::mpsc::{Receiver, Sender}; use tracing::{error, info}; -use uniswapx_rs::order::{ - decode_order, encode_order, ExclusiveDutchOrder, OrderResolution, ResolvedOrder, -}; +use uniswapx_rs::order::{Order, OrderResolution, V2DutchOrder}; use super::types::{Action, Event}; const BLOCK_TIME: u64 = 12; const DONE_EXPIRY: u64 = 300; -const REACTOR_ADDRESS: &str = "0xe80bF394d190851E215D5F67B67f8F5A52783F1E"; -pub const WETH_ADDRESS: &str = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; -pub const EXECUTOR_ADDRESS: &str = "TODO: Fill in swaprouter02 executor address"; - -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -struct TokenInTokenOut { - token_in: String, - token_out: String, -} - -#[derive(Debug, Clone)] -enum OrderStatus { - Open(ResolvedOrder), - Done, -} +const REACTOR_ADDRESS: &str = "0x00000011F84B9aa48e5f8aA8B9897600006289Be"; #[derive(Debug)] #[allow(dead_code)] pub struct UniswapXUniswapFill { /// Ethers client. client: Arc, + /// executor address + executor_address: String, /// Amount of profits to bid in gas bid_percentage: u64, last_block_number: u64, @@ -75,6 +62,7 @@ impl UniswapXUniswapFill { Self { client, + executor_address: config.executor_address, bid_percentage: config.bid_percentage, last_block_number: 0, last_block_timestamp: 0, @@ -105,14 +93,28 @@ impl Strategy for UniswapXUniswapFill } } +impl UniswapXStrategy for UniswapXUniswapFill {} + impl UniswapXUniswapFill { + fn decode_order(&self, encoded_order: &str) -> Result> { + let encoded_order = if encoded_order.starts_with("0x") { + &encoded_order[2..] + } else { + encoded_order + }; + let order_hex: Vec = hex::decode(encoded_order)?; + + Ok(V2DutchOrder::decode_inner(&order_hex, false)?) + } + // Process new orders as they come in. async fn process_order_event(&mut self, event: UniswapXOrder) -> Option { if self.last_block_timestamp == 0 { return None; } - let order = decode_order(&event.encoded_order) + let order = self + .decode_order(&event.encoded_order) .map_err(|e| error!("failed to decode: {}", e)) .ok()?; @@ -141,13 +143,21 @@ impl UniswapXUniswapFill { info!( "Sending trade: num trades: {} routed quote: {}, batch needs: {}, profit: {} wei", orders.len(), - event.route.quote, + event.route.quote_gas_adjusted, amount_out_required, profit ); - + let signed_orders = self.get_signed_orders(orders.clone()).ok()?; return Some(Action::SubmitTx(SubmitTxToMempool { - tx: self.build_fill(event).ok()?, + tx: self + .build_fill( + self.client.clone(), + &self.executor_address, + signed_orders, + event, + ) + .await + .ok()?, gas_bid_info: Some(GasBidInfo { bid_percentage: self.bid_percentage, total_profit: profit, @@ -185,31 +195,23 @@ impl UniswapXUniswapFill { None } - // builds a transaction to fill an order - fn build_fill(&self, RoutedOrder { request, route }: RoutedOrder) -> Result { - let reactor = - ExclusiveDutchOrderReactor::new(H160::from_str(REACTOR_ADDRESS)?, self.client.clone()); + /// encode orders into generic signed orders + fn get_signed_orders(&self, orders: Vec) -> Result> { let mut signed_orders: Vec = Vec::new(); - for batch in request.orders.iter() { - let OrderData { - order, signature, .. - } = batch; - signed_orders.push(SignedOrder { - order: Bytes::from(encode_order(order)), - sig: Bytes::from_str(signature)?, - }); + for batch in orders.iter() { + match &batch.order { + Order::V2DutchOrder(order) => { + signed_orders.push(SignedOrder { + order: Bytes::from(order.encode_inner()), + sig: Bytes::from_str(&batch.signature)?, + }); + } + _ => { + return Err(anyhow::anyhow!("Invalid order type")); + } + } } - // abi encode as [tokens to approve, multicall data] - let calldata = ethabi::encode(&[ - Token::Array(vec![Token::Address(H160::from_str(&request.token_in)?)]), - Token::Bytes(Bytes::from_str(&route.method_parameters.calldata)?.encode()), - ]); - let mut call = reactor.execute_batch( - signed_orders, - H160::from_str(EXECUTOR_ADDRESS)?, - Bytes::from(calldata), - ); - Ok(call.tx.set_chain_id(CHAIN_ID).clone()) + Ok(signed_orders) } fn get_order_batches(&self) -> HashMap { @@ -293,11 +295,18 @@ impl UniswapXUniswapFill { let binding = self.open_orders.clone(); let order_hashes: Vec<(&String, &OrderData)> = binding.iter().collect(); for (order_hash, order_data) in order_hashes { - self.update_order_state( - order_data.order.clone(), - order_data.signature.clone(), - order_hash.clone().to_string(), - ); + match &order_data.order { + Order::V2DutchOrder(order) => { + self.update_order_state( + order.clone(), + order_data.signature.clone(), + order_hash.clone().to_string(), + ); + } + _ => { + error!("Invalid order type"); + } + } } } @@ -311,17 +320,13 @@ impl UniswapXUniswapFill { } } - fn update_order_state( - &mut self, - order: ExclusiveDutchOrder, - signature: String, - order_hash: String, - ) { + fn update_order_state(&mut self, order: V2DutchOrder, signature: String, order_hash: String) { let resolved = order.resolve(self.last_block_timestamp + BLOCK_TIME); let order_status: OrderStatus = match resolved { OrderResolution::Expired => OrderStatus::Done, OrderResolution::Invalid => OrderStatus::Done, OrderResolution::Resolved(resolved_order) => OrderStatus::Open(resolved_order), + _ => OrderStatus::Done, }; match order_status { @@ -339,21 +344,18 @@ impl UniswapXUniswapFill { self.open_orders.insert( order_hash.clone(), OrderData { - order, + order: Order::V2DutchOrder(order), hash: order_hash, signature, resolved: resolved_order, }, ); } + // Noop + _ => {} } } - fn current_timestamp(&self) -> Result { - let start = SystemTime::now(); - Ok(start.duration_since(UNIX_EPOCH)?.as_secs()) - } - fn get_profit_eth(&self, RoutedOrder { request, route }: &RoutedOrder) -> Option { let quote = U256::from_str_radix(&route.quote, 10).ok()?; let amount_out_required =