diff --git a/Cargo.lock b/Cargo.lock index 3983107bf..8b302db2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4920,6 +4920,28 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "cfg-if", + "miette-derive", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "mime" version = "0.3.17" @@ -8457,6 +8479,7 @@ dependencies = [ "http 1.3.1", "itertools 0.10.5", "lazy_static", + "miette", "mini-moka", "mockall", "num-bigint", diff --git a/Cargo.toml b/Cargo.toml index a20a44a75..950f9ea59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ hex = "0.4.3" chrono = { version = "0.4.26", features = ["serde"] } # Error handling +miette = "7.6.0" thiserror = "1" # Async & concurrency @@ -80,8 +81,8 @@ approx = "0.5.1" rstest = "0.23.0" rstest_reuse = "0.7.0" tracing-subscriber = { version = "0.3.17", default-features = false, features = [ - "env-filter", - "fmt", + "env-filter", + "fmt", ] } tempfile = "3.13.0" @@ -103,7 +104,7 @@ tycho-execution = "0.118.0" default = ["evm", "rfq"] network_tests = [] evm = [ - "dep:foundry-config", "dep:foundry-evm", "dep:revm", "dep:revm-inspectors", "dep:alloy", + "dep:foundry-config", "dep:foundry-evm", "dep:revm", "dep:revm-inspectors", "dep:alloy", ] rfq = ["dep:reqwest", "dep:async-trait", "dep:tokio-tungstenite", "dep:async-stream", "dep:http", "dep:prost"] diff --git a/examples/rfq_quickstart/cli.rs b/examples/rfq_quickstart/cli.rs new file mode 100644 index 000000000..2538a317c --- /dev/null +++ b/examples/rfq_quickstart/cli.rs @@ -0,0 +1,62 @@ +use clap::{Args, Parser}; +use miette::{miette, Result}; +use tycho_common::models::Chain; + +#[derive(Clone, Debug, Parser)] +pub struct RfqCommand { + #[command(flatten)] + pub swap_args: SwapArgs, +} + +#[derive(Clone, Debug, Args, Default)] +pub struct SwapArgs { + #[arg(long)] + pub sell_token: Option, + #[arg(long)] + pub buy_token: Option, + #[arg(long, default_value_t = 10.0)] + pub sell_amount: f64, + /// The minimum TVL threshold for RFQ quotes in USD + #[arg(long, default_value_t = 1000.0)] + pub tvl_threshold: f64, + #[arg(long, default_value = "ethereum")] + pub chain: Chain, +} + +impl SwapArgs { + fn parse_args(mut self) -> Result { + // By default, we request quotes for USDC to WETH on whatever chain we choose + if self.buy_token.is_none() { + self.buy_token = Some(match self.chain { + Chain::Ethereum => "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".to_string(), + Chain::Base => "0x4200000000000000000000000000000000000006".to_string(), + _ => { + return Err(miette!( + "RFQ quickstart does not yet support chain {chain}", + chain = self.chain + )) + } + }); + } + if self.sell_token.is_none() { + self.sell_token = Some(match self.chain { + Chain::Ethereum => "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".to_string(), + Chain::Base => "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913".to_string(), + _ => { + return Err(miette!( + "RFQ quickstart does not yet support chain {chain}", + chain = self.chain + )) + } + }); + } + Ok(self) + } +} + +impl RfqCommand { + pub async fn parse_args(mut self) -> Result { + self.swap_args = self.swap_args.parse_args()?; + Ok(self) + } +} diff --git a/examples/rfq_quickstart/env.rs b/examples/rfq_quickstart/env.rs new file mode 100644 index 000000000..b014b49f2 --- /dev/null +++ b/examples/rfq_quickstart/env.rs @@ -0,0 +1,26 @@ +use std::{env, env::VarError}; + +use miette::{miette, Result}; + +fn _get_env(var_name: &str) -> Result> { + match env::var(var_name) { + Ok(val) => { + Ok(Some(val)) + }, + Err(e) => match e { + VarError::NotPresent => Ok(None), + VarError::NotUnicode(_) => Err(miette!("The environment variable `{var_name}` cannot be decoded because it is not some valid Unicode")), + }, + } +} + +pub fn get_env(var_name: &str) -> Option { + _get_env(var_name).ok().flatten() +} + +pub fn get_env_with_default(var_name: &str, default_value: String) -> String { + _get_env(var_name) + .ok() + .flatten() + .unwrap_or(default_value) +} diff --git a/examples/rfq_quickstart/main.rs b/examples/rfq_quickstart/main.rs index 350ad8a02..9a5ae1397 100644 --- a/examples/rfq_quickstart/main.rs +++ b/examples/rfq_quickstart/main.rs @@ -1,24 +1,20 @@ -use std::{collections::HashSet, env, str::FromStr, sync::Arc}; +mod cli; +mod env; +mod rfq_stream; +mod swap_executor; +mod tycho_client; +mod utils; + +use std::{str::FromStr, sync::Arc}; use alloy::{ - eips::BlockNumberOrTag, - network::{Ethereum, EthereumWallet}, - primitives::{Address, Bytes as AlloyBytes, Keccak256, Signature, TxKind, B256, U256}, - providers::{ - fillers::{FillProvider, JoinFill, WalletFiller}, - Identity, Provider, ProviderBuilder, RootProvider, - }, - rpc::types::{ - simulate::{SimBlock, SimulatePayload}, - TransactionInput, TransactionRequest, - }, + primitives::{Address, Keccak256, Signature}, signers::{local::PrivateKeySigner, SignerSync}, sol_types::{eip712_domain, SolStruct, SolValue}, }; use clap::Parser; -use dialoguer::{theme::ColorfulTheme, Select}; use dotenv::dotenv; -use foundry_config::NamedChain; +use miette::{IntoDiagnostic, Result}; use num_bigint::BigUint; use num_traits::ToPrimitive; use tokio::sync::mpsc; @@ -26,637 +22,64 @@ use tracing_subscriber::EnvFilter; use tycho_common::{models::token::Token, simulation::protocol_sim::ProtocolSim, Bytes}; use tycho_execution::encoding::{ errors::EncodingError, - evm::{approvals::permit2::PermitSingle, encoder_builders::TychoRouterEncoderBuilder}, + evm::approvals::permit2::PermitSingle, models, - models::{EncodedSolution, Solution, SwapBuilder, Transaction, UserTransferType}, + models::{EncodedSolution, Solution, SwapBuilder, Transaction}, }; use tycho_simulation::{ evm::protocol::u256_num::biguint_to_u256, protocol::models::{ProtocolComponent, Update}, - rfq::{ - protocols::bebop::{client_builder::BebopClientBuilder, state::BebopState}, - stream::RFQStreamBuilder, - }, - tycho_common::models::Chain, - utils::{get_default_url, load_all_tokens}, }; -#[derive(Parser)] -struct Cli { - #[arg(long)] - sell_token: Option, - #[arg(long)] - buy_token: Option, - #[arg(long, default_value_t = 10.0)] - sell_amount: f64, - /// The minimum TVL threshold for RFQ quotes in USD - #[arg(long, default_value_t = 1000.0)] - tvl_threshold: f64, - #[arg(long, default_value = "ethereum")] - chain: Chain, -} - -impl Cli { - fn with_defaults(mut self) -> Self { - // By default, we request quotes for USDC to WETH on whatever chain we choose - - if self.buy_token.is_none() { - self.buy_token = Some(match self.chain { - Chain::Ethereum => "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".to_string(), - Chain::Base => "0x4200000000000000000000000000000000000006".to_string(), - _ => { - panic!("RFQ quickstart does not yet support chain {chain}", chain = self.chain) - } - }); - } - - if self.sell_token.is_none() { - self.sell_token = Some(match self.chain { - Chain::Ethereum => "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".to_string(), - Chain::Base => "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913".to_string(), - _ => { - panic!("RFQ quickstart does not yet support chain {chain}", chain = self.chain) - } - }); - } - - self - } -} +use crate::{ + cli::RfqCommand, + env::get_env, + rfq_stream::{RFQStreamClient, RFQStreamProcessor}, + tycho_client::TychoClient, +}; #[tokio::main] -async fn main() { - tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .with_target(false) - .init(); - - let cli = Cli::parse().with_defaults(); - - let chain = cli.chain; - - dotenv().expect("Missing .env file"); - let tycho_url = env::var("TYCHO_URL").unwrap_or_else(|_| { - get_default_url(&chain) - .unwrap_or_else(|| panic!("Unknown URL for chain {chain}", chain = cli.chain)) - }); - - let tycho_api_key: String = - env::var("TYCHO_API_KEY").unwrap_or_else(|_| "sampletoken".to_string()); - - // Get credentials for any RFQ(s) we are using - let bebop_user = env::var("BEBOP_USER") - .expect("BEBOP_USER environment variable is required. Contact Bebop for credentials."); - let bebop_key = env::var("BEBOP_KEY") - .expect("BEBOP_KEY environment variable is required. Contact Bebop for credentials."); - - println!("Loading tokens from Tycho... {url}", url = tycho_url.as_str()); - let all_tokens = - load_all_tokens(tycho_url.as_str(), false, Some(tycho_api_key.as_str()), chain, None, None) - .await; - println!("Tokens loaded: {num}", num = all_tokens.len()); - - let sell_token_address = Bytes::from_str( - &cli.sell_token - .expect("Sell token not provided"), - ) - .expect("Invalid address for sell token"); - let buy_token_address = Bytes::from_str( - &cli.buy_token - .expect("Buy token not provided"), - ) - .expect("Invalid address for buy token"); - let sell_token = all_tokens - .get(&sell_token_address) - .expect("Sell token not found") - .clone(); - let buy_token = all_tokens - .get(&buy_token_address) - .expect("Buy token not found") - .clone(); - let amount_in = - BigUint::from((cli.sell_amount * 10f64.powi(sell_token.decimals as i32)) as u128); - - println!( - "Looking for RFQ quotes for {amount} {sell_symbol} -> {buy_symbol} on {chain:?}", - amount = cli.sell_amount, - sell_symbol = sell_token.symbol, - buy_symbol = buy_token.symbol - ); - - let swapper_pk = env::var("PRIVATE_KEY").ok(); - // Initialize the encoder - let encoder = TychoRouterEncoderBuilder::new() - .chain(chain) - .user_transfer_type(UserTransferType::TransferFromPermit2) - .build() - .expect("Failed to build encoder"); +async fn main() -> Result<()> { + dotenv().into_diagnostic()?; + setup_tracing(); - // Set up RFQ client using the builder pattern - let mut rfq_tokens = HashSet::new(); - rfq_tokens.insert(sell_token_address.clone()); - rfq_tokens.insert(buy_token_address.clone()); + let cli = RfqCommand::parse().parse_args().await?; - println!("Connecting to RFQ WebSocket..."); - let bebop_client = BebopClientBuilder::new(chain, bebop_user, bebop_key) - .tokens(rfq_tokens) - .tvl_threshold(cli.tvl_threshold) - .build() - .expect("Failed to create RFQ clients"); + let tycho_client = TychoClient::from_env(cli.swap_args.chain)?; + let all_tokens = tycho_client.load_tokens().await; + let (sell_token, buy_token, amount_in) = + tycho_client.get_token_info(&cli.swap_args, &all_tokens)?; - let (tx, mut rx) = mpsc::channel::(100); - - let rfq_stream_builder = RFQStreamBuilder::new() - .add_client::("bebop", Box::new(bebop_client)) + let rfq_stream_builder = RFQStreamClient::new() + .add_bebop(cli.swap_args.chain, cli.swap_args.tvl_threshold, &sell_token, &buy_token)? + // .add_hashflow(&cli.swap_args) .set_tokens(all_tokens.clone()) - .await; - + .await? + .finish(); + let (tx, mut rx) = mpsc::channel::(100); println!("Connected to RFQs! Streaming live price levels...\n"); - - // Start the RFQ stream in a background task tokio::spawn(rfq_stream_builder.build(tx)); - // Stream quotes from RFQ stream - while let Some(update) = rx.recv().await { - // Drain any additional buffered messages to get the most recent one - // - // ⚠️Warning: This works fine only if you assume that this message is entirely - // representative of the current state, as done in this quickstart. - // You should comment out this code portion if you would like to manually track removed - // components. - let mut latest_update = update; - let mut drained_count = 0; - while let Ok(newer_update) = - tokio::time::timeout(std::time::Duration::from_millis(10), rx.recv()).await - { - if let Some(newer_update) = newer_update { - latest_update = newer_update; - drained_count += 1; - } else { - break; - } - } - if drained_count > 0 { - println!( - "Fast-forwarded through {drained_count} older RFQ updates to get latest prices" - ); - } - let update = latest_update; - - println!( - "Received RFQ price levels with {} new pairs for block/timestamp {}", - &update.states.len(), - update.block_number_or_timestamp - ); - - // Process state updates - for (comp_id, state) in &update.states { - if let Some(component) = update.new_pairs.get(comp_id) { - let tokens = &component.tokens; - - // Check if this component trades our desired pair - if HashSet::from([&sell_token, &buy_token]) - .is_subset(&HashSet::from_iter(tokens.iter())) - { - // Try to calculate amount out using the state - if let Ok(amount_out_result) = - state.get_amount_out(amount_in.clone(), &sell_token, &buy_token) - { - let amount_out = amount_out_result.amount; - - println!( - "Best indicative price for swap {}: {} {} -> {} {}", - component.protocol_system, - format_token_amount(&amount_in, &sell_token), - sell_token.symbol, - format_token_amount(&amount_out, &buy_token), - buy_token.symbol - ); - - // Clone expected_amount to avoid ownership issues - let expected_amount_copy = amount_out.clone(); - - // Check if we have a private key first - if swapper_pk.is_none() { - println!( - "\nSigner private key was not provided. Skipping simulation/execution. Set PRIVATE_KEY env variable to perform simulation/execution.\n" - ); - continue; - } - - // Create signer and provider now that we know we have a private key - let pk_str = swapper_pk.as_ref().unwrap(); - let pk = - B256::from_str(pk_str).expect("Failed to convert swapper pk to B256"); - let signer = PrivateKeySigner::from_bytes(&pk) - .expect("Failed to create PrivateKeySigner"); - let tx_signer = EthereumWallet::from(signer.clone()); - let provider = ProviderBuilder::default() - .with_chain(NamedChain::try_from(chain.id()).expect("Invalid chain")) - .wallet(tx_signer) - .connect(&env::var("RPC_URL").expect("RPC_URL env var not set")) - .await - .expect("Failed to connect provider"); - - // Print token balances before showing the swap options - match get_token_balance( - &provider, - Address::from_slice(&sell_token.address), - signer.address(), - Address::from_slice(&chain.native_token().address), - ) - .await - { - Ok(balance) => { - let formatted_balance = format_token_amount(&balance, &sell_token); - println!( - "\nYour balance: {formatted_balance} {sell_symbol}", - sell_symbol = sell_token.symbol - ); - - if balance < amount_in { - let required = format_token_amount(&amount_in, &sell_token); - println!("⚠️ Warning: Insufficient balance for swap. You have {formatted_balance} {sell_symbol} but need {required} {sell_symbol}", - formatted_balance = formatted_balance, - sell_symbol = sell_token.symbol, - ); - continue; - } - } - Err(e) => eprintln!("Failed to get token balance: {e}"), - } - - // Also show buy token balance - match get_token_balance( - &provider, - Address::from_slice(&buy_token.address), - signer.address(), - Address::from_slice(&chain.native_token().address), - ) - .await - { - Ok(balance) => { - let formatted_balance = format_token_amount(&balance, &buy_token); - println!( - "Your {buy_symbol} balance: {formatted_balance} {buy_symbol}", - buy_symbol = buy_token.symbol - ); - } - Err(e) => eprintln!( - "Failed to get {buy_symbol} balance: {e}", - buy_symbol = buy_token.symbol - ), - } - - println!("Would you like to simulate or execute this swap?"); - println!("Please be aware that the market might move while you make your decision, which might lead to a revert if you've set a min amount out or slippage."); - println!( - "Warning: slippage is set to 0.25% during execution by default.\n" - ); - let options = - vec!["Simulate the swap", "Execute the swap", "Skip this swap"]; - let selection = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("What would you like to do?") - .default(0) - .items(&options) - .interact() - .unwrap_or(2); // Default to skip if error - - let choice = match selection { - 0 => "simulate", - 1 => "execute", - _ => "skip", - }; - - match choice { - "simulate" => { - println!("\nSimulating RFQ swap..."); - println!("Step 1: Encoding the permit2 transaction..."); - let approve_function_signature = "approve(address,uint256)"; - let args = ( - Address::from_str("0x000000000022D473030F116dDEE9F6B43aC78BA3") - .expect("Couldn't convert to address"), - biguint_to_u256(&amount_in), - ); - let approval_data = - encode_input(approve_function_signature, args.abi_encode()); - let nonce = provider - .get_transaction_count(signer.address()) - .await - .expect("Failed to get nonce"); - let block = provider - .get_block_by_number(BlockNumberOrTag::Latest) - .await - .expect("Failed to fetch latest block") - .expect("Block not found"); - let base_fee = block - .header - .base_fee_per_gas - .expect("Base fee not available"); - let max_priority_fee_per_gas = 1_000_000_000u64; - let max_fee_per_gas = base_fee + max_priority_fee_per_gas; - - let approval_request = TransactionRequest { - to: Some(TxKind::Call(Address::from_slice( - &sell_token_address, - ))), - from: Some(signer.address()), - value: None, - input: TransactionInput { - input: Some(AlloyBytes::from(approval_data)), - data: None, - }, - gas: Some(100_000u64), - chain_id: Some(chain.id()), - max_fee_per_gas: Some(max_fee_per_gas.into()), - max_priority_fee_per_gas: Some(max_priority_fee_per_gas.into()), - nonce: Some(nonce), - ..Default::default() - }; - - println!("Step 2: Encoding the solution transaction..."); - - let solution = create_solution( - component.clone(), - Arc::from(state.clone_box()), - sell_token.clone(), - buy_token.clone(), - amount_in.clone(), - Bytes::from(signer.address().to_vec()), - amount_out.clone(), - ); - - let encoded_solution = encoder - .encode_solutions(vec![solution.clone()]) - .expect("Failed to encode router calldata")[0] - .clone(); - - let tx = encode_tycho_router_call( - chain.id(), - encoded_solution.clone(), - &solution, - chain.native_token().address, - signer.clone(), - ) - .expect("Failed to encode router call"); - - let swap_request = TransactionRequest { - to: Some(TxKind::Call(Address::from_slice(&tx.to))), - from: Some(signer.address()), - value: Some(biguint_to_u256(&tx.value)), - input: TransactionInput { - input: Some(AlloyBytes::from(tx.data)), - data: None, - }, - gas: Some(800_000u64), - chain_id: Some(chain.id()), - max_fee_per_gas: Some(max_fee_per_gas.into()), - max_priority_fee_per_gas: Some(max_priority_fee_per_gas.into()), - nonce: Some(nonce + 1), - ..Default::default() - }; - - println!("Step 3: Simulating approval and solution transactions together..."); - let approval_payload = SimulatePayload { - block_state_calls: vec![SimBlock { - block_overrides: None, - state_overrides: None, - calls: vec![approval_request.clone(), swap_request], - }], - trace_transfers: true, - validation: true, - return_full_transactions: true, - }; - - match provider - .simulate(&approval_payload) - .await - { - Ok(output) => { - let mut all_successful = true; - for block in output.iter() { - println!( - "\nSimulated Block {block_num}:", - block_num = block.inner.header.number - ); - for transaction in block.calls.iter() { - println!( - " RFQ Swap: Status: {status:?}, Gas Used: {gas_used}", - status = transaction.status, - gas_used = transaction.gas_used - ); - if !transaction.status { - all_successful = false; - } - } - } - - if all_successful { - println!("\n✅ Simulation successful!"); - } else { - println!("\n❌ Simulation failed! One or more transactions reverted."); - println!("Consider adjusting parameters and re-simulating before execution."); - } - println!(); - continue; - } - Err(e) => { - eprintln!("\n❌ Simulation failed: {e:?}"); - println!("Your RPC provider does not support transaction simulation. Consider proceeding with execution instead or switching RPC provider."); - } - } - } - "execute" => { - println!("\nExecuting RFQ swap..."); - - // Step 1: Send permit2 approval first - println!("Step 1: Sending permit2 approval..."); - let approve_function_signature = "approve(address,uint256)"; - let args = ( - Address::from_str("0x000000000022D473030F116dDEE9F6B43aC78BA3") - .expect("Couldn't convert to address"), - biguint_to_u256(&amount_in), - ); - let approval_data = - encode_input(approve_function_signature, args.abi_encode()); - let nonce = provider - .get_transaction_count(signer.address()) - .await - .expect("Failed to get nonce"); - - let block = provider - .get_block_by_number(BlockNumberOrTag::Latest) - .await - .expect("Failed to fetch latest block") - .expect("Block not found"); - - let base_fee = block - .header - .base_fee_per_gas - .expect("Base fee not available"); - let max_priority_fee_per_gas = 1_000_000_000u64; - let max_fee_per_gas = base_fee + max_priority_fee_per_gas; - - let approval_request = TransactionRequest { - to: Some(TxKind::Call(Address::from_slice( - &sell_token_address, - ))), - from: Some(signer.address()), - value: None, - input: TransactionInput { - input: Some(AlloyBytes::from(approval_data)), - data: None, - }, - gas: Some(100_000u64), - chain_id: Some(chain.id()), - max_fee_per_gas: Some(max_fee_per_gas.into()), - max_priority_fee_per_gas: Some(max_priority_fee_per_gas.into()), - nonce: Some(nonce), - ..Default::default() - }; - - let approval_receipt = match provider - .send_transaction(approval_request) - .await - { - Ok(receipt) => receipt, - Err(e) => { - eprintln!("\nFailed to send approval transaction: {e:?}\n"); - continue; - } - }; - - let approval_result = match approval_receipt.get_receipt().await { - Ok(result) => result, - Err(e) => { - eprintln!("\nFailed to get approval receipt: {e:?}\n"); - continue; - } - }; - - println!( - "Approval transaction sent with hash: {hash:?} and status: {status:?}", - hash = approval_result.transaction_hash, - status = approval_result.status() - ); - - if !approval_result.status() { - eprintln!("\nApproval transaction failed! Cannot proceed with swap.\n"); - continue; - } - - println!("Step 2: Encoding solution transaction..."); - - let solution = create_solution( - component.clone(), - Arc::from(state.clone_box()), - sell_token.clone(), - buy_token.clone(), - amount_in.clone(), - Bytes::from(signer.address().to_vec()), - amount_out.clone(), - ); - - // Encode the swaps of the solution - let encoded_solution = encoder - .encode_solutions(vec![solution.clone()]) - .expect("Failed to encode router calldata")[0] - .clone(); - - let swap_tx = encode_tycho_router_call( - chain.id(), - encoded_solution.clone(), - &solution, - chain.native_token().address, - signer.clone(), - ) - .expect("Failed to encode router call"); - - let swap_request = TransactionRequest { - to: Some(TxKind::Call(Address::from_slice(&swap_tx.to))), - from: Some(signer.address()), - value: Some(biguint_to_u256(&swap_tx.value)), - input: TransactionInput { - input: Some(AlloyBytes::from(swap_tx.data)), - data: None, - }, - gas: Some(800_000u64), - chain_id: Some(chain.id()), - max_fee_per_gas: Some(max_fee_per_gas.into()), - max_priority_fee_per_gas: Some(max_priority_fee_per_gas.into()), - nonce: Some(nonce + 1), - ..Default::default() - }; - - let swap_receipt = match provider - .send_transaction(swap_request) - .await - { - Ok(receipt) => receipt, - Err(e) => { - eprintln!("\nFailed to send swap transaction: {e:?}\n"); - continue; - } - }; - - let swap_result = match swap_receipt.get_receipt().await { - Ok(result) => result, - Err(e) => { - eprintln!("\nFailed to get swap receipt: {e:?}\n"); - continue; - } - }; - - println!( - "Swap transaction sent with hash: {hash:?} and status: {status:?}", - hash = swap_result.transaction_hash, - status = swap_result.status() - ); - - if swap_result.status() { - println!( - "\n✅ Swap executed successfully! Exiting the session...\n" - ); - - // Calculate the correct price ratio - let (forward_price, _reverse_price) = format_price_ratios( - &amount_in, - &expected_amount_copy, - &sell_token, - &buy_token, - ); - - println!( - "Summary: Swapped {formatted_in} {sell_symbol} → {formatted_out} {buy_symbol} at a price of {forward_price:.6} {buy_symbol} per {sell_symbol}", - formatted_in = format_token_amount(&amount_in, &sell_token), - sell_symbol = sell_token.symbol, - formatted_out = format_token_amount(&expected_amount_copy, &buy_token), - buy_symbol = buy_token.symbol, - ); - return; // Exit the program after successful execution - } else { - eprintln!("\nSwap transaction failed!\n"); - continue; - } - } - "skip" => { - println!("\nSkipping this swap...\n"); - continue; - } - _ => { - println!("\nInvalid input. Please choose 'simulate', 'execute' or 'skip'.\n"); - continue; - } - } - } - } - } else { - println!("No matching pair found in update."); - } - } + let swapper_pk = get_env("PRIVATE_KEY"); + let rfq_stream_processor = RFQStreamProcessor::new(); + rfq_stream_processor + .process_rfq_stream( + &mut rx, + &sell_token, + &buy_token, + amount_in, + cli.swap_args.chain, + swapper_pk, + ) + .await?; + Ok(()) +} - println!("\nWaiting for more price levels... (Press Ctrl+C to exit)"); - } +fn setup_tracing() { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .with_target(false) + .init(); } // Format token amounts to human-readable values @@ -827,39 +250,3 @@ pub fn encode_input(selector: &str, mut encoded_args: Vec) -> Vec { call_data.extend(encoded_args); call_data } - -async fn get_token_balance( - provider: &FillProvider< - JoinFill>, - RootProvider, - >, - token_address: Address, - wallet_address: Address, - native_token_address: Address, -) -> Result> { - let balance = if token_address == native_token_address { - provider - .get_balance(wallet_address) - .await? - } else { - let balance_of_signature = "balanceOf(address)"; - let data = encode_input(balance_of_signature, (wallet_address,).abi_encode()); - - let result = provider - .call(TransactionRequest { - to: Some(TxKind::Call(token_address)), - input: TransactionInput { input: Some(AlloyBytes::from(data)), data: None }, - ..Default::default() - }) - .await?; - - U256::from_be_bytes( - result - .to_vec() - .try_into() - .unwrap_or([0u8; 32]), - ) - }; - // Convert the U256 to BigUint - Ok(BigUint::from_bytes_be(&balance.to_be_bytes::<32>())) -} diff --git a/examples/rfq_quickstart/rfq_stream.rs b/examples/rfq_quickstart/rfq_stream.rs new file mode 100644 index 000000000..8677fb33d --- /dev/null +++ b/examples/rfq_quickstart/rfq_stream.rs @@ -0,0 +1,388 @@ +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, + time::Duration, +}; + +use alloy::{ + primitives::{Address, Bytes as AlloyBytes, TxKind, U256}, + providers::Provider, + rpc::types::{TransactionInput, TransactionRequest}, + sol_types::SolValue, +}; +use dialoguer::{theme::ColorfulTheme, Select}; +use miette::{IntoDiagnostic, Result}; +use num_bigint::BigUint; +use tokio::{sync::mpsc, time::timeout}; +use tycho_common::{ + models::{token::Token, Chain}, + simulation::protocol_sim::ProtocolSim, + Bytes, +}; +use tycho_execution::encoding::{ + evm::encoder_builders::TychoRouterEncoderBuilder, models::UserTransferType, +}; +use tycho_simulation::{ + protocol::models::{ProtocolComponent, Update}, + rfq::{ + protocols::bebop::{client_builder::BebopClientBuilder, state::BebopState}, + stream::RFQStreamBuilder, + }, +}; + +use crate::{ + encode_input, + env::get_env, + format_token_amount, + swap_executor::{SignerData, SwapExecutor}, +}; + +pub struct RFQStreamClient { + builder: RFQStreamBuilder, +} + +impl RFQStreamClient { + pub fn new() -> Self { + RFQStreamClient { builder: RFQStreamBuilder::new() } + } + + pub fn add_bebop( + mut self, + chain: Chain, + tvl_threshold: f64, + sell_token: &Token, + buy_token: &Token, + ) -> Result { + match (get_env("BEBOP_USER"), get_env("BEBOP_KEY")) { + (Some(user), Some(key)) => { + println!("Connecting to Bebop RFQ WebSocket..."); + let client = BebopClientBuilder::new(chain, user, key) + .tokens([sell_token.address.clone(), buy_token.address.clone()].into()) + .tvl_threshold(tvl_threshold) + .build() + .into_diagnostic()?; + self.builder = self + .builder + .add_client::("bebop", Box::new(client)); + Ok(self) + } + _ => Ok(self), + } + } + + pub async fn set_tokens(mut self, all_tokens: HashMap) -> Result { + self.builder = self + .builder + .set_tokens(all_tokens) + .await; + Ok(self) + } + + pub fn finish(self) -> RFQStreamBuilder { + self.builder + } +} + +pub struct RFQStreamProcessor; + +impl RFQStreamProcessor { + pub fn new() -> Self { + RFQStreamProcessor + } + + pub async fn process_rfq_stream( + &self, + rx: &mut mpsc::Receiver, + sell_token: &Token, + buy_token: &Token, + amount_in: BigUint, + chain: Chain, + swapper_pk: Option, + ) -> Result<()> { + let encoder = TychoRouterEncoderBuilder::new() + .chain(chain) + .user_transfer_type(UserTransferType::TransferFromPermit2) + .build() + .expect("Failed to build encoder"); + let swap_executor = match swapper_pk.clone() { + Some(pk) => { + let rpc_url = get_env("RPC_URL").expect("RPC_URL not set"); + SwapExecutor::new(chain, encoder) + .with_signer(pk, &rpc_url) + .await? + } + None => SwapExecutor::new(chain, encoder), + }; + while let Some(update) = rx.recv().await { + // Drain any additional buffered messages to get the most recent one + // + // ⚠️Warning: This works fine only if you assume that this message is entirely + // representative of the current state, as done in this quickstart. + // You should comment out this code portion if you would like to manually track removed + // components. + let update = Self::drain_to_latest_update(update, rx).await; + println!( + "Received RFQ price levels with {} new pairs for block/timestamp {}", + &update.states.len(), + update.block_number_or_timestamp + ); + Self::process_update(update, sell_token, buy_token, &amount_in, &swap_executor).await?; + println!("\nWaiting for more price levels... (Press Ctrl+C to exit)"); + } + Ok(()) + } + + async fn drain_to_latest_update( + mut latest_update: Update, + rx: &mut mpsc::Receiver, + ) -> Update { + let mut drained_count = 0; + while let Ok(newer_update) = timeout(Duration::from_millis(10), rx.recv()).await { + if let Some(newer_update) = newer_update { + latest_update = newer_update; + drained_count += 1; + } else { + break; + } + } + if drained_count > 0 { + println!( + "Fast-forwarded through {drained_count} older RFQ updates to get latest prices" + ); + } + latest_update + } + + async fn process_update( + update: Update, + sell_token: &Token, + buy_token: &Token, + amount_in: &BigUint, + swap_executor: &SwapExecutor, + ) -> Result<()> { + for (component, state) in Self::filter_matching_components(&update, sell_token, buy_token) { + if let Some(amount_out) = + Self::calculate_amount_out(state.as_ref(), amount_in, sell_token, buy_token) + { + println!( + "Best indicative price for swap {}: {} {} -> {} {}", + component.protocol_system, + format_token_amount(amount_in, sell_token), + sell_token.symbol, + format_token_amount(&amount_out, buy_token), + buy_token.symbol + ); + + if let Some(signer_data) = swap_executor.signer_data.as_ref() { + if Self::get_token_balances( + sell_token, + buy_token, + amount_in, + swap_executor, + signer_data, + ) + .await? + .is_empty() + { + continue; + } + + println!("Would you like to simulate or execute this swap?"); + println!("Please be aware that the market might move while you make your decision, which might lead to a revert if you've set a min amount out or slippage."); + println!("Warning: slippage is set to 0.25% during execution by default.\n"); + + let user_choice = Self::handle_user_interaction(); + match user_choice.as_str() { + "simulate" => { + swap_executor + .simulate_swap( + &component, + state.as_ref(), + sell_token, + buy_token, + amount_in, + &amount_out, + signer_data, + ) + .await?; + } + "execute" => { + swap_executor + .execute_swap( + &component, + state.as_ref(), + sell_token, + buy_token, + amount_in, + &amount_out, + signer_data, + ) + .await?; + } + _ => println!("Skipping this swap..."), + } + } else { + println!("Signer private key not provided. Skipping simulation/execution."); + } + } + } + Ok(()) + } + + fn filter_matching_components<'a>( + update: &'a Update, + sell_token: &'a Token, + buy_token: &'a Token, + ) -> impl Iterator)> + 'a { + update + .states + .iter() + .filter_map(move |(comp_id, state)| { + update + .new_pairs + .get(comp_id) + .and_then(|component| { + let tokens = &component.tokens; + if HashSet::from([sell_token, buy_token]) + .is_subset(&HashSet::from_iter(tokens.iter())) + { + Some((component.clone(), state.clone_box())) + } else { + None + } + }) + }) + } + + fn calculate_amount_out( + state: &dyn ProtocolSim, + amount_in: &BigUint, + sell_token: &Token, + buy_token: &Token, + ) -> Option { + state + .get_amount_out(amount_in.clone(), sell_token, buy_token) + .ok() + .map(|result| result.amount) + } + + async fn get_token_balances( + sell_token: &Token, + buy_token: &Token, + amount_in: &BigUint, + swap_executor: &SwapExecutor, + signer_data: &SignerData, + ) -> Result> { + let mut balances = HashMap::new(); + + // Show sell token balance + match Self::get_token_balance( + &signer_data.provider, + Address::from_slice(&sell_token.address), + signer_data.signer.address(), + Address::from_slice( + &swap_executor + .chain + .native_token() + .address, + ), + ) + .await + { + Ok(balance) => { + let formatted_balance = format_token_amount(&balance, sell_token); + println!( + "\nYour balance: {formatted_balance} {sell_symbol}", + sell_symbol = sell_token.symbol + ); + if &balance < amount_in { + let required = format_token_amount(amount_in, sell_token); + println!("⚠️ Warning: Insufficient balance for swap. You have {formatted_balance} {sell_symbol} but need {required} {sell_symbol}", + formatted_balance = formatted_balance, + sell_symbol = sell_token.symbol, + ); + return Ok(balances); + } + balances.insert(Address::from_slice(&sell_token.address), balance); + } + Err(e) => eprintln!("Failed to get token balance: {e}"), + } + + // Show buy token balance + match Self::get_token_balance( + &signer_data.provider, + Address::from_slice(&buy_token.address), + signer_data.signer.address(), + Address::from_slice( + &swap_executor + .chain + .native_token() + .address, + ), + ) + .await + { + Ok(balance) => { + let formatted_balance = format_token_amount(&balance, buy_token); + println!( + "Your {buy_symbol} balance: {formatted_balance} {buy_symbol}", + buy_symbol = buy_token.symbol + ); + balances.insert(Address::from_slice(&buy_token.address), balance); + } + Err(e) => { + eprintln!("Failed to get {buy_symbol} balance: {e}", buy_symbol = buy_token.symbol) + } + } + Ok(balances) + } + + async fn get_token_balance( + provider: &Arc, + token_address: Address, + wallet_address: Address, + native_token_address: Address, + ) -> Result> { + let balance = if token_address == native_token_address { + provider + .get_balance(wallet_address) + .await? + } else { + let balance_of_signature = "balanceOf(address)"; + let data = encode_input(balance_of_signature, (wallet_address,).abi_encode()); + + let result = provider + .call(TransactionRequest { + to: Some(TxKind::Call(token_address)), + input: TransactionInput { input: Some(AlloyBytes::from(data)), data: None }, + ..Default::default() + }) + .await?; + + U256::from_be_bytes( + result + .to_vec() + .try_into() + .unwrap_or([0u8; 32]), + ) + }; + // Convert the U256 to BigUint + Ok(BigUint::from_bytes_be(&balance.to_be_bytes::<32>())) + } + + fn handle_user_interaction() -> String { + let options = vec!["Simulate the swap", "Execute the swap", "Skip this swap"]; + let selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("What would you like to do?") + .default(0) + .items(&options) + .interact() + .unwrap_or(2); // Default to skip if error + + match selection { + 0 => "simulate".to_string(), + 1 => "execute".to_string(), + _ => "skip".to_string(), + } + } +} diff --git a/examples/rfq_quickstart/swap_executor.rs b/examples/rfq_quickstart/swap_executor.rs new file mode 100644 index 000000000..db847cc75 --- /dev/null +++ b/examples/rfq_quickstart/swap_executor.rs @@ -0,0 +1,375 @@ +use std::{process::exit, str::FromStr, sync::Arc}; + +use alloy::{ + eips::BlockNumberOrTag, + network::EthereumWallet, + primitives::{Address, Bytes as AlloyBytes, TxKind, B256}, + providers::{Provider, ProviderBuilder}, + rpc::types::{ + simulate::{SimBlock, SimulatePayload}, + TransactionInput, TransactionRequest, + }, + signers::{ + k256::ecdsa::SigningKey, + local::{LocalSigner, PrivateKeySigner}, + }, + sol_types::SolValue, +}; +use foundry_config::NamedChain; +use miette::Result; +use num_bigint::BigUint; +use tycho_common::{ + models::{token::Token, Chain}, + simulation::protocol_sim::ProtocolSim, + Bytes, +}; +use tycho_execution::encoding::tycho_encoder::TychoEncoder; +use tycho_simulation::{ + evm::protocol::u256_num::biguint_to_u256, protocol::models::ProtocolComponent, +}; + +use crate::{ + create_solution, encode_input, encode_tycho_router_call, format_price_ratios, + format_token_amount, +}; + +pub struct SwapExecutor { + pub chain: Chain, + pub encoder: Box, + pub signer_data: Option, +} + +pub struct SignerData { + pub signer: LocalSigner, + pub provider: Arc, +} + +impl SwapExecutor { + pub fn new(chain: Chain, encoder: Box) -> Self { + Self { chain, encoder, signer_data: None } + } + + pub async fn with_signer(mut self, pk: String, rpc_url: &str) -> Result { + let signer = + PrivateKeySigner::from_bytes(&B256::from_str(&pk).expect("Invalid private key")) + .expect("Failed to create signer"); + let provider = ProviderBuilder::default() + .with_chain(NamedChain::try_from(self.chain.id()).expect("Invalid chain")) + .wallet(EthereumWallet::from(signer.clone())) + .connect(rpc_url) + .await + .expect("Failed to connect provider"); + self.signer_data = Some(SignerData { signer, provider: Arc::new(provider) }); + Ok(self) + } + + #[allow(clippy::too_many_arguments)] + pub async fn simulate_swap( + &self, + component: &ProtocolComponent, + state: &dyn ProtocolSim, + sell_token: &Token, + buy_token: &Token, + amount_in: &BigUint, + amount_out: &BigUint, + signer_data: &SignerData, + ) -> Result<()> { + println!("\nSimulating RFQ swap..."); + println!("Step 1: Encoding the permit2 transaction..."); + let approve_function_signature = "approve(address,uint256)"; + let args = ( + Address::from_str("0x000000000022D473030F116dDEE9F6B43aC78BA3") + .expect("Invalid address"), + biguint_to_u256(amount_in), + ); + let approval_data = encode_input(approve_function_signature, args.abi_encode()); + let nonce = signer_data + .provider + .get_transaction_count(signer_data.signer.address()) + .await + .expect("Failed to get nonce"); + let block = signer_data + .provider + .get_block_by_number(BlockNumberOrTag::Latest) + .await + .expect("Failed to fetch latest block") + .expect("Block not found"); + let base_fee = block + .header + .base_fee_per_gas + .expect("Base fee not available"); + let max_priority_fee_per_gas = 1_000_000_000u64; + let max_fee_per_gas = base_fee + max_priority_fee_per_gas; + let approval_request = TransactionRequest { + to: Some(TxKind::Call(Address::from_slice(&sell_token.address))), + from: Some(signer_data.signer.address()), + value: None, + input: TransactionInput { input: Some(AlloyBytes::from(approval_data)), data: None }, + gas: Some(100_000u64), + chain_id: Some(self.chain.id()), + max_fee_per_gas: Some(max_fee_per_gas.into()), + max_priority_fee_per_gas: Some(max_priority_fee_per_gas.into()), + nonce: Some(nonce), + ..Default::default() + }; + + println!("Step 2: Encoding the solution transaction..."); + + let solution = create_solution( + component.clone(), + Arc::from(state.clone_box()), + sell_token.clone(), + buy_token.clone(), + amount_in.clone(), + Bytes::from(signer_data.signer.address().to_vec()), + amount_out.clone(), + ); + + let encoded_solution = self + .encoder + .encode_solutions(vec![solution.clone()]) + .expect("Failed to encode router calldata")[0] + .clone(); + + let tx = encode_tycho_router_call( + self.chain.id(), + encoded_solution.clone(), + &solution, + self.chain.native_token().address, + signer_data.signer.clone(), + ) + .expect("Failed to encode router call"); + + let swap_request = TransactionRequest { + to: Some(TxKind::Call(Address::from_slice(&tx.to))), + from: Some(signer_data.signer.address()), + value: Some(biguint_to_u256(&tx.value)), + input: TransactionInput { input: Some(AlloyBytes::from(tx.data)), data: None }, + gas: Some(800_000u64), + chain_id: Some(self.chain.id()), + max_fee_per_gas: Some(max_fee_per_gas.into()), + max_priority_fee_per_gas: Some(max_priority_fee_per_gas.into()), + nonce: Some(nonce + 1), + ..Default::default() + }; + + println!("Step 3: Simulating approval and solution transactions together..."); + let approval_payload = SimulatePayload { + block_state_calls: vec![SimBlock { + block_overrides: None, + state_overrides: None, + calls: vec![approval_request.clone(), swap_request], + }], + trace_transfers: true, + validation: true, + return_full_transactions: true, + }; + + match signer_data + .provider + .simulate(&approval_payload) + .await + { + Ok(output) => { + let mut all_successful = true; + for block in output.iter() { + println!( + "\nSimulated Block {block_num}:", + block_num = block.inner.header.number + ); + for transaction in block.calls.iter() { + println!( + " RFQ Swap: Status: {status:?}, Gas Used: {gas_used}", + status = transaction.status, + gas_used = transaction.gas_used + ); + if !transaction.status { + all_successful = false; + } + } + } + + if all_successful { + println!("\n✅ Simulation successful!"); + } else { + println!("\n❌ Simulation failed! One or more transactions reverted."); + println!("Consider adjusting parameters and re-simulating before execution."); + } + } + Err(e) => { + eprintln!("\n❌ Simulation failed: {e:?}"); + println!("Your RPC provider does not support transaction simulation. Consider switching RPC provider."); + } + } + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + pub async fn execute_swap( + &self, + component: &ProtocolComponent, + state: &dyn ProtocolSim, + sell_token: &Token, + buy_token: &Token, + amount_in: &BigUint, + amount_out: &BigUint, + signer_data: &SignerData, + ) -> Result<()> { + println!("Executing RFQ swap..."); + + println!("Step 1: Sending permit2 approval..."); + let approve_function_signature = "approve(address,uint256)"; + let args = ( + Address::from_str("0x000000000022D473030F116dDEE9F6B43aC78BA3") + .expect("Invalid address"), + biguint_to_u256(amount_in), + ); + let approval_data = encode_input(approve_function_signature, args.abi_encode()); + let nonce = signer_data + .provider + .get_transaction_count(signer_data.signer.address()) + .await + .expect("Failed to get nonce"); + let block = signer_data + .provider + .get_block_by_number(BlockNumberOrTag::Latest) + .await + .expect("Failed to fetch latest block") + .expect("Block not found"); + let base_fee = block + .header + .base_fee_per_gas + .expect("Base fee not available"); + let max_priority_fee_per_gas = 1_000_000_000u64; + let max_fee_per_gas = base_fee + max_priority_fee_per_gas; + let approval_request = TransactionRequest { + to: Some(TxKind::Call(Address::from_slice(&sell_token.address))), + from: Some(signer_data.signer.address()), + value: None, + input: TransactionInput { input: Some(AlloyBytes::from(approval_data)), data: None }, + gas: Some(100_000u64), + chain_id: Some(self.chain.id()), + max_fee_per_gas: Some(max_fee_per_gas.into()), + max_priority_fee_per_gas: Some(max_priority_fee_per_gas.into()), + nonce: Some(nonce), + ..Default::default() + }; + + let approval_receipt = match signer_data + .provider + .send_transaction(approval_request) + .await + { + Ok(receipt) => receipt, + Err(e) => { + eprintln!("\nFailed to send approval transaction: {e:?}\n"); + return Ok(()); + } + }; + + let approval_result = match approval_receipt.get_receipt().await { + Ok(result) => result, + Err(e) => { + eprintln!("\nFailed to get approval receipt: {e:?}\n"); + return Ok(()); + } + }; + + println!( + "Approval transaction sent with hash: {hash:?} and status: {status:?}", + hash = approval_result.transaction_hash, + status = approval_result.status() + ); + + if !approval_result.status() { + eprintln!("\nApproval transaction failed! Cannot proceed with swap.\n"); + return Ok(()); + } + + println!("Step 2: Encoding solution transaction..."); + + let solution = create_solution( + component.clone(), + Arc::from(state.clone_box()), + sell_token.clone(), + buy_token.clone(), + amount_in.clone(), + Bytes::from(signer_data.signer.address().to_vec()), + amount_out.clone(), + ); + + let encoded_solution = self + .encoder + .encode_solutions(vec![solution.clone()]) + .expect("Failed to encode router calldata")[0] + .clone(); + + let tx = encode_tycho_router_call( + self.chain.id(), + encoded_solution.clone(), + &solution, + self.chain.native_token().address, + signer_data.signer.clone(), + ) + .expect("Failed to encode router call"); + + let swap_request = TransactionRequest { + to: Some(TxKind::Call(Address::from_slice(&tx.to))), + from: Some(signer_data.signer.address()), + value: Some(biguint_to_u256(&tx.value)), + input: TransactionInput { input: Some(AlloyBytes::from(tx.data)), data: None }, + gas: Some(800_000u64), + chain_id: Some(self.chain.id()), + max_fee_per_gas: Some(max_fee_per_gas.into()), + max_priority_fee_per_gas: Some(max_priority_fee_per_gas.into()), + nonce: Some(nonce + 1), + ..Default::default() + }; + + let swap_receipt = match signer_data + .provider + .send_transaction(swap_request) + .await + { + Ok(receipt) => receipt, + Err(e) => { + eprintln!("\nFailed to send swap transaction: {e:?}\n"); + return Ok(()); + } + }; + + let swap_result = match swap_receipt.get_receipt().await { + Ok(result) => result, + Err(e) => { + eprintln!("\nFailed to get swap receipt: {e:?}\n"); + return Ok(()); + } + }; + + println!( + "Swap transaction sent with hash: {hash:?} and status: {status:?}", + hash = swap_result.transaction_hash, + status = swap_result.status() + ); + + if swap_result.status() { + println!("\n✅ Swap executed successfully! Exiting the session...\n"); + + // Calculate the correct price ratio + let (forward_price, _reverse_price) = + format_price_ratios(amount_in, amount_out, sell_token, buy_token); + + println!( + "Summary: Swapped {formatted_in} {sell_symbol} → {formatted_out} {buy_symbol} at a price of {forward_price:.6} {buy_symbol} per {sell_symbol}", + formatted_in = format_token_amount(amount_in, sell_token), + sell_symbol = sell_token.symbol, + formatted_out = format_token_amount(amount_out, buy_token), + buy_symbol = buy_token.symbol, + ); + exit(0); // Exit the program after successful execution + } else { + eprintln!("\nSwap transaction failed!\n"); + Ok(()) + } + } +} diff --git a/examples/rfq_quickstart/tycho_client.rs b/examples/rfq_quickstart/tycho_client.rs new file mode 100644 index 000000000..59e8aebbe --- /dev/null +++ b/examples/rfq_quickstart/tycho_client.rs @@ -0,0 +1,98 @@ +use std::{collections::HashMap, str::FromStr}; + +use miette::{miette, IntoDiagnostic, Result, WrapErr}; +use num_bigint::BigUint; +use tycho_common::{ + models::{token::Token, Chain}, + Bytes, +}; +use tycho_simulation::utils::load_all_tokens; + +use crate::{ + cli::SwapArgs, + env::{get_env, get_env_with_default}, +}; + +pub struct TychoClient { + url: String, + api_key: String, + chain: Chain, +} + +impl TychoClient { + pub fn new(url: String, api_key: String, chain: Chain) -> Self { + Self { url, api_key, chain } + } + + pub fn from_env(chain: Chain) -> Result { + let default_url = Self::get_default_url(&chain) + .ok_or_else(|| miette!("No default URL for chain {chain:?}"))?; + Ok(Self::new( + get_env_with_default("TYCHO_URL", default_url), + get_env("TYCHO_API_KEY").ok_or(miette!( + "The environment variable `TYCHO_API_KEY` is not set. Please set it to your Tycho API key." + ))?, + chain + )) + } + + /// Get the default Tycho URL for the given chain. + fn get_default_url(chain: &Chain) -> Option { + match chain { + Chain::Ethereum => Some("tycho-beta.propellerheads.xyz".to_string()), + Chain::Base => Some("tycho-base-beta.propellerheads.xyz".to_string()), + Chain::Unichain => Some("tycho-unichain-beta.propellerheads.xyz".to_string()), + _ => None, + } + } + + pub async fn load_tokens(&self) -> HashMap { + println!("Loading tokens from Tycho... {}", self.url); + let tokens = + load_all_tokens(&self.url, false, Some(&self.api_key), self.chain, None, None).await; + println!("Tokens loaded: {}", tokens.len()); + tokens + } + + pub fn get_token_info( + &self, + swap_args: &SwapArgs, + all_tokens: &HashMap, + ) -> Result<(Token, Token, BigUint)> { + let sell_token_address = Bytes::from_str( + swap_args + .sell_token + .as_ref() + .ok_or_else(|| miette!("Sell token not provided"))?, + ) + .into_diagnostic() + .wrap_err("Invalid address for sell token")?; + let buy_token_address = Bytes::from_str( + swap_args + .buy_token + .as_ref() + .ok_or_else(|| miette!("Buy token not provided"))?, + ) + .into_diagnostic() + .wrap_err("Invalid address for buy token")?; + let sell_token = all_tokens + .get(&sell_token_address) + .ok_or_else(|| miette!("Sell token not found"))? + .clone(); + let buy_token = all_tokens + .get(&buy_token_address) + .ok_or_else(|| miette!("Buy token not found"))? + .clone(); + let amount_in = + BigUint::from((swap_args.sell_amount * 10f64.powi(sell_token.decimals as i32)) as u128); + + println!( + "Looking for RFQ quotes for {amount} {sell_symbol} -> {buy_symbol} on {chain:?}", + amount = swap_args.sell_amount, + sell_symbol = sell_token.symbol, + buy_symbol = buy_token.symbol, + chain = swap_args.chain + ); + Ok((sell_token, buy_token, amount_in)) + } +} diff --git a/examples/rfq_quickstart/utils.rs b/examples/rfq_quickstart/utils.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/examples/rfq_quickstart/utils.rs @@ -0,0 +1 @@ + diff --git a/src/evm/protocol/filters.rs b/src/evm/protocol/filters.rs index a6e6e3608..a6e37c586 100644 --- a/src/evm/protocol/filters.rs +++ b/src/evm/protocol/filters.rs @@ -161,7 +161,7 @@ pub fn curve_pool_filter(component: &ComponentWithState) -> bool { "Filtering out Curve pool {} because it belongs to an unsupported factory", component.component.id ); - return false + return false; } }; @@ -181,7 +181,7 @@ pub fn curve_pool_filter(component: &ComponentWithState) -> bool { "Filtering out Curve pool {} because it has a rebasing token that is not supported", component.component.id ); - return false + return false; } true diff --git a/src/rfq/protocols/bebop/state.rs b/src/rfq/protocols/bebop/state.rs index 0db63e6c3..aa537a305 100644 --- a/src/rfq/protocols/bebop/state.rs +++ b/src/rfq/protocols/bebop/state.rs @@ -97,7 +97,7 @@ impl ProtocolSim for BebopState { return Err(SimulationError::RecoverableError(format!( "Invalid token addresses: {}, {}", token_in.address, token_out.address - ))) + ))); }; // if sell base is true -> use bids // if sell base is false -> use asks AND amount is in quote token so the levels need to be @@ -134,7 +134,7 @@ impl ProtocolSim for BebopState { if remaining_amount_in > 0.0 { return Err(SimulationError::InvalidInput( format!("Pool has not enough liquidity to support complete swap. input amount: {amount_in}, consumed amount: {}", amount_in-remaining_amount_in), - Some(res))) + Some(res))); } Ok(res) @@ -156,7 +156,7 @@ impl ProtocolSim for BebopState { } else { return Err(SimulationError::RecoverableError(format!( "Invalid token addresses: {sell_token}, {buy_token}" - ))) + ))); }; // If there are no price levels, return 0 for both limits diff --git a/src/rfq/protocols/hashflow/client.rs b/src/rfq/protocols/hashflow/client.rs index bf3959acb..2a9ed077b 100644 --- a/src/rfq/protocols/hashflow/client.rs +++ b/src/rfq/protocols/hashflow/client.rs @@ -415,7 +415,7 @@ impl RFQClient for HashflowClient { return Err(RFQError::QuoteNotFound(format!( "Hashflow quote not found for {} {} ->{}", params.amount_in, params.token_in, params.token_out, - ))) + ))); } // We assume there will be only one quote request at a time let quote = quotes[0].clone(); @@ -425,7 +425,7 @@ impl RFQClient for HashflowClient { { return Err(RFQError::FatalError( "Quote tokens don't match request tokens".to_string(), - )) + )); } let mut quote_attributes: HashMap = HashMap::new(); @@ -522,7 +522,7 @@ impl RFQClient for HashflowClient { return Err(RFQError::QuoteNotFound(format!( "Hashflow quote not found for {} {} ->{}", params.amount_in, params.token_in, params.token_out, - ))) + ))); } } "fail" => { diff --git a/src/rfq/stream.rs b/src/rfq/stream.rs index 55a7bbaac..c50e4a25f 100644 --- a/src/rfq/stream.rs +++ b/src/rfq/stream.rs @@ -213,7 +213,7 @@ mod tests { if error_at_time == current_time { return Err(RFQError::FatalError(format!( "{name} stream is dying and can't go on" - ))) + ))); }; }; let protocol_component =