From b3db4ca79e245570c80f34696ca13e0f612199c2 Mon Sep 17 00:00:00 2001 From: Markus Date: Thu, 27 Feb 2025 19:39:39 +0000 Subject: [PATCH 1/7] Usability Fixes - Added Flashbots Protect integration with --flashbots flag for MEV protection - Implemented interactive arrow-key selection menus using dialoguer - Refactored transaction code into reusable function with proper error handling - Improved price and token amount formatting with human-readable values - Enhanced error recovery for stream disconnections and simulation failures - Added better display of swap prices showing rates in both directions --- Cargo.lock | 15 +- Cargo.toml | 3 + examples/quickstart/main.rs | 311 ++++++++++++++++++++++++++---------- 3 files changed, 244 insertions(+), 85 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8924ad36..f77b2553 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2522,6 +2522,18 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "dialoguer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59c6f2989294b9a498d3ad5491a79c6deb604617378e1cdc4bfc1c1361fe2f87" +dependencies = [ + "console", + "shell-words", + "tempfile", + "zeroize", +] + [[package]] name = "dialoguer" version = "0.11.0" @@ -3077,7 +3089,7 @@ dependencies = [ "alloy-sol-types", "base64 0.22.1", "chrono", - "dialoguer", + "dialoguer 0.11.0", "ecdsa", "eyre", "forge-script-sequence", @@ -7907,6 +7919,7 @@ dependencies = [ "chrono", "clap", "crossterm", + "dialoguer 0.10.4", "dotenv", "foundry-config", "foundry-evm", diff --git a/Cargo.toml b/Cargo.toml index 54815623..34aad38f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,9 @@ revm-inspectors = { version = "0.10", features = ["serde"], optional = true } num-bigint = "0.4.6" tokio-stream = "0.1.16" +# Dialoguer +dialoguer = "0.10.4" + [dev-dependencies] tokio-test = "0.4.4" approx = "0.5.1" diff --git a/examples/quickstart/main.rs b/examples/quickstart/main.rs index ac2d3ea6..8df8aa7c 100644 --- a/examples/quickstart/main.rs +++ b/examples/quickstart/main.rs @@ -1,8 +1,7 @@ use std::{ collections::{HashMap, HashSet}, default::Default, - env, io, - io::Write, + env, str::FromStr, }; @@ -25,6 +24,7 @@ use alloy_sol_types::SolValue; use clap::Parser; use futures::StreamExt; use num_bigint::BigUint; +use num_traits::ToPrimitive; use tracing_subscriber::EnvFilter; use tycho_core::Bytes; use tycho_execution::encoding::{ @@ -53,6 +53,7 @@ use tycho_simulation::{ tycho_core::models::Chain, utils::load_all_tokens, }; +use dialoguer::{theme::ColorfulTheme, Select}; const FAKE_PK: &str = "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234"; @@ -69,6 +70,9 @@ struct Cli { tvl_threshold: f64, #[arg(short, long, default_value = FAKE_PK)] swapper_pk: String, + /// Whether to use Flashbots Protect for transaction submission + #[arg(long, default_value_t = false)] + flashbots: bool, } #[tokio::main] @@ -111,7 +115,7 @@ async fn main() { BigUint::from((cli.sell_amount * 10f64.powi(sell_token.decimals as i32)) as u128); println!( - "Looking for the best swap for {} {} -> {}", + "Looking for pool with best price for {} {} -> {}", cli.sell_amount, sell_token.symbol, buy_token.symbol ); let mut pairs: HashMap = HashMap::new(); @@ -152,17 +156,32 @@ async fn main() { .expect("Failed to private key signer"); let tx_signer = EthereumWallet::from(wallet.clone()); - let provider = ProviderBuilder::new() - .wallet(tx_signer.clone()) - .on_http( - env::var("ETH_RPC_URL") - .expect("ETH_RPC_URL env var not set") - .parse() - .expect("Failed to parse ETH_RPC_URL"), - ); + let provider = { + let builder = ProviderBuilder::new().wallet(tx_signer.clone()); + + if cli.flashbots { + println!("Using Flashbots Protect for transaction submission"); + builder.on_http("https://rpc.flashbots.net/fast".parse().expect("Failed to parse Flashbots URL")) + } else { + println!("Using standard RPC for transaction submission"); + builder.on_http( + env::var("ETH_RPC_URL") + .expect("ETH_RPC_URL env var not set") + .parse() + .expect("Failed to parse ETH_RPC_URL"), + ) + } + }; - while let Some(message) = protocol_stream.next().await { - let message = message.expect("Could not receive message"); + while let Some(message_result) = protocol_stream.next().await { + let message = match message_result { + Ok(msg) => msg, + Err(e) => { + eprintln!("Error receiving message: {:?}. Continuing to next message...", e); + continue; + } + }; + let best_pool = get_best_swap( message, &mut pairs, @@ -191,29 +210,66 @@ async fn main() { println!("Signer private key was not provided. Skipping simulation/execution..."); continue } - println!("Do you want to simulate, execute or skip 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."); - print!("(simulate/execute/skip): "); - io::stdout().flush().unwrap(); - let mut input = String::new(); - io::stdin() - .read_line(&mut input) - .expect("Failed to read input"); - - let input = input.trim().to_lowercase(); - - match input.as_str() { + println!("What would you like to do?"); + 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!("Simulating by performing an approval (for permit2) and a swap transaction..."); + + if cli.flashbots { + println!("Note: Transaction simulation is limited when using Flashbots Protect."); + println!("Flashbots only attempts to include transactions that won't revert."); + println!("Do you want to proceed with execution instead?"); + let yes_no_options = vec!["Yes", "No"]; + let yes_no_selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Do you want to proceed with execution instead?") + .default(1) // Default to No + .items(&yes_no_options) + .interact() + .unwrap_or(1); // Default to No if error + + if yes_no_selection == 0 { + match execute_swap_transaction( + provider.clone(), + &amount_in, + wallet.address(), + &sell_token_address, + tx.clone(), + ).await { + Ok(_) => return, + Err(e) => { + eprintln!("Failed to execute transaction: {:?}", e); + continue; + } + } + } else { + println!("Skipping this swap..."); + continue; + } + } + let (approval_request, swap_request) = get_tx_requests( provider.clone(), biguint_to_u256(&amount_in), wallet.address(), Address::from_slice(&sell_token_address), - tx, + tx.clone(), ) .await; - + let payload = SimulatePayload { block_state_calls: vec![SimBlock { block_overrides: None, @@ -225,67 +281,68 @@ async fn main() { return_full_transactions: true, }; - let output = provider - .simulate(&payload) - .await - .expect("Failed to simulate transaction"); - - for block in output.iter() { - println!("Simulated Block {}:", block.inner.header.number); - for (j, transaction) in block.calls.iter().enumerate() { - println!( - " Transaction {}: Status: {:?}, Gas Used: {}", - j + 1, - transaction.status, - transaction.gas_used - ); + match provider.simulate(&payload).await { + Ok(output) => { + for block in output.iter() { + println!("Simulated Block {}:", block.inner.header.number); + for (j, transaction) in block.calls.iter().enumerate() { + println!( + " Transaction {}: Status: {:?}, Gas Used: {}", + j + 1, + transaction.status, + transaction.gas_used + ); + } + } + }, + Err(e) => { + eprintln!("Simulation failed: {:?}", e); + println!("Your RPC provider does not support transaction simulation."); + println!("Do you want to proceed with execution instead?"); + let yes_no_options = vec!["Yes", "No"]; + let yes_no_selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Do you want to proceed with execution instead?") + .default(1) // Default to No + .items(&yes_no_options) + .interact() + .unwrap_or(1); // Default to No if error + + if yes_no_selection == 0 { + match execute_swap_transaction( + provider.clone(), + &amount_in, + wallet.address(), + &sell_token_address, + tx.clone(), + ).await { + Ok(_) => return, + Err(e) => { + eprintln!("Failed to execute transaction: {:?}", e); + continue; + } + } + } else { + println!("Skipping this swap..."); + continue; + } } } - // println!("Full simulation logs: {:?}", output); return; } "execute" => { - println!("Executing by performing an approval (for permit2) and a swap transaction..."); - let (approval_request, swap_request) = get_tx_requests( + match execute_swap_transaction( provider.clone(), - biguint_to_u256(&amount_in), + &amount_in, wallet.address(), - Address::from_slice(&sell_token_address), + &sell_token_address, tx, - ) - .await; - - let approval_receipt = provider - .send_transaction(approval_request) - .await - .expect("Failed to send transaction"); - - let approval_result = approval_receipt - .get_receipt() - .await - .expect("Failed to get approval receipt"); - println!( - "Approval transaction sent with hash: {:?} and status: {:?}", - approval_result.transaction_hash, - approval_result.status() - ); - - let swap_receipt = provider - .send_transaction(swap_request) - .await - .expect("Failed to send transaction"); - - let swap_result = swap_receipt - .get_receipt() - .await - .expect("Failed to get swap receipt"); - println!( - "Swap transaction sent with hash: {:?} and status: {:?}", - swap_result.transaction_hash, - swap_result.status() - ); - - return; + ).await { + Ok(_) => return, + Err(e) => { + eprintln!("Failed to execute transaction: {:?}", e); + continue; + } + } } "skip" => { println!("Skipping this swap..."); @@ -344,16 +401,23 @@ fn get_best_swap( { println!("The best swap (out of {} possible pools) is:", amounts_out.len()); println!( - "protocol: {:?}", + "Protocol: {:?}", pairs .get(key) .expect("Failed to get best pool") .protocol_system ); - println!("id: {:?}", key); + println!("Pool address: {:?}", key); + let formatted_in = format_token_amount(&amount_in, &sell_token); + let formatted_out = format_token_amount(amount_out, &buy_token); + let (forward_price, reverse_price) = format_price_ratios(&amount_in, amount_out, &sell_token, &buy_token); + println!( - "swap: {:?} {:} -> {:?} {:}", - amount_in, sell_token.symbol, amount_out, buy_token.symbol + "Swap: {} {} -> {} {} \nPrice: {:.6} {} per {}, {:.6} {} per {}", + formatted_in, sell_token.symbol, + formatted_out, buy_token.symbol, + forward_price, buy_token.symbol, sell_token.symbol, + reverse_price, sell_token.symbol, buy_token.symbol ); Some(key.to_string()) } else { @@ -460,3 +524,82 @@ async fn get_tx_requests( (approval_request, swap_request) } + +// Helper function to format token amounts to human-readable values +fn format_token_amount(amount: &BigUint, token: &Token) -> String { + let decimal_amount = amount.to_f64().unwrap_or(0.0) / 10f64.powi(token.decimals as i32); + format!("{:.6}", decimal_amount) +} + +// Calculate price ratios in both directions +fn format_price_ratios( + amount_in: &BigUint, + amount_out: &BigUint, + token_in: &Token, + token_out: &Token +) -> (f64, f64) { + // Convert to f64 for calculation + let decimal_in = amount_in.to_f64().unwrap_or(0.0) / 10f64.powi(token_in.decimals as i32); + let decimal_out = amount_out.to_f64().unwrap_or(0.0) / 10f64.powi(token_out.decimals as i32); + + if decimal_in > 0.0 && decimal_out > 0.0 { + // Forward price: out per in + let forward = decimal_out / decimal_in; + // Reverse price: in per out + let reverse = decimal_in / decimal_out; + (forward, reverse) + } else { + (0.0, 0.0) + } +} + +async fn execute_swap_transaction( + provider: FillProvider< + JoinFill>, + ReqwestProvider, + Http, + Ethereum, + >, + amount_in: &BigUint, + wallet_address: Address, + sell_token_address: &Bytes, + tx: Transaction, +) -> Result<(), Box> { + println!("Executing by performing an approval (for permit2) and a swap transaction..."); + let (approval_request, swap_request) = get_tx_requests( + provider.clone(), + biguint_to_u256(amount_in), + wallet_address, + Address::from_slice(sell_token_address), + tx.clone(), + ) + .await; + + let approval_receipt = provider + .send_transaction(approval_request) + .await?; + + let approval_result = approval_receipt + .get_receipt() + .await?; + println!( + "Approval transaction sent with hash: {:?} and status: {:?}", + approval_result.transaction_hash, + approval_result.status() + ); + + let swap_receipt = provider + .send_transaction(swap_request) + .await?; + + let swap_result = swap_receipt + .get_receipt() + .await?; + println!( + "Swap transaction sent with hash: {:?} and status: {:?}", + swap_result.transaction_hash, + swap_result.status() + ); + + Ok(()) +} From 4c4a3882570dad4688598ba6047f0d2fbed40ed2 Mon Sep 17 00:00:00 2001 From: Markus Date: Thu, 27 Feb 2025 21:08:24 +0000 Subject: [PATCH 2/7] Remove Flashbots RPC --- examples/quickstart/main.rs | 59 +++++-------------------------------- 1 file changed, 8 insertions(+), 51 deletions(-) diff --git a/examples/quickstart/main.rs b/examples/quickstart/main.rs index 8df8aa7c..e848c260 100644 --- a/examples/quickstart/main.rs +++ b/examples/quickstart/main.rs @@ -70,9 +70,6 @@ struct Cli { tvl_threshold: f64, #[arg(short, long, default_value = FAKE_PK)] swapper_pk: String, - /// Whether to use Flashbots Protect for transaction submission - #[arg(long, default_value_t = false)] - flashbots: bool, } #[tokio::main] @@ -156,22 +153,14 @@ async fn main() { .expect("Failed to private key signer"); let tx_signer = EthereumWallet::from(wallet.clone()); - let provider = { - let builder = ProviderBuilder::new().wallet(tx_signer.clone()); - - if cli.flashbots { - println!("Using Flashbots Protect for transaction submission"); - builder.on_http("https://rpc.flashbots.net/fast".parse().expect("Failed to parse Flashbots URL")) - } else { - println!("Using standard RPC for transaction submission"); - builder.on_http( - env::var("ETH_RPC_URL") - .expect("ETH_RPC_URL env var not set") - .parse() - .expect("Failed to parse ETH_RPC_URL"), - ) - } - }; + let provider = ProviderBuilder::new() + .wallet(tx_signer.clone()) + .on_http( + env::var("ETH_RPC_URL") + .expect("ETH_RPC_URL env var not set") + .parse() + .expect("Failed to parse ETH_RPC_URL"), + ); while let Some(message_result) = protocol_stream.next().await { let message = match message_result { @@ -229,38 +218,6 @@ async fn main() { "simulate" => { println!("Simulating by performing an approval (for permit2) and a swap transaction..."); - if cli.flashbots { - println!("Note: Transaction simulation is limited when using Flashbots Protect."); - println!("Flashbots only attempts to include transactions that won't revert."); - println!("Do you want to proceed with execution instead?"); - let yes_no_options = vec!["Yes", "No"]; - let yes_no_selection = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("Do you want to proceed with execution instead?") - .default(1) // Default to No - .items(&yes_no_options) - .interact() - .unwrap_or(1); // Default to No if error - - if yes_no_selection == 0 { - match execute_swap_transaction( - provider.clone(), - &amount_in, - wallet.address(), - &sell_token_address, - tx.clone(), - ).await { - Ok(_) => return, - Err(e) => { - eprintln!("Failed to execute transaction: {:?}", e); - continue; - } - } - } else { - println!("Skipping this swap..."); - continue; - } - } - let (approval_request, swap_request) = get_tx_requests( provider.clone(), biguint_to_u256(&amount_in), From af2488b0459adca7cb31b8432db0de5189b60d50 Mon Sep 17 00:00:00 2001 From: Markus Date: Thu, 27 Feb 2025 19:39:39 +0000 Subject: [PATCH 3/7] Usability Fixes - Added Flashbots Protect integration with --flashbots flag for MEV protection - Implemented interactive arrow-key selection menus using dialoguer - Refactored transaction code into reusable function with proper error handling - Improved price and token amount formatting with human-readable values - Enhanced error recovery for stream disconnections and simulation failures - Added better display of swap prices showing rates in both directions Took 21 minutes --- Cargo.lock | 15 +- Cargo.toml | 3 + examples/quickstart/main.rs | 308 ++++++++++++++++++++++++++---------- 3 files changed, 243 insertions(+), 83 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f310c69e..29775aa1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2522,6 +2522,18 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "dialoguer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59c6f2989294b9a498d3ad5491a79c6deb604617378e1cdc4bfc1c1361fe2f87" +dependencies = [ + "console", + "shell-words", + "tempfile", + "zeroize", +] + [[package]] name = "dialoguer" version = "0.11.0" @@ -3077,7 +3089,7 @@ dependencies = [ "alloy-sol-types", "base64 0.22.1", "chrono", - "dialoguer", + "dialoguer 0.11.0", "ecdsa", "eyre", "forge-script-sequence", @@ -7907,6 +7919,7 @@ dependencies = [ "chrono", "clap", "crossterm", + "dialoguer 0.10.4", "dotenv", "foundry-config", "foundry-evm", diff --git a/Cargo.toml b/Cargo.toml index 93ebf364..12e71897 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,9 @@ revm-inspectors = { version = "0.10", features = ["serde"], optional = true } num-bigint = "0.4.6" tokio-stream = "0.1.16" +# Dialoguer +dialoguer = "0.10.4" + [dev-dependencies] tokio-test = "0.4.4" approx = "0.5.1" diff --git a/examples/quickstart/main.rs b/examples/quickstart/main.rs index 68d4af1a..e9d0df46 100644 --- a/examples/quickstart/main.rs +++ b/examples/quickstart/main.rs @@ -1,8 +1,7 @@ use std::{ collections::{HashMap, HashSet}, default::Default, - env, io, - io::Write, + env, str::FromStr, }; @@ -25,6 +24,7 @@ use alloy_sol_types::SolValue; use clap::Parser; use futures::StreamExt; use num_bigint::BigUint; +use num_traits::ToPrimitive; use tracing_subscriber::EnvFilter; use tycho_core::Bytes; use tycho_execution::encoding::{ @@ -53,6 +53,7 @@ use tycho_simulation::{ tycho_core::models::Chain, utils::load_all_tokens, }; +use dialoguer::{theme::ColorfulTheme, Select}; const FAKE_PK: &str = "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234"; @@ -69,6 +70,9 @@ struct Cli { tvl_threshold: f64, #[arg(short, long, default_value = FAKE_PK)] swapper_pk: String, + /// Whether to use Flashbots Protect for transaction submission + #[arg(long, default_value_t = false)] + flashbots: bool, } #[tokio::main] @@ -111,7 +115,7 @@ async fn main() { BigUint::from((cli.sell_amount * 10f64.powi(sell_token.decimals as i32)) as u128); println!( - "Looking for the best swap for {} {} -> {}", + "Looking for pool with best price for {} {} -> {}", cli.sell_amount, sell_token.symbol, buy_token.symbol ); let mut pairs: HashMap = HashMap::new(); @@ -152,17 +156,32 @@ async fn main() { .expect("Failed to private key signer"); let tx_signer = EthereumWallet::from(wallet.clone()); - let provider = ProviderBuilder::new() - .wallet(tx_signer.clone()) - .on_http( - env::var("ETH_RPC_URL") - .expect("ETH_RPC_URL env var not set") - .parse() - .expect("Failed to parse ETH_RPC_URL"), - ); + let provider = { + let builder = ProviderBuilder::new().wallet(tx_signer.clone()); + + if cli.flashbots { + println!("Using Flashbots Protect for transaction submission"); + builder.on_http("https://rpc.flashbots.net/fast".parse().expect("Failed to parse Flashbots URL")) + } else { + println!("Using standard RPC for transaction submission"); + builder.on_http( + env::var("ETH_RPC_URL") + .expect("ETH_RPC_URL env var not set") + .parse() + .expect("Failed to parse ETH_RPC_URL"), + ) + } + }; + + while let Some(message_result) = protocol_stream.next().await { + let message = match message_result { + Ok(msg) => msg, + Err(e) => { + eprintln!("Error receiving message: {:?}. Continuing to next message...", e); + continue; + } + }; - while let Some(message) = protocol_stream.next().await { - let message = message.expect("Could not receive message"); let best_swap = get_best_swap( message, &mut pairs, @@ -192,27 +211,65 @@ async fn main() { println!("Signer private key was not provided. Skipping simulation/execution..."); continue } - println!("Do you want to simulate, execute or skip this swap?"); + println!("What would you like to do?"); 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."); - print!("(simulate/execute/skip): "); - io::stdout().flush().unwrap(); - let mut input = String::new(); - io::stdin() - .read_line(&mut input) - .expect("Failed to read input"); - - let input = input.trim().to_lowercase(); - - match input.as_str() { + 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!("Simulating by performing an approval (for permit2) and a swap transaction..."); + + if cli.flashbots { + println!("Note: Transaction simulation is limited when using Flashbots Protect."); + println!("Flashbots only attempts to include transactions that won't revert."); + println!("Do you want to proceed with execution instead?"); + let yes_no_options = vec!["Yes", "No"]; + let yes_no_selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Do you want to proceed with execution instead?") + .default(1) // Default to No + .items(&yes_no_options) + .interact() + .unwrap_or(1); // Default to No if error + + if yes_no_selection == 0 { + match execute_swap_transaction( + provider.clone(), + &amount_in, + wallet.address(), + &sell_token_address, + tx.clone(), + ).await { + Ok(_) => return, + Err(e) => { + eprintln!("Failed to execute transaction: {:?}", e); + continue; + } + } + } else { + println!("Skipping this swap..."); + continue; + } + } + let (approval_request, swap_request) = get_tx_requests( provider.clone(), biguint_to_u256(&amount_in), wallet.address(), Address::from_slice(&sell_token_address), - tx, + tx.clone(), ) .await; @@ -227,67 +284,68 @@ async fn main() { return_full_transactions: true, }; - let output = provider - .simulate(&payload) - .await - .expect("Failed to simulate transaction"); - - for block in output.iter() { - println!("Simulated Block {}:", block.inner.header.number); - for (j, transaction) in block.calls.iter().enumerate() { - println!( - " Transaction {}: Status: {:?}, Gas Used: {}", - j + 1, - transaction.status, - transaction.gas_used - ); + match provider.simulate(&payload).await { + Ok(output) => { + for block in output.iter() { + println!("Simulated Block {}:", block.inner.header.number); + for (j, transaction) in block.calls.iter().enumerate() { + println!( + " Transaction {}: Status: {:?}, Gas Used: {}", + j + 1, + transaction.status, + transaction.gas_used + ); + } + } + }, + Err(e) => { + eprintln!("Simulation failed: {:?}", e); + println!("Your RPC provider does not support transaction simulation."); + println!("Do you want to proceed with execution instead?"); + let yes_no_options = vec!["Yes", "No"]; + let yes_no_selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Do you want to proceed with execution instead?") + .default(1) // Default to No + .items(&yes_no_options) + .interact() + .unwrap_or(1); // Default to No if error + + if yes_no_selection == 0 { + match execute_swap_transaction( + provider.clone(), + &amount_in, + wallet.address(), + &sell_token_address, + tx.clone(), + ).await { + Ok(_) => return, + Err(e) => { + eprintln!("Failed to execute transaction: {:?}", e); + continue; + } + } + } else { + println!("Skipping this swap..."); + continue; + } } } - // println!("Full simulation logs: {:?}", output); return; } "execute" => { - println!("Executing by performing an approval (for permit2) and a swap transaction..."); - let (approval_request, swap_request) = get_tx_requests( + match execute_swap_transaction( provider.clone(), - biguint_to_u256(&amount_in), + &amount_in, wallet.address(), - Address::from_slice(&sell_token_address), + &sell_token_address, tx, - ) - .await; - - let approval_receipt = provider - .send_transaction(approval_request) - .await - .expect("Failed to send transaction"); - - let approval_result = approval_receipt - .get_receipt() - .await - .expect("Failed to get approval receipt"); - println!( - "Approval transaction sent with hash: {:?} and status: {:?}", - approval_result.transaction_hash, - approval_result.status() - ); - - let swap_receipt = provider - .send_transaction(swap_request) - .await - .expect("Failed to send transaction"); - - let swap_result = swap_receipt - .get_receipt() - .await - .expect("Failed to get swap receipt"); - println!( - "Swap transaction sent with hash: {:?} and status: {:?}", - swap_result.transaction_hash, - swap_result.status() - ); - - return; + ).await { + Ok(_) => return, + Err(e) => { + eprintln!("Failed to execute transaction: {:?}", e); + continue; + } + } } "skip" => { println!("Skipping this swap..."); @@ -346,16 +404,23 @@ fn get_best_swap( { println!("The best swap (out of {} possible pools) is:", amounts_out.len()); println!( - "protocol: {:?}", + "Protocol: {:?}", pairs .get(key) .expect("Failed to get best pool") .protocol_system ); - println!("id: {:?}", key); + println!("Pool address: {:?}", key); + let formatted_in = format_token_amount(&amount_in, &sell_token); + let formatted_out = format_token_amount(amount_out, &buy_token); + let (forward_price, reverse_price) = format_price_ratios(&amount_in, amount_out, &sell_token, &buy_token); + println!( - "swap: {:?} {:} -> {:?} {:}", - amount_in, sell_token.symbol, amount_out, buy_token.symbol + "Swap: {} {} -> {} {} \nPrice: {:.6} {} per {}, {:.6} {} per {}", + formatted_in, sell_token.symbol, + formatted_out, buy_token.symbol, + forward_price, buy_token.symbol, sell_token.symbol, + reverse_price, sell_token.symbol, buy_token.symbol ); Some((key.to_string(), amount_out.clone())) } else { @@ -465,3 +530,82 @@ async fn get_tx_requests( (approval_request, swap_request) } + +// Helper function to format token amounts to human-readable values +fn format_token_amount(amount: &BigUint, token: &Token) -> String { + let decimal_amount = amount.to_f64().unwrap_or(0.0) / 10f64.powi(token.decimals as i32); + format!("{:.6}", decimal_amount) +} + +// Calculate price ratios in both directions +fn format_price_ratios( + amount_in: &BigUint, + amount_out: &BigUint, + token_in: &Token, + token_out: &Token +) -> (f64, f64) { + // Convert to f64 for calculation + let decimal_in = amount_in.to_f64().unwrap_or(0.0) / 10f64.powi(token_in.decimals as i32); + let decimal_out = amount_out.to_f64().unwrap_or(0.0) / 10f64.powi(token_out.decimals as i32); + + if decimal_in > 0.0 && decimal_out > 0.0 { + // Forward price: out per in + let forward = decimal_out / decimal_in; + // Reverse price: in per out + let reverse = decimal_in / decimal_out; + (forward, reverse) + } else { + (0.0, 0.0) + } +} + +async fn execute_swap_transaction( + provider: FillProvider< + JoinFill>, + ReqwestProvider, + Http, + Ethereum, + >, + amount_in: &BigUint, + wallet_address: Address, + sell_token_address: &Bytes, + tx: Transaction, +) -> Result<(), Box> { + println!("Executing by performing an approval (for permit2) and a swap transaction..."); + let (approval_request, swap_request) = get_tx_requests( + provider.clone(), + biguint_to_u256(amount_in), + wallet_address, + Address::from_slice(sell_token_address), + tx.clone(), + ) + .await; + + let approval_receipt = provider + .send_transaction(approval_request) + .await?; + + let approval_result = approval_receipt + .get_receipt() + .await?; + println!( + "Approval transaction sent with hash: {:?} and status: {:?}", + approval_result.transaction_hash, + approval_result.status() + ); + + let swap_receipt = provider + .send_transaction(swap_request) + .await?; + + let swap_result = swap_receipt + .get_receipt() + .await?; + println!( + "Swap transaction sent with hash: {:?} and status: {:?}", + swap_result.transaction_hash, + swap_result.status() + ); + + Ok(()) +} From ac75c99d98f5848110e17cda04bec0e849aa3e0d Mon Sep 17 00:00:00 2001 From: Markus Date: Thu, 27 Feb 2025 21:08:24 +0000 Subject: [PATCH 4/7] Remove Flashbots RPC Took 6 seconds --- examples/quickstart/main.rs | 59 +++++-------------------------------- 1 file changed, 8 insertions(+), 51 deletions(-) diff --git a/examples/quickstart/main.rs b/examples/quickstart/main.rs index e9d0df46..b75b4cf3 100644 --- a/examples/quickstart/main.rs +++ b/examples/quickstart/main.rs @@ -70,9 +70,6 @@ struct Cli { tvl_threshold: f64, #[arg(short, long, default_value = FAKE_PK)] swapper_pk: String, - /// Whether to use Flashbots Protect for transaction submission - #[arg(long, default_value_t = false)] - flashbots: bool, } #[tokio::main] @@ -156,22 +153,14 @@ async fn main() { .expect("Failed to private key signer"); let tx_signer = EthereumWallet::from(wallet.clone()); - let provider = { - let builder = ProviderBuilder::new().wallet(tx_signer.clone()); - - if cli.flashbots { - println!("Using Flashbots Protect for transaction submission"); - builder.on_http("https://rpc.flashbots.net/fast".parse().expect("Failed to parse Flashbots URL")) - } else { - println!("Using standard RPC for transaction submission"); - builder.on_http( - env::var("ETH_RPC_URL") - .expect("ETH_RPC_URL env var not set") - .parse() - .expect("Failed to parse ETH_RPC_URL"), - ) - } - }; + let provider = ProviderBuilder::new() + .wallet(tx_signer.clone()) + .on_http( + env::var("ETH_RPC_URL") + .expect("ETH_RPC_URL env var not set") + .parse() + .expect("Failed to parse ETH_RPC_URL"), + ); while let Some(message_result) = protocol_stream.next().await { let message = match message_result { @@ -232,38 +221,6 @@ async fn main() { "simulate" => { println!("Simulating by performing an approval (for permit2) and a swap transaction..."); - if cli.flashbots { - println!("Note: Transaction simulation is limited when using Flashbots Protect."); - println!("Flashbots only attempts to include transactions that won't revert."); - println!("Do you want to proceed with execution instead?"); - let yes_no_options = vec!["Yes", "No"]; - let yes_no_selection = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("Do you want to proceed with execution instead?") - .default(1) // Default to No - .items(&yes_no_options) - .interact() - .unwrap_or(1); // Default to No if error - - if yes_no_selection == 0 { - match execute_swap_transaction( - provider.clone(), - &amount_in, - wallet.address(), - &sell_token_address, - tx.clone(), - ).await { - Ok(_) => return, - Err(e) => { - eprintln!("Failed to execute transaction: {:?}", e); - continue; - } - } - } else { - println!("Skipping this swap..."); - continue; - } - } - let (approval_request, swap_request) = get_tx_requests( provider.clone(), biguint_to_u256(&amount_in), From c0993ebcbeed580831ad0b00f3b3125dc337af2e Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Fri, 28 Feb 2025 11:23:04 +0000 Subject: [PATCH 5/7] fix: Polish quickstart after Markus' changes --- don't change below this line --- ENG-4225 Took 8 minutes --- examples/quickstart/main.rs | 46 ++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/examples/quickstart/main.rs b/examples/quickstart/main.rs index b75b4cf3..e722cde8 100644 --- a/examples/quickstart/main.rs +++ b/examples/quickstart/main.rs @@ -22,6 +22,7 @@ use alloy::{ use alloy_primitives::{Address, Bytes as AlloyBytes, B256, U256}; use alloy_sol_types::SolValue; use clap::Parser; +use dialoguer::{theme::ColorfulTheme, Select}; use futures::StreamExt; use num_bigint::BigUint; use num_traits::ToPrimitive; @@ -53,7 +54,6 @@ use tycho_simulation::{ tycho_core::models::Chain, utils::load_all_tokens, }; -use dialoguer::{theme::ColorfulTheme, Select}; const FAKE_PK: &str = "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234"; @@ -200,7 +200,7 @@ async fn main() { println!("Signer private key was not provided. Skipping simulation/execution..."); continue } - println!("What would you like to do?"); + 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."); let options = vec!["Simulate the swap", "Execute the swap", "Skip this swap"]; @@ -254,7 +254,7 @@ async fn main() { ); } } - }, + } Err(e) => { eprintln!("Simulation failed: {:?}", e); println!("Your RPC provider does not support transaction simulation."); @@ -274,7 +274,9 @@ async fn main() { wallet.address(), &sell_token_address, tx.clone(), - ).await { + ) + .await + { Ok(_) => return, Err(e) => { eprintln!("Failed to execute transaction: {:?}", e); @@ -296,7 +298,9 @@ async fn main() { wallet.address(), &sell_token_address, tx, - ).await { + ) + .await + { Ok(_) => return, Err(e) => { eprintln!("Failed to execute transaction: {:?}", e); @@ -370,14 +374,21 @@ fn get_best_swap( println!("Pool address: {:?}", key); let formatted_in = format_token_amount(&amount_in, &sell_token); let formatted_out = format_token_amount(amount_out, &buy_token); - let (forward_price, reverse_price) = format_price_ratios(&amount_in, amount_out, &sell_token, &buy_token); + let (forward_price, reverse_price) = + format_price_ratios(&amount_in, amount_out, &sell_token, &buy_token); println!( "Swap: {} {} -> {} {} \nPrice: {:.6} {} per {}, {:.6} {} per {}", - formatted_in, sell_token.symbol, - formatted_out, buy_token.symbol, - forward_price, buy_token.symbol, sell_token.symbol, - reverse_price, sell_token.symbol, buy_token.symbol + formatted_in, + sell_token.symbol, + formatted_out, + buy_token.symbol, + forward_price, + buy_token.symbol, + sell_token.symbol, + reverse_price, + sell_token.symbol, + buy_token.symbol ); Some((key.to_string(), amount_out.clone())) } else { @@ -488,7 +499,7 @@ async fn get_tx_requests( (approval_request, swap_request) } -// Helper function to format token amounts to human-readable values +// Format token amounts to human-readable values fn format_token_amount(amount: &BigUint, token: &Token) -> String { let decimal_amount = amount.to_f64().unwrap_or(0.0) / 10f64.powi(token.decimals as i32); format!("{:.6}", decimal_amount) @@ -499,16 +510,13 @@ fn format_price_ratios( amount_in: &BigUint, amount_out: &BigUint, token_in: &Token, - token_out: &Token + token_out: &Token, ) -> (f64, f64) { - // Convert to f64 for calculation let decimal_in = amount_in.to_f64().unwrap_or(0.0) / 10f64.powi(token_in.decimals as i32); let decimal_out = amount_out.to_f64().unwrap_or(0.0) / 10f64.powi(token_out.decimals as i32); if decimal_in > 0.0 && decimal_out > 0.0 { - // Forward price: out per in let forward = decimal_out / decimal_in; - // Reverse price: in per out let reverse = decimal_in / decimal_out; (forward, reverse) } else { @@ -542,9 +550,7 @@ async fn execute_swap_transaction( .send_transaction(approval_request) .await?; - let approval_result = approval_receipt - .get_receipt() - .await?; + let approval_result = approval_receipt.get_receipt().await?; println!( "Approval transaction sent with hash: {:?} and status: {:?}", approval_result.transaction_hash, @@ -555,9 +561,7 @@ async fn execute_swap_transaction( .send_transaction(swap_request) .await?; - let swap_result = swap_receipt - .get_receipt() - .await?; + let swap_result = swap_receipt.get_receipt().await?; println!( "Swap transaction sent with hash: {:?} and status: {:?}", swap_result.transaction_hash, From b2961d0bc6d7ac99dfb0e59323b00a8cfaf78ac1 Mon Sep 17 00:00:00 2001 From: Markus Date: Fri, 28 Feb 2025 12:32:54 +0000 Subject: [PATCH 6/7] remove unused dialoguer --- examples/quickstart/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/quickstart/main.rs b/examples/quickstart/main.rs index 51cfe618..34abe055 100644 --- a/examples/quickstart/main.rs +++ b/examples/quickstart/main.rs @@ -54,7 +54,6 @@ use tycho_simulation::{ tycho_core::models::Chain, utils::load_all_tokens, }; -use dialoguer::{theme::ColorfulTheme, Select}; const FAKE_PK: &str = "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234"; From 0398fb64aeb07f3f0167bd5d6c82ec5572b51ad2 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Fri, 28 Feb 2025 13:52:14 +0000 Subject: [PATCH 7/7] chore: cargo format --- don't change below this line --- ENG-4225 Took 4 minutes --- examples/quickstart/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/quickstart/main.rs b/examples/quickstart/main.rs index 34abe055..e722cde8 100644 --- a/examples/quickstart/main.rs +++ b/examples/quickstart/main.rs @@ -229,7 +229,7 @@ async fn main() { tx.clone(), ) .await; - + let payload = SimulatePayload { block_state_calls: vec![SimBlock { block_overrides: None,