diff --git a/examples/encoding-example/main.rs b/examples/encoding-example/main.rs index 7ccc0bc..d60a144 100644 --- a/examples/encoding-example/main.rs +++ b/examples/encoding-example/main.rs @@ -6,9 +6,8 @@ use tycho_common::{ Bytes, }; use tycho_execution::encoding::{ - evm::encoder_builder::EVMEncoderBuilder, + evm::encoder_builders::TychoRouterEncoderBuilder, models::{Solution, Swap}, - tycho_encoder::TychoEncoder, }; fn main() { @@ -19,10 +18,9 @@ fn main() { .expect("Failed to create user address"); // Initialize the encoder - let encoder = EVMEncoderBuilder::new() + let encoder = TychoRouterEncoderBuilder::new() .chain(Chain::Ethereum) - .initialize_tycho_router_with_permit2(swapper_pk) - .expect("Failed to create encoder builder") + .swapper_pk(swapper_pk) .build() .expect("Failed to build encoder"); @@ -64,7 +62,7 @@ fn main() { // Encode the solution let tx = encoder - .encode_router_calldata(vec![solution.clone()]) + .encode_calldata(vec![solution.clone()]) .expect("Failed to encode router calldata")[0] .clone(); println!(" ====== Simple swap WETH -> USDC ======"); @@ -137,7 +135,7 @@ fn main() { // Encode the solution let complex_tx = encoder - .encode_router_calldata(vec![complex_solution]) + .encode_calldata(vec![complex_solution]) .expect("Failed to encode router calldata")[0] .clone(); diff --git a/src/bin/tycho-encode.rs b/src/bin/tycho-encode.rs index c0216fa..1edefd7 100644 --- a/src/bin/tycho-encode.rs +++ b/src/bin/tycho-encode.rs @@ -3,7 +3,9 @@ use std::io::{self, Read}; use clap::{Parser, Subcommand}; use tycho_common::{hex_bytes::Bytes, models::Chain}; use tycho_execution::encoding::{ - evm::encoder_builder::EVMEncoderBuilder, models::Solution, tycho_encoder::TychoEncoder, + evm::encoder_builders::{TychoExecutorEncoderBuilder, TychoRouterEncoderBuilder}, + models::Solution, + tycho_encoder::TychoEncoder, }; #[derive(Parser)] @@ -45,19 +47,16 @@ pub struct Cli { executors_file_path: Option, #[arg(short, long)] router_address: Option, + #[arg(short, long)] + swapper_pk: Option, } #[derive(Subcommand)] pub enum Commands { - /// Use the Tycho router encoding strategy + /// Use Tycho router encoding TychoRouter, - /// Use the Tycho router encoding strategy with Permit2 approval and token in transfer - TychoRouterPermit2 { - #[arg(short, long)] - swapper_pk: String, - }, - /// Use the direct execution encoding strategy - DirectExecution, + /// Use direct execution encoding + TychoExecutor, } fn main() -> Result<(), Box> { @@ -75,24 +74,26 @@ fn main() -> Result<(), Box> { } let solution: Solution = serde_json::from_str(&buffer)?; - let mut builder = EVMEncoderBuilder::new().chain(chain); - - if let Some(config_path) = cli.executors_file_path { - builder = builder.executors_file_path(config_path); - } - if let Some(router_address) = cli.router_address { - builder = builder.router_address(router_address); - } - - builder = match cli.command { - Commands::TychoRouter => builder.initialize_tycho_router()?, - Commands::TychoRouterPermit2 { swapper_pk } => { - builder.initialize_tycho_router_with_permit2(swapper_pk)? + let encoder: Box = match cli.command { + Commands::TychoRouter => { + let mut builder = TychoRouterEncoderBuilder::new().chain(chain); + if let Some(config_path) = cli.executors_file_path { + builder = builder.executors_file_path(config_path); + } + if let Some(router_address) = cli.router_address { + builder = builder.router_address(router_address); + } + if let Some(swapper_pk) = cli.swapper_pk { + builder = builder.swapper_pk(swapper_pk); + } + builder.build()? } - Commands::DirectExecution => builder.initialize_direct_execution()?, + Commands::TychoExecutor => TychoExecutorEncoderBuilder::new() + .chain(chain) + .build()?, }; - let encoder = builder.build()?; - let transactions = encoder.encode_router_calldata(vec![solution])?; + + let transactions = encoder.encode_calldata(vec![solution])?; let encoded = serde_json::json!({ "to": format!("0x{}", hex::encode(&transactions[0].to)), "value": format!("0x{}", hex::encode(transactions[0].value.to_bytes_be())), diff --git a/src/encoding/evm/encoder_builder.rs b/src/encoding/evm/encoder_builder.rs deleted file mode 100644 index e20c148..0000000 --- a/src/encoding/evm/encoder_builder.rs +++ /dev/null @@ -1,150 +0,0 @@ -use tycho_common::{models::Chain, Bytes}; - -use crate::encoding::{ - errors::EncodingError, - evm::{ - strategy_encoder::strategy_encoders::{ExecutorStrategyEncoder, SplitSwapStrategyEncoder}, - swap_encoder::swap_encoder_registry::SwapEncoderRegistry, - tycho_encoder::EVMTychoEncoder, - }, - strategy_encoder::StrategyEncoder, -}; - -/// Builder pattern for constructing an `EVMTychoEncoder` with customizable options. -/// -/// This struct allows setting a chain and strategy encoder before building the final encoder. -pub struct EVMEncoderBuilder { - strategy: Option>, - chain: Option, - executors_file_path: Option, - router_address: Option, -} - -impl Default for EVMEncoderBuilder { - fn default() -> Self { - Self::new() - } -} - -impl EVMEncoderBuilder { - pub fn new() -> Self { - EVMEncoderBuilder { - chain: None, - strategy: None, - executors_file_path: None, - router_address: None, - } - } - pub fn chain(mut self, chain: Chain) -> Self { - self.chain = Some(chain); - self - } - - /// Sets the `executors_file_path` manually. - /// If it's not set, the default path will be used (config/executor_addresses.json) - pub fn executors_file_path(mut self, executors_file_path: String) -> Self { - self.executors_file_path = Some(executors_file_path); - self - } - - /// Sets the `router_address` manually. - /// If it's not set, the default router address will be used (config/router_addresses.json) - pub fn router_address(mut self, router_address: Bytes) -> Self { - self.router_address = Some(router_address); - self - } - - /// Sets the `strategy_encoder` manually. - /// - /// **Note**: This method should not be used in combination with `tycho_router` or - /// `direct_execution`. - pub fn strategy_encoder(mut self, strategy: Box) -> Self { - self.strategy = Some(strategy); - self - } - - /// Shortcut method to initialize a `SplitSwapStrategyEncoder` without any approval nor token in - /// transfer. **Note**: Should not be used at the same time as `strategy_encoder`. - pub fn initialize_tycho_router(self) -> Result { - if let Some(chain) = self.chain { - let swap_encoder_registry = - SwapEncoderRegistry::new(self.executors_file_path.clone(), chain)?; - let strategy = Box::new(SplitSwapStrategyEncoder::new( - chain, - swap_encoder_registry, - None, - self.router_address.clone(), - )?); - Ok(EVMEncoderBuilder { - chain: Some(chain), - strategy: Some(strategy), - executors_file_path: self.executors_file_path, - router_address: self.router_address, - }) - } else { - Err(EncodingError::FatalError( - "Please set the chain before setting the tycho router".to_string(), - )) - } - } - - /// Shortcut method to initialize a `SplitSwapStrategyEncoder` with Permit2 approval and token - /// in transfer. **Note**: Should not be used at the same time as `strategy_encoder`. - pub fn initialize_tycho_router_with_permit2( - self, - swapper_pk: String, - ) -> Result { - if let Some(chain) = self.chain { - let swap_encoder_registry = - SwapEncoderRegistry::new(self.executors_file_path.clone(), chain)?; - let strategy = Box::new(SplitSwapStrategyEncoder::new( - chain, - swap_encoder_registry, - Some(swapper_pk), - self.router_address.clone(), - )?); - Ok(EVMEncoderBuilder { - chain: Some(chain), - strategy: Some(strategy), - executors_file_path: self.executors_file_path, - router_address: self.router_address, - }) - } else { - Err(EncodingError::FatalError( - "Please set the chain before setting the tycho router".to_string(), - )) - } - } - - /// Shortcut method to initialize an `ExecutorStrategyEncoder`. - /// **Note**: Should not be used at the same time as `strategy_encoder`. - pub fn initialize_direct_execution(self) -> Result { - if let Some(chain) = self.chain { - let swap_encoder_registry = - SwapEncoderRegistry::new(self.executors_file_path.clone(), chain)?; - let strategy = Box::new(ExecutorStrategyEncoder::new(swap_encoder_registry)); - Ok(EVMEncoderBuilder { - chain: Some(chain), - strategy: Some(strategy), - executors_file_path: self.executors_file_path, - router_address: self.router_address, - }) - } else { - Err(EncodingError::FatalError( - "Please set the chain before setting the strategy".to_string(), - )) - } - } - - /// Builds the `EVMTychoEncoder` instance using the configured chain and strategy. - /// Returns an error if either the chain or strategy has not been set. - pub fn build(self) -> Result { - if let (Some(chain), Some(strategy)) = (self.chain, self.strategy) { - EVMTychoEncoder::new(chain, strategy) - } else { - Err(EncodingError::FatalError( - "Please set the chain and strategy before building the encoder".to_string(), - )) - } - } -} diff --git a/src/encoding/evm/encoder_builders.rs b/src/encoding/evm/encoder_builders.rs new file mode 100644 index 0000000..13e3355 --- /dev/null +++ b/src/encoding/evm/encoder_builders.rs @@ -0,0 +1,137 @@ +use tycho_common::{models::Chain, Bytes}; + +use crate::encoding::{ + errors::EncodingError, + evm::{ + strategy_encoder::strategy_encoders::SplitSwapStrategyEncoder, + swap_encoder::swap_encoder_registry::SwapEncoderRegistry, + tycho_encoders::{TychoExecutorEncoder, TychoRouterEncoder}, + }, + strategy_encoder::StrategyEncoder, + tycho_encoder::TychoEncoder, +}; + +/// Builder pattern for constructing a `TychoRouterEncoder` with customizable options. +/// +/// This struct allows setting a chain and strategy encoder before building the final encoder. +pub struct TychoRouterEncoderBuilder { + swapper_pk: Option, + strategy: Option>, + chain: Option, + executors_file_path: Option, + router_address: Option, +} + +impl Default for TychoRouterEncoderBuilder { + fn default() -> Self { + Self::new() + } +} + +impl TychoRouterEncoderBuilder { + pub fn new() -> Self { + TychoRouterEncoderBuilder { + swapper_pk: None, + chain: None, + strategy: None, + executors_file_path: None, + router_address: None, + } + } + pub fn chain(mut self, chain: Chain) -> Self { + self.chain = Some(chain); + self + } + + /// Sets the `executors_file_path` manually. + /// If it's not set, the default path will be used (config/executor_addresses.json) + pub fn executors_file_path(mut self, executors_file_path: String) -> Self { + self.executors_file_path = Some(executors_file_path); + self + } + + /// Sets the `router_address` manually. + /// If it's not set, the default router address will be used (config/router_addresses.json) + pub fn router_address(mut self, router_address: Bytes) -> Self { + self.router_address = Some(router_address); + self + } + + pub fn swapper_pk(mut self, swapper_pk: String) -> Self { + self.swapper_pk = Some(swapper_pk); + self + } + + /// Sets the `strategy_encoder` manually. + /// + /// **Note**: This method should not be used in combination with `tycho_router` or + /// `direct_execution`. + pub fn strategy_encoder(mut self, strategy: Box) -> Self { + self.strategy = Some(strategy); + self + } + + /// Builds the `TychoRouterEncoder` instance using the configured chain and strategy. + /// Returns an error if either the chain or strategy has not been set. + pub fn build(self) -> Result, EncodingError> { + if let Some(chain) = self.chain { + let swap_encoder_registry = + SwapEncoderRegistry::new(self.executors_file_path.clone(), chain)?; + + let strategy = Box::new(SplitSwapStrategyEncoder::new( + chain, + swap_encoder_registry, + self.swapper_pk, + self.router_address.clone(), + )?); + Ok(Box::new(TychoRouterEncoder::new(chain, strategy)?)) + } else { + Err(EncodingError::FatalError( + "Please set the chain and strategy before building the encoder".to_string(), + )) + } + } +} + +/// Builder pattern for constructing a `TychoExecutorEncoder` with customizable options. +pub struct TychoExecutorEncoderBuilder { + chain: Option, + executors_file_path: Option, +} + +impl Default for TychoExecutorEncoderBuilder { + fn default() -> Self { + Self::new() + } +} + +impl TychoExecutorEncoderBuilder { + pub fn new() -> Self { + TychoExecutorEncoderBuilder { chain: None, executors_file_path: None } + } + pub fn chain(mut self, chain: Chain) -> Self { + self.chain = Some(chain); + self + } + + /// Sets the `executors_file_path` manually. + /// If it's not set, the default path will be used (config/executor_addresses.json) + pub fn executors_file_path(mut self, executors_file_path: String) -> Self { + self.executors_file_path = Some(executors_file_path); + self + } + + /// Builds the `TychoExecutorEncoder` instance using the configured chain and strategy. + /// Returns an error if either the chain or strategy has not been set. + pub fn build(self) -> Result, EncodingError> { + if let Some(chain) = self.chain { + let swap_encoder_registry = + SwapEncoderRegistry::new(self.executors_file_path.clone(), chain)?; + Ok(Box::new(TychoExecutorEncoder::new(chain, swap_encoder_registry)?)) + } else { + Err(EncodingError::FatalError( + "Please set the chain and strategy before building the encoder".to_string(), + )) + } + } +} diff --git a/src/encoding/evm/strategy_encoder/group_swaps.rs b/src/encoding/evm/group_swaps.rs similarity index 100% rename from src/encoding/evm/strategy_encoder/group_swaps.rs rename to src/encoding/evm/group_swaps.rs diff --git a/src/encoding/evm/mod.rs b/src/encoding/evm/mod.rs index b86b4dd..344aaa1 100644 --- a/src/encoding/evm/mod.rs +++ b/src/encoding/evm/mod.rs @@ -1,7 +1,8 @@ pub mod approvals; mod constants; -pub mod encoder_builder; +pub mod encoder_builders; +mod group_swaps; pub mod strategy_encoder; mod swap_encoder; -pub mod tycho_encoder; +pub mod tycho_encoders; pub mod utils; diff --git a/src/encoding/evm/strategy_encoder/mod.rs b/src/encoding/evm/strategy_encoder/mod.rs index 14137b6..8f53524 100644 --- a/src/encoding/evm/strategy_encoder/mod.rs +++ b/src/encoding/evm/strategy_encoder/mod.rs @@ -1,3 +1,2 @@ -mod group_swaps; pub mod strategy_encoders; mod strategy_validators; diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 519045b..94d55a1 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -12,9 +12,9 @@ use crate::encoding::{ evm::{ approvals::permit2::Permit2, constants::DEFAULT_ROUTERS_JSON, - strategy_encoder::{ - group_swaps::group_swaps, - strategy_validators::{SequentialSwapValidator, SplitSwapValidator, SwapValidator}, + group_swaps::group_swaps, + strategy_encoder::strategy_validators::{ + SequentialSwapValidator, SplitSwapValidator, SwapValidator, }, swap_encoder::swap_encoder_registry::SwapEncoderRegistry, utils::{ @@ -592,77 +592,6 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { } } -/// This strategy encoder is used for solutions that are sent directly to the executor, bypassing -/// the router. Only one solution with one swap is supported. -/// -/// # Fields -/// * `swap_encoder_registry`: SwapEncoderRegistry, containing all possible swap encoders -#[derive(Clone)] -pub struct ExecutorStrategyEncoder { - swap_encoder_registry: SwapEncoderRegistry, -} - -impl ExecutorStrategyEncoder { - pub fn new(swap_encoder_registry: SwapEncoderRegistry) -> Self { - Self { swap_encoder_registry } - } -} - -impl StrategyEncoder for ExecutorStrategyEncoder { - fn encode_strategy(&self, solution: Solution) -> Result<(Vec, Bytes), EncodingError> { - let grouped_swaps = group_swaps(solution.clone().swaps); - let number_of_groups = grouped_swaps.len(); - if number_of_groups > 1 { - return Err(EncodingError::InvalidInput(format!( - "Executor strategy only supports one swap for non-groupable protocols. Found {}", - number_of_groups - ))) - } - - let grouped_swap = grouped_swaps - .first() - .ok_or_else(|| EncodingError::FatalError("Swap grouping failed".to_string()))?; - - let receiver = solution.receiver; - - let swap_encoder = self - .get_swap_encoder(&grouped_swap.protocol_system) - .ok_or_else(|| { - EncodingError::InvalidInput(format!( - "Swap encoder not found for protocol: {}", - grouped_swap.protocol_system - )) - })?; - - let mut grouped_protocol_data: Vec = vec![]; - for swap in grouped_swap.swaps.iter() { - let encoding_context = EncodingContext { - receiver: receiver.clone(), - exact_out: solution.exact_out, - router_address: None, - group_token_in: grouped_swap.input_token.clone(), - group_token_out: grouped_swap.output_token.clone(), - }; - let protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context.clone())?; - grouped_protocol_data.extend(protocol_data); - } - - let executor_address = Bytes::from_str(swap_encoder.executor_address()) - .map_err(|_| EncodingError::FatalError("Invalid executor address".to_string()))?; - - Ok((grouped_protocol_data, executor_address)) - } - - fn get_swap_encoder(&self, protocol_system: &str) -> Option<&Box> { - self.swap_encoder_registry - .get_encoder(protocol_system) - } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } -} - #[cfg(test)] mod tests { use std::{collections::HashMap, str::FromStr}; @@ -697,198 +626,6 @@ mod tests { .unwrap() } - #[test] - fn test_executor_strategy_encode() { - let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = ExecutorStrategyEncoder::new(swap_encoder_registry); - - let token_in = weth(); - let token_out = Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f"); - - let swap = Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - }; - - let solution = Solution { - exact_out: false, - given_token: token_in, - given_amount: BigUint::from(1000000000000000000u64), - expected_amount: Some(BigUint::from(1000000000000000000u64)), - checked_token: token_out, - checked_amount: None, - sender: Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(), - // The receiver was generated with `makeAddr("bob") using forge` - receiver: Bytes::from_str("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e").unwrap(), - swaps: vec![swap], - slippage: None, - native_action: None, - }; - - let (protocol_data, executor_address) = encoder - .encode_strategy(solution) - .unwrap(); - let hex_protocol_data = encode(&protocol_data); - assert_eq!( - executor_address, - Bytes::from_str("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").unwrap() - ); - assert_eq!( - hex_protocol_data, - String::from(concat!( - // in token - "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - // component id - "a478c2975ab1ea89e8196811f51a7b7ade33eb11", - // receiver - "1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e", - // zero for one - "00", - )) - ); - } - - #[test] - fn test_executor_strategy_encode_too_many_swaps() { - let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = ExecutorStrategyEncoder::new(swap_encoder_registry); - - let token_in = weth(); - let token_out = Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f"); - - let swap = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - }; - - let solution = Solution { - exact_out: false, - given_token: token_in, - given_amount: BigUint::from(1000000000000000000u64), - expected_amount: Some(BigUint::from(1000000000000000000u64)), - checked_token: token_out, - checked_amount: None, - sender: Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(), - receiver: Bytes::from_str("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e").unwrap(), - swaps: vec![swap.clone(), swap], - slippage: None, - native_action: None, - }; - - let result = encoder.encode_strategy(solution); - assert!(result.is_err()); - } - - #[test] - fn test_executor_strategy_encode_grouped_swaps() { - let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = ExecutorStrategyEncoder::new(swap_encoder_registry); - - let eth = eth(); - let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); - let pepe = Bytes::from_str("0x6982508145454Ce325dDbE47a25d4ec3d2311933").unwrap(); - - // Fee and tick spacing information for this test is obtained by querying the - // USV4 Position Manager contract: 0xbd216513d74c8cf14cf4747e6aaa6420ff64ee9e - // Using the poolKeys function with the first 25 bytes of the pool id - let pool_fee_usdc_eth = Bytes::from(BigInt::from(3000).to_signed_bytes_be()); - let tick_spacing_usdc_eth = Bytes::from(BigInt::from(60).to_signed_bytes_be()); - let mut static_attributes_usdc_eth: HashMap = HashMap::new(); - static_attributes_usdc_eth.insert("key_lp_fee".into(), pool_fee_usdc_eth); - static_attributes_usdc_eth.insert("tick_spacing".into(), tick_spacing_usdc_eth); - - let pool_fee_eth_pepe = Bytes::from(BigInt::from(25000).to_signed_bytes_be()); - let tick_spacing_eth_pepe = Bytes::from(BigInt::from(500).to_signed_bytes_be()); - let mut static_attributes_eth_pepe: HashMap = HashMap::new(); - static_attributes_eth_pepe.insert("key_lp_fee".into(), pool_fee_eth_pepe); - static_attributes_eth_pepe.insert("tick_spacing".into(), tick_spacing_eth_pepe); - - let swap_usdc_eth = Swap { - component: ProtocolComponent { - id: "0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d" - .to_string(), - protocol_system: "uniswap_v4".to_string(), - static_attributes: static_attributes_usdc_eth, - ..Default::default() - }, - token_in: usdc.clone(), - token_out: eth.clone(), - split: 0f64, - }; - - let swap_eth_pepe = Swap { - component: ProtocolComponent { - id: "0xecd73ecbf77219f21f129c8836d5d686bbc27d264742ddad620500e3e548e2c9" - .to_string(), - protocol_system: "uniswap_v4".to_string(), - static_attributes: static_attributes_eth_pepe, - ..Default::default() - }, - token_in: eth.clone(), - token_out: pepe.clone(), - split: 0f64, - }; - - let solution = Solution { - exact_out: false, - given_token: usdc, - given_amount: BigUint::from_str("1000_000000").unwrap(), - checked_token: pepe, - expected_amount: Some(BigUint::from_str("105_152_000000000000000000").unwrap()), - checked_amount: None, - slippage: None, - sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - swaps: vec![swap_usdc_eth, swap_eth_pepe], - ..Default::default() - }; - - let (protocol_data, executor_address) = encoder - .encode_strategy(solution) - .unwrap(); - let hex_protocol_data = encode(&protocol_data); - assert_eq!( - executor_address, - Bytes::from_str("0xf62849f9a0b5bf2913b396098f7c7019b51a820a").unwrap() - ); - assert_eq!( - hex_protocol_data, - String::from(concat!( - // group in token - "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - // group out token - "6982508145454ce325ddbe47a25d4ec3d2311933", - // zero for one - "00", - // executor address - "f62849f9a0b5bf2913b396098f7c7019b51a820a", - // first pool intermediary token (ETH) - "0000000000000000000000000000000000000000", - // fee - "000bb8", - // tick spacing - "00003c", - // second pool intermediary token (PEPE) - "6982508145454ce325ddbe47a25d4ec3d2311933", - // fee - "0061a8", - // tick spacing - "0001f4" - )) - ); - } - #[rstest] #[case::with_check_no_slippage( None, diff --git a/src/encoding/evm/tycho_encoder.rs b/src/encoding/evm/tycho_encoder.rs deleted file mode 100644 index 85f541f..0000000 --- a/src/encoding/evm/tycho_encoder.rs +++ /dev/null @@ -1,720 +0,0 @@ -use std::collections::HashSet; - -use num_bigint::BigUint; -use tycho_common::Bytes; - -use crate::encoding::{ - errors::EncodingError, - models::{Chain, NativeAction, Solution, Transaction}, - strategy_encoder::StrategyEncoder, - tycho_encoder::TychoEncoder, -}; - -/// Represents an encoder for a swap using any strategy supported by the strategy registry. -/// -/// # Fields -/// * `strategy_encoder`: Strategy encoder to follow for encoding the solution -/// * `native_address`: Address of the chain's native token -/// * `wrapped_address`: Address of the chain's wrapped native token -pub struct EVMTychoEncoder { - strategy_encoder: Box, - native_address: Bytes, - wrapped_address: Bytes, -} - -impl Clone for EVMTychoEncoder { - fn clone(&self) -> Self { - Self { - strategy_encoder: self.strategy_encoder.clone_box(), - native_address: self.native_address.clone(), - wrapped_address: self.wrapped_address.clone(), - } - } -} - -impl EVMTychoEncoder { - pub fn new( - chain: tycho_common::models::Chain, - strategy_encoder: Box, - ) -> Result { - let chain: Chain = Chain::from(chain); - let native_address = chain.native_token()?; - let wrapped_address = chain.wrapped_token()?; - Ok(EVMTychoEncoder { strategy_encoder, native_address, wrapped_address }) - } -} - -impl EVMTychoEncoder { - /// Raises an `EncodingError` if the solution is not considered valid. - /// - /// A solution is considered valid if all the following conditions are met: - /// * The solution is not exact out. - /// * The solution has at least one swap. - /// * If the solution is wrapping, the given token is the chain's native token and the first - /// swap's input is the chain's wrapped token. - /// * If the solution is unwrapping, the checked token is the chain's native token and the last - /// swap's output is the chain's wrapped token. - /// * The token cannot appear more than once in the solution unless it is the first and last - /// token (i.e. a true cyclical swap). - fn validate_solution(&self, solution: &Solution) -> Result<(), EncodingError> { - if solution.exact_out { - return Err(EncodingError::FatalError( - "Currently only exact input solutions are supported".to_string(), - )); - } - if solution.swaps.is_empty() { - return Err(EncodingError::FatalError("No swaps found in solution".to_string())); - } - if let Some(native_action) = solution.clone().native_action { - if native_action == NativeAction::Wrap { - if solution.given_token != self.native_address { - return Err(EncodingError::FatalError( - "Native token must be the input token in order to wrap".to_string(), - )); - } - if let Some(first_swap) = solution.swaps.first() { - if first_swap.token_in != self.wrapped_address { - return Err(EncodingError::FatalError( - "Wrapped token must be the first swap's input in order to wrap" - .to_string(), - )); - } - } - } else if native_action == NativeAction::Unwrap { - if solution.checked_token != self.native_address { - return Err(EncodingError::FatalError( - "Native token must be the output token in order to unwrap".to_string(), - )); - } - if let Some(last_swap) = solution.swaps.last() { - if last_swap.token_out != self.wrapped_address { - return Err(EncodingError::FatalError( - "Wrapped token must be the last swap's output in order to unwrap" - .to_string(), - )); - } - } - } - } - - let mut solution_tokens = vec![]; - let mut split_tokens_already_considered = HashSet::new(); - for (i, swap) in solution.swaps.iter().enumerate() { - // so we don't count the split tokens more than once - if swap.split != 0.0 { - if !split_tokens_already_considered.contains(&swap.token_in) { - solution_tokens.push(swap.token_in.clone()); - split_tokens_already_considered.insert(swap.token_in.clone()); - } - } else { - // it might be the last swap of the split or a regular swap - if !split_tokens_already_considered.contains(&swap.token_in) { - solution_tokens.push(swap.token_in.clone()); - } - } - if i == solution.swaps.len() - 1 { - solution_tokens.push(swap.token_out.clone()); - } - } - - if solution_tokens.len() != - solution_tokens - .iter() - .cloned() - .collect::>() - .len() - { - if let Some(last_swap) = solution.swaps.last() { - if solution.swaps[0].token_in != last_swap.token_out { - return Err(EncodingError::FatalError( - "Cyclical swaps are only allowed if they are the first and last token of a solution".to_string(), - )); - } else { - // it is a valid cyclical swap - // we don't support any wrapping or unwrapping in this case - if let Some(_native_action) = solution.clone().native_action { - return Err(EncodingError::FatalError( - "Wrapping/Unwrapping is not available in cyclical swaps".to_string(), - )); - } - } - } - } - Ok(()) - } -} - -impl TychoEncoder for EVMTychoEncoder { - fn encode_router_calldata( - &self, - solutions: Vec, - ) -> Result, EncodingError> { - let mut transactions: Vec = Vec::new(); - for solution in solutions.iter() { - self.validate_solution(solution)?; - - let (contract_interaction, target_address) = self - .strategy_encoder - .encode_strategy(solution.clone())?; - - let value = if solution.given_token == self.native_address { - solution.given_amount.clone() - } else { - BigUint::ZERO - }; - - transactions.push(Transaction { - value, - data: contract_interaction, - to: target_address, - }); - } - Ok(transactions) - } -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use tycho_common::models::{protocol::ProtocolComponent, Chain as TychoCoreChain}; - - use super::*; - use crate::encoding::{ - models::Swap, strategy_encoder::StrategyEncoder, swap_encoder::SwapEncoder, - }; - - fn dai() -> Bytes { - Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap() - } - - fn eth() -> Bytes { - Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap() - } - - fn weth() -> Bytes { - Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap() - } - - fn usdc() -> Bytes { - Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap() - } - - fn wbtc() -> Bytes { - Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap() - } - - #[derive(Clone)] - struct MockStrategy; - - impl StrategyEncoder for MockStrategy { - fn encode_strategy(&self, _solution: Solution) -> Result<(Vec, Bytes), EncodingError> { - Ok(( - Bytes::from_str("0x1234") - .unwrap() - .to_vec(), - Bytes::from_str("0xabcd").unwrap(), - )) - } - - fn get_swap_encoder(&self, _protocol_system: &str) -> Option<&Box> { - None - } - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } - } - - fn get_mocked_tycho_encoder() -> EVMTychoEncoder { - let strategy_encoder = Box::new(MockStrategy {}); - EVMTychoEncoder::new(TychoCoreChain::Ethereum, strategy_encoder).unwrap() - } - - #[test] - fn test_encode_router_calldata() { - let encoder = get_mocked_tycho_encoder(); - let eth_amount_in = BigUint::from(1000u32); - let swap = Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: weth(), - token_out: dai(), - split: 0f64, - }; - - let solution = Solution { - exact_out: false, - given_amount: eth_amount_in.clone(), - given_token: eth(), - swaps: vec![swap], - native_action: Some(NativeAction::Wrap), - ..Default::default() - }; - - let transactions = encoder.encode_router_calldata(vec![solution]); - - assert!(transactions.is_ok()); - let transactions = transactions.unwrap(); - assert_eq!(transactions.len(), 1); - assert_eq!(transactions[0].value, eth_amount_in); - assert_eq!(transactions[0].data, Bytes::from_str("0x1234").unwrap()); - assert_eq!(transactions[0].to, Bytes::from_str("0xabcd").unwrap()); - } - - #[test] - fn test_validate_fails_for_exact_out() { - let encoder = get_mocked_tycho_encoder(); - let solution = Solution { - exact_out: true, // This should cause an error - ..Default::default() - }; - let result = encoder.validate_solution(&solution); - - assert!(result.is_err()); - assert_eq!( - result.err().unwrap(), - EncodingError::FatalError( - "Currently only exact input solutions are supported".to_string() - ) - ); - } - - #[test] - fn test_validate_passes_for_wrap() { - let encoder = get_mocked_tycho_encoder(); - let swap = Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: weth(), - token_out: dai(), - split: 0f64, - }; - - let solution = Solution { - exact_out: false, - given_token: eth(), - checked_token: dai(), - checked_amount: None, - swaps: vec![swap], - native_action: Some(NativeAction::Wrap), - ..Default::default() - }; - - let result = encoder.validate_solution(&solution); - - assert!(result.is_ok()); - } - - #[test] - fn test_validate_fails_for_wrap_wrong_input() { - let encoder = get_mocked_tycho_encoder(); - let swap = Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: weth(), - token_out: dai(), - split: 0f64, - }; - - let solution = Solution { - exact_out: false, - given_token: weth(), - swaps: vec![swap], - native_action: Some(NativeAction::Wrap), - ..Default::default() - }; - - let result = encoder.validate_solution(&solution); - - assert!(result.is_err()); - assert_eq!( - result.err().unwrap(), - EncodingError::FatalError( - "Native token must be the input token in order to wrap".to_string() - ) - ); - } - - #[test] - fn test_validate_fails_for_wrap_wrong_first_swap() { - let encoder = get_mocked_tycho_encoder(); - let swap = Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: eth(), - token_out: dai(), - split: 0f64, - }; - - let solution = Solution { - exact_out: false, - given_token: eth(), - swaps: vec![swap], - native_action: Some(NativeAction::Wrap), - ..Default::default() - }; - - let result = encoder.validate_solution(&solution); - - assert!(result.is_err()); - assert_eq!( - result.err().unwrap(), - EncodingError::FatalError( - "Wrapped token must be the first swap's input in order to wrap".to_string() - ) - ); - } - - #[test] - fn test_validate_fails_no_swaps() { - let encoder = get_mocked_tycho_encoder(); - let solution = Solution { - exact_out: false, - given_token: eth(), - swaps: vec![], - native_action: Some(NativeAction::Wrap), - ..Default::default() - }; - - let result = encoder.validate_solution(&solution); - - assert!(result.is_err()); - assert_eq!( - result.err().unwrap(), - EncodingError::FatalError("No swaps found in solution".to_string()) - ); - } - - #[test] - fn test_validate_passes_for_unwrap() { - let encoder = get_mocked_tycho_encoder(); - let swap = Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: dai(), - token_out: weth(), - split: 0f64, - }; - - let solution = Solution { - exact_out: false, - checked_token: eth(), - checked_amount: None, - swaps: vec![swap], - native_action: Some(NativeAction::Unwrap), - ..Default::default() - }; - - let result = encoder.validate_solution(&solution); - - assert!(result.is_ok()); - } - - #[test] - fn test_validate_fails_for_unwrap_wrong_output() { - let encoder = get_mocked_tycho_encoder(); - let swap = Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: dai(), - token_out: weth(), - split: 0f64, - }; - - let solution = Solution { - exact_out: false, - given_token: dai(), - checked_token: weth(), - swaps: vec![swap], - native_action: Some(NativeAction::Unwrap), - ..Default::default() - }; - - let result = encoder.validate_solution(&solution); - - assert!(result.is_err()); - assert_eq!( - result.err().unwrap(), - EncodingError::FatalError( - "Native token must be the output token in order to unwrap".to_string() - ) - ); - } - - #[test] - fn test_validate_fails_for_unwrap_wrong_last_swap() { - let encoder = get_mocked_tycho_encoder(); - let swap = Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: dai(), - token_out: eth(), - split: 0f64, - }; - - let solution = Solution { - exact_out: false, - checked_token: eth(), - swaps: vec![swap], - native_action: Some(NativeAction::Unwrap), - ..Default::default() - }; - - let result = encoder.validate_solution(&solution); - - assert!(result.is_err()); - assert_eq!( - result.err().unwrap(), - EncodingError::FatalError( - "Wrapped token must be the last swap's output in order to unwrap".to_string() - ) - ); - } - - #[test] - fn test_validate_cyclical_swap() { - // This validation passes because the cyclical swap is the first and last token - // 50% -> WETH - // DAI - -> DAI - // 50% -> WETH - // (some of the pool addresses in this test are fake) - let encoder = get_mocked_tycho_encoder(); - let swaps = vec![ - Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: dai(), - token_out: weth(), - split: 0.5f64, - }, - Swap { - component: ProtocolComponent { - id: "0x0000000000000000000000000000000000000000".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: dai(), - token_out: weth(), - split: 0f64, - }, - Swap { - component: ProtocolComponent { - id: "0x0000000000000000000000000000000000000000".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: weth(), - token_out: dai(), - split: 0f64, - }, - ]; - - let solution = Solution { - exact_out: false, - given_token: dai(), - checked_token: dai(), - swaps, - ..Default::default() - }; - - let result = encoder.validate_solution(&solution); - - assert!(result.is_ok()); - } - - #[test] - fn test_validate_cyclical_swap_fail() { - // This test should fail because the cyclical swap is not the first and last token - // DAI -> WETH -> USDC -> DAI -> WBTC - // (some of the pool addresses in this test are fake) - let encoder = get_mocked_tycho_encoder(); - let swaps = vec![ - Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: dai(), - token_out: weth(), - split: 0f64, - }, - Swap { - component: ProtocolComponent { - id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: weth(), - token_out: usdc(), - split: 0f64, - }, - Swap { - component: ProtocolComponent { - id: "0x0000000000000000000000000000000000000000".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: usdc(), - token_out: dai(), - split: 0f64, - }, - Swap { - component: ProtocolComponent { - id: "0x0000000000000000000000000000000000000000".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: dai(), - token_out: wbtc(), - split: 0f64, - }, - ]; - - let solution = Solution { - exact_out: false, - given_token: dai(), - checked_token: wbtc(), - swaps, - ..Default::default() - }; - - let result = encoder.validate_solution(&solution); - - assert!(result.is_err()); - assert_eq!( - result.err().unwrap(), - EncodingError::FatalError( - "Cyclical swaps are only allowed if they are the first and last token of a solution".to_string() - ) - ); - } - #[test] - fn test_validate_cyclical_swap_split_output() { - // This validation passes because it is a valid cyclical swap - // -> WETH - // WETH -> DAI - // -> WETH - // (some of the pool addresses in this test are fake) - let encoder = get_mocked_tycho_encoder(); - let swaps = vec![ - Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: weth(), - token_out: dai(), - split: 0f64, - }, - Swap { - component: ProtocolComponent { - id: "0x0000000000000000000000000000000000000000".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: dai(), - token_out: weth(), - split: 0.5f64, - }, - Swap { - component: ProtocolComponent { - id: "0x0000000000000000000000000000000000000000".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: dai(), - token_out: weth(), - split: 0f64, - }, - ]; - - let solution = Solution { - exact_out: false, - given_token: weth(), - checked_token: weth(), - swaps, - ..Default::default() - }; - - let result = encoder.validate_solution(&solution); - - assert!(result.is_ok()); - } - - #[test] - fn test_validate_cyclical_swap_native_action_fail() { - // This validation fails because there is a native action with a valid cyclical swap - // ETH -> WETH -> DAI -> WETH - // (some of the pool addresses in this test are fake) - let encoder = get_mocked_tycho_encoder(); - let swaps = vec![ - Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: weth(), - token_out: dai(), - split: 0f64, - }, - Swap { - component: ProtocolComponent { - id: "0x0000000000000000000000000000000000000000".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: dai(), - token_out: weth(), - split: 0f64, - }, - ]; - - let solution = Solution { - exact_out: false, - given_token: eth(), - checked_token: weth(), - swaps, - native_action: Some(NativeAction::Wrap), - ..Default::default() - }; - - let result = encoder.validate_solution(&solution); - - assert!(result.is_err()); - assert_eq!( - result.err().unwrap(), - EncodingError::FatalError( - "Wrapping/Unwrapping is not available in cyclical swaps" - .to_string() - .to_string() - ) - ); - } -} diff --git a/src/encoding/evm/tycho_encoders.rs b/src/encoding/evm/tycho_encoders.rs new file mode 100644 index 0000000..6176786 --- /dev/null +++ b/src/encoding/evm/tycho_encoders.rs @@ -0,0 +1,1041 @@ +use std::{collections::HashSet, str::FromStr}; + +use num_bigint::BigUint; +use tycho_common::Bytes; + +use crate::encoding::{ + errors::EncodingError, + evm::{group_swaps::group_swaps, swap_encoder::swap_encoder_registry::SwapEncoderRegistry}, + models::{Chain, EncodingContext, NativeAction, Solution, Transaction}, + strategy_encoder::StrategyEncoder, + tycho_encoder::TychoEncoder, +}; + +/// Encodes solutions to be used by the TychoRouter. +/// +/// # Fields +/// * `strategy_encoder`: Strategy encoder to follow for encoding the solution +/// * `native_address`: Address of the chain's native token +/// * `wrapped_address`: Address of the chain's wrapped native token +pub struct TychoRouterEncoder { + strategy_encoder: Box, + native_address: Bytes, + wrapped_address: Bytes, +} + +impl TychoRouterEncoder { + pub fn new( + chain: tycho_common::models::Chain, + strategy_encoder: Box, + ) -> Result { + let chain: Chain = Chain::from(chain); + let native_address = chain.native_token()?; + let wrapped_address = chain.wrapped_token()?; + Ok(TychoRouterEncoder { strategy_encoder, native_address, wrapped_address }) + } +} + +impl TychoEncoder for TychoRouterEncoder { + fn encode_calldata(&self, solutions: Vec) -> Result, EncodingError> { + let mut transactions: Vec = Vec::new(); + for solution in solutions.iter() { + self.validate_solution(solution)?; + + let (contract_interaction, target_address) = self + .strategy_encoder + .encode_strategy(solution.clone())?; + + let value = if solution.given_token == self.native_address { + solution.given_amount.clone() + } else { + BigUint::ZERO + }; + + transactions.push(Transaction { + value, + data: contract_interaction, + to: target_address, + }); + } + Ok(transactions) + } + + /// Raises an `EncodingError` if the solution is not considered valid. + /// + /// A solution is considered valid if all the following conditions are met: + /// * The solution is not exact out. + /// * The solution has at least one swap. + /// * If the solution is wrapping, the given token is the chain's native token and the first + /// swap's input is the chain's wrapped token. + /// * If the solution is unwrapping, the checked token is the chain's native token and the last + /// swap's output is the chain's wrapped token. + /// * The token cannot appear more than once in the solution unless it is the first and last + /// token (i.e. a true cyclical swap). + fn validate_solution(&self, solution: &Solution) -> Result<(), EncodingError> { + if solution.exact_out { + return Err(EncodingError::FatalError( + "Currently only exact input solutions are supported".to_string(), + )); + } + if solution.swaps.is_empty() { + return Err(EncodingError::FatalError("No swaps found in solution".to_string())); + } + if let Some(native_action) = solution.clone().native_action { + if native_action == NativeAction::Wrap { + if solution.given_token != self.native_address { + return Err(EncodingError::FatalError( + "Native token must be the input token in order to wrap".to_string(), + )); + } + if let Some(first_swap) = solution.swaps.first() { + if first_swap.token_in != self.wrapped_address { + return Err(EncodingError::FatalError( + "Wrapped token must be the first swap's input in order to wrap" + .to_string(), + )); + } + } + } else if native_action == NativeAction::Unwrap { + if solution.checked_token != self.native_address { + return Err(EncodingError::FatalError( + "Native token must be the output token in order to unwrap".to_string(), + )); + } + if let Some(last_swap) = solution.swaps.last() { + if last_swap.token_out != self.wrapped_address { + return Err(EncodingError::FatalError( + "Wrapped token must be the last swap's output in order to unwrap" + .to_string(), + )); + } + } + } + } + + let mut solution_tokens = vec![]; + let mut split_tokens_already_considered = HashSet::new(); + for (i, swap) in solution.swaps.iter().enumerate() { + // so we don't count the split tokens more than once + if swap.split != 0.0 { + if !split_tokens_already_considered.contains(&swap.token_in) { + solution_tokens.push(swap.token_in.clone()); + split_tokens_already_considered.insert(swap.token_in.clone()); + } + } else { + // it might be the last swap of the split or a regular swap + if !split_tokens_already_considered.contains(&swap.token_in) { + solution_tokens.push(swap.token_in.clone()); + } + } + if i == solution.swaps.len() - 1 { + solution_tokens.push(swap.token_out.clone()); + } + } + + if solution_tokens.len() != + solution_tokens + .iter() + .cloned() + .collect::>() + .len() + { + if let Some(last_swap) = solution.swaps.last() { + if solution.swaps[0].token_in != last_swap.token_out { + return Err(EncodingError::FatalError( + "Cyclical swaps are only allowed if they are the first and last token of a solution".to_string(), + )); + } else { + // it is a valid cyclical swap + // we don't support any wrapping or unwrapping in this case + if let Some(_native_action) = solution.clone().native_action { + return Err(EncodingError::FatalError( + "Wrapping/Unwrapping is not available in cyclical swaps".to_string(), + )); + } + } + } + } + Ok(()) + } +} + +/// Represents an encoder for one swap to be executed directly against an Executor. This is useful +/// when you want to bypass the Tycho Router, use your own Router contract and just need the +/// calldata for a particular swap. +/// +/// # Fields +/// * `swap_encoder_registry`: Registry of swap encoders +/// * `native_address`: Address of the chain's native token +pub struct TychoExecutorEncoder { + swap_encoder_registry: SwapEncoderRegistry, + native_address: Bytes, +} + +impl TychoExecutorEncoder { + pub fn new( + chain: tycho_common::models::Chain, + swap_encoder_registry: SwapEncoderRegistry, + ) -> Result { + let chain: Chain = Chain::from(chain); + let native_address = chain.native_token()?; + Ok(TychoExecutorEncoder { swap_encoder_registry, native_address }) + } + + fn encode_executor_calldata( + &self, + solution: Solution, + ) -> Result<(Vec, Bytes), EncodingError> { + let grouped_swaps = group_swaps(solution.clone().swaps); + let number_of_groups = grouped_swaps.len(); + if number_of_groups > 1 { + return Err(EncodingError::InvalidInput(format!( + "Tycho executor encoder only supports one swap. Found {}", + number_of_groups + ))) + } + + let grouped_swap = grouped_swaps + .first() + .ok_or_else(|| EncodingError::FatalError("Swap grouping failed".to_string()))?; + + let receiver = solution.receiver; + + let swap_encoder = self + .swap_encoder_registry + .get_encoder(&grouped_swap.protocol_system) + .ok_or_else(|| { + EncodingError::InvalidInput(format!( + "Swap encoder not found for protocol: {}", + grouped_swap.protocol_system + )) + })?; + + let mut grouped_protocol_data: Vec = vec![]; + for swap in grouped_swap.swaps.iter() { + let encoding_context = EncodingContext { + receiver: receiver.clone(), + exact_out: solution.exact_out, + router_address: None, + group_token_in: grouped_swap.input_token.clone(), + group_token_out: grouped_swap.output_token.clone(), + }; + let protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context.clone())?; + grouped_protocol_data.extend(protocol_data); + } + + let executor_address = Bytes::from_str(swap_encoder.executor_address()) + .map_err(|_| EncodingError::FatalError("Invalid executor address".to_string()))?; + + Ok((grouped_protocol_data, executor_address)) + } +} + +impl TychoEncoder for TychoExecutorEncoder { + fn encode_calldata(&self, solutions: Vec) -> Result, EncodingError> { + let mut transactions: Vec = Vec::new(); + let solution = solutions + .first() + .ok_or(EncodingError::FatalError("No solutions found".to_string()))?; + self.validate_solution(solution)?; + + let (contract_interaction, target_address) = + self.encode_executor_calldata(solution.clone())?; + + let value = if solution.given_token == self.native_address { + solution.given_amount.clone() + } else { + BigUint::ZERO + }; + + transactions.push(Transaction { value, data: contract_interaction, to: target_address }); + Ok(transactions) + } + + /// Raises an `EncodingError` if the solution is not considered valid. + /// + /// A solution is considered valid if all the following conditions are met: + /// * The solution is not exact out. + fn validate_solution(&self, solution: &Solution) -> Result<(), EncodingError> { + if solution.exact_out { + return Err(EncodingError::FatalError( + "Currently only exact input solutions are supported".to_string(), + )); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use tycho_common::models::{protocol::ProtocolComponent, Chain as TychoCoreChain}; + + use super::*; + use crate::encoding::{ + models::Swap, strategy_encoder::StrategyEncoder, swap_encoder::SwapEncoder, + }; + + fn dai() -> Bytes { + Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap() + } + + fn eth() -> Bytes { + Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap() + } + + fn weth() -> Bytes { + Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap() + } + + fn usdc() -> Bytes { + Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap() + } + + fn wbtc() -> Bytes { + Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap() + } + + mod router_encoder { + use super::*; + + #[derive(Clone)] + struct MockStrategy; + + impl StrategyEncoder for MockStrategy { + fn encode_strategy( + &self, + _solution: Solution, + ) -> Result<(Vec, Bytes), EncodingError> { + Ok(( + Bytes::from_str("0x1234") + .unwrap() + .to_vec(), + Bytes::from_str("0xabcd").unwrap(), + )) + } + + fn get_swap_encoder(&self, _protocol_system: &str) -> Option<&Box> { + None + } + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } + } + + fn get_mocked_tycho_router_encoder() -> TychoRouterEncoder { + let strategy_encoder = Box::new(MockStrategy {}); + TychoRouterEncoder::new(TychoCoreChain::Ethereum, strategy_encoder).unwrap() + } + #[test] + fn test_encode_router_calldata() { + let encoder = get_mocked_tycho_router_encoder(); + let eth_amount_in = BigUint::from(1000u32); + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth(), + token_out: dai(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + given_amount: eth_amount_in.clone(), + given_token: eth(), + swaps: vec![swap], + native_action: Some(NativeAction::Wrap), + ..Default::default() + }; + + let transactions = encoder.encode_calldata(vec![solution]); + + assert!(transactions.is_ok()); + let transactions = transactions.unwrap(); + assert_eq!(transactions.len(), 1); + assert_eq!(transactions[0].value, eth_amount_in); + assert_eq!(transactions[0].data, Bytes::from_str("0x1234").unwrap()); + assert_eq!(transactions[0].to, Bytes::from_str("0xabcd").unwrap()); + } + + #[test] + fn test_validate_fails_for_exact_out() { + let encoder = get_mocked_tycho_router_encoder(); + let solution = Solution { + exact_out: true, // This should cause an error + ..Default::default() + }; + let result = encoder.validate_solution(&solution); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + EncodingError::FatalError( + "Currently only exact input solutions are supported".to_string() + ) + ); + } + + #[test] + fn test_validate_passes_for_wrap() { + let encoder = get_mocked_tycho_router_encoder(); + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth(), + token_out: dai(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + given_token: eth(), + checked_token: dai(), + checked_amount: None, + swaps: vec![swap], + native_action: Some(NativeAction::Wrap), + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_ok()); + } + + #[test] + fn test_validate_fails_for_wrap_wrong_input() { + let encoder = get_mocked_tycho_router_encoder(); + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth(), + token_out: dai(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + given_token: weth(), + swaps: vec![swap], + native_action: Some(NativeAction::Wrap), + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + EncodingError::FatalError( + "Native token must be the input token in order to wrap".to_string() + ) + ); + } + + #[test] + fn test_validate_fails_for_wrap_wrong_first_swap() { + let encoder = get_mocked_tycho_router_encoder(); + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: eth(), + token_out: dai(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + given_token: eth(), + swaps: vec![swap], + native_action: Some(NativeAction::Wrap), + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + EncodingError::FatalError( + "Wrapped token must be the first swap's input in order to wrap".to_string() + ) + ); + } + + #[test] + fn test_validate_fails_no_swaps() { + let encoder = get_mocked_tycho_router_encoder(); + let solution = Solution { + exact_out: false, + given_token: eth(), + swaps: vec![], + native_action: Some(NativeAction::Wrap), + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + EncodingError::FatalError("No swaps found in solution".to_string()) + ); + } + + #[test] + fn test_validate_passes_for_unwrap() { + let encoder = get_mocked_tycho_router_encoder(); + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai(), + token_out: weth(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + checked_token: eth(), + checked_amount: None, + swaps: vec![swap], + native_action: Some(NativeAction::Unwrap), + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_ok()); + } + + #[test] + fn test_validate_fails_for_unwrap_wrong_output() { + let encoder = get_mocked_tycho_router_encoder(); + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai(), + token_out: weth(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + given_token: dai(), + checked_token: weth(), + swaps: vec![swap], + native_action: Some(NativeAction::Unwrap), + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + EncodingError::FatalError( + "Native token must be the output token in order to unwrap".to_string() + ) + ); + } + + #[test] + fn test_validate_fails_for_unwrap_wrong_last_swap() { + let encoder = get_mocked_tycho_router_encoder(); + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai(), + token_out: eth(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + checked_token: eth(), + swaps: vec![swap], + native_action: Some(NativeAction::Unwrap), + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + EncodingError::FatalError( + "Wrapped token must be the last swap's output in order to unwrap".to_string() + ) + ); + } + + #[test] + fn test_validate_cyclical_swap() { + // This validation passes because the cyclical swap is the first and last token + // 50% -> WETH + // DAI - -> DAI + // 50% -> WETH + // (some of the pool addresses in this test are fake) + let encoder = get_mocked_tycho_router_encoder(); + let swaps = vec![ + Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai(), + token_out: weth(), + split: 0.5f64, + }, + Swap { + component: ProtocolComponent { + id: "0x0000000000000000000000000000000000000000".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai(), + token_out: weth(), + split: 0f64, + }, + Swap { + component: ProtocolComponent { + id: "0x0000000000000000000000000000000000000000".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth(), + token_out: dai(), + split: 0f64, + }, + ]; + + let solution = Solution { + exact_out: false, + given_token: dai(), + checked_token: dai(), + swaps, + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_ok()); + } + + #[test] + fn test_validate_cyclical_swap_fail() { + // This test should fail because the cyclical swap is not the first and last token + // DAI -> WETH -> USDC -> DAI -> WBTC + // (some of the pool addresses in this test are fake) + let encoder = get_mocked_tycho_router_encoder(); + let swaps = vec![ + Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai(), + token_out: weth(), + split: 0f64, + }, + Swap { + component: ProtocolComponent { + id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth(), + token_out: usdc(), + split: 0f64, + }, + Swap { + component: ProtocolComponent { + id: "0x0000000000000000000000000000000000000000".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: usdc(), + token_out: dai(), + split: 0f64, + }, + Swap { + component: ProtocolComponent { + id: "0x0000000000000000000000000000000000000000".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai(), + token_out: wbtc(), + split: 0f64, + }, + ]; + + let solution = Solution { + exact_out: false, + given_token: dai(), + checked_token: wbtc(), + swaps, + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + EncodingError::FatalError( + "Cyclical swaps are only allowed if they are the first and last token of a solution".to_string() + ) + ); + } + #[test] + fn test_validate_cyclical_swap_split_output() { + // This validation passes because it is a valid cyclical swap + // -> WETH + // WETH -> DAI + // -> WETH + // (some of the pool addresses in this test are fake) + let encoder = get_mocked_tycho_router_encoder(); + let swaps = vec![ + Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth(), + token_out: dai(), + split: 0f64, + }, + Swap { + component: ProtocolComponent { + id: "0x0000000000000000000000000000000000000000".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai(), + token_out: weth(), + split: 0.5f64, + }, + Swap { + component: ProtocolComponent { + id: "0x0000000000000000000000000000000000000000".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai(), + token_out: weth(), + split: 0f64, + }, + ]; + + let solution = Solution { + exact_out: false, + given_token: weth(), + checked_token: weth(), + swaps, + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_ok()); + } + + #[test] + fn test_validate_cyclical_swap_native_action_fail() { + // This validation fails because there is a native action with a valid cyclical swap + // ETH -> WETH -> DAI -> WETH + // (some of the pool addresses in this test are fake) + let encoder = get_mocked_tycho_router_encoder(); + let swaps = vec![ + Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth(), + token_out: dai(), + split: 0f64, + }, + Swap { + component: ProtocolComponent { + id: "0x0000000000000000000000000000000000000000".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai(), + token_out: weth(), + split: 0f64, + }, + ]; + + let solution = Solution { + exact_out: false, + given_token: eth(), + checked_token: weth(), + swaps, + native_action: Some(NativeAction::Wrap), + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + EncodingError::FatalError( + "Wrapping/Unwrapping is not available in cyclical swaps" + .to_string() + .to_string() + ) + ); + } + } + + mod executor_encoder { + use std::{collections::HashMap, str::FromStr}; + + use alloy::hex::encode; + use num_bigint::{BigInt, BigUint}; + use tycho_common::{models::protocol::ProtocolComponent, Bytes}; + + use super::*; + use crate::encoding::{ + evm::swap_encoder::swap_encoder_registry::SwapEncoderRegistry, + models::{Solution, Swap}, + }; + + fn get_swap_encoder_registry() -> SwapEncoderRegistry { + SwapEncoderRegistry::new( + Some("config/test_executor_addresses.json".to_string()), + TychoCoreChain::Ethereum, + ) + .unwrap() + } + + #[test] + fn test_executor_encoder_encode() { + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = + TychoExecutorEncoder::new(TychoCoreChain::Ethereum, swap_encoder_registry).unwrap(); + + let token_in = weth(); + let token_out = dai(); + + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + given_token: token_in, + given_amount: BigUint::from(1000000000000000000u64), + expected_amount: Some(BigUint::from(1000000000000000000u64)), + checked_token: token_out, + checked_amount: None, + sender: Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(), + // The receiver was generated with `makeAddr("bob") using forge` + receiver: Bytes::from_str("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e").unwrap(), + swaps: vec![swap], + slippage: None, + native_action: None, + }; + + let transactions = encoder + .encode_calldata(vec![solution]) + .unwrap(); + let transaction = transactions + .first() + .expect("Expected at least one transaction"); + let hex_protocol_data = encode(&transaction.data); + assert_eq!( + transaction.to, + Bytes::from_str("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").unwrap() + ); + assert_eq!( + hex_protocol_data, + String::from(concat!( + // in token + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + // component id + "a478c2975ab1ea89e8196811f51a7b7ade33eb11", + // receiver + "1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e", + // zero for one + "00", + )) + ); + } + + #[test] + fn test_executor_encoder_too_many_swaps() { + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = + TychoExecutorEncoder::new(TychoCoreChain::Ethereum, swap_encoder_registry).unwrap(); + + let token_in = weth(); + let token_out = dai(); + + let swap = Swap { + component: ProtocolComponent { + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + given_token: token_in, + given_amount: BigUint::from(1000000000000000000u64), + expected_amount: Some(BigUint::from(1000000000000000000u64)), + checked_token: token_out, + checked_amount: None, + sender: Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(), + receiver: Bytes::from_str("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e").unwrap(), + swaps: vec![swap.clone(), swap], + slippage: None, + native_action: None, + }; + + let result = encoder.encode_calldata(vec![solution]); + assert!(result.is_err()); + } + + #[test] + fn test_executor_encoder_grouped_swaps() { + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = + TychoExecutorEncoder::new(TychoCoreChain::Ethereum, swap_encoder_registry).unwrap(); + + let eth = eth(); + let usdc = usdc(); + let pepe = Bytes::from_str("0x6982508145454Ce325dDbE47a25d4ec3d2311933").unwrap(); + + // Fee and tick spacing information for this test is obtained by querying the + // USV4 Position Manager contract: 0xbd216513d74c8cf14cf4747e6aaa6420ff64ee9e + // Using the poolKeys function with the first 25 bytes of the pool id + let pool_fee_usdc_eth = Bytes::from(BigInt::from(3000).to_signed_bytes_be()); + let tick_spacing_usdc_eth = Bytes::from(BigInt::from(60).to_signed_bytes_be()); + let mut static_attributes_usdc_eth: HashMap = HashMap::new(); + static_attributes_usdc_eth.insert("key_lp_fee".into(), pool_fee_usdc_eth); + static_attributes_usdc_eth.insert("tick_spacing".into(), tick_spacing_usdc_eth); + + let pool_fee_eth_pepe = Bytes::from(BigInt::from(25000).to_signed_bytes_be()); + let tick_spacing_eth_pepe = Bytes::from(BigInt::from(500).to_signed_bytes_be()); + let mut static_attributes_eth_pepe: HashMap = HashMap::new(); + static_attributes_eth_pepe.insert("key_lp_fee".into(), pool_fee_eth_pepe); + static_attributes_eth_pepe.insert("tick_spacing".into(), tick_spacing_eth_pepe); + + let swap_usdc_eth = Swap { + component: ProtocolComponent { + id: "0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d" + .to_string(), + protocol_system: "uniswap_v4".to_string(), + static_attributes: static_attributes_usdc_eth, + ..Default::default() + }, + token_in: usdc.clone(), + token_out: eth.clone(), + split: 0f64, + }; + + let swap_eth_pepe = Swap { + component: ProtocolComponent { + id: "0xecd73ecbf77219f21f129c8836d5d686bbc27d264742ddad620500e3e548e2c9" + .to_string(), + protocol_system: "uniswap_v4".to_string(), + static_attributes: static_attributes_eth_pepe, + ..Default::default() + }, + token_in: eth.clone(), + token_out: pepe.clone(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + given_token: usdc, + given_amount: BigUint::from_str("1000_000000").unwrap(), + checked_token: pepe, + expected_amount: Some(BigUint::from_str("105_152_000000000000000000").unwrap()), + checked_amount: None, + slippage: None, + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + swaps: vec![swap_usdc_eth, swap_eth_pepe], + ..Default::default() + }; + + let transactions = encoder + .encode_calldata(vec![solution]) + .unwrap(); + let transaction = transactions + .first() + .expect("Expected at least one transaction"); + let hex_protocol_data = encode(&transaction.data); + assert_eq!( + transaction.to, + Bytes::from_str("0xf62849f9a0b5bf2913b396098f7c7019b51a820a").unwrap() + ); + assert_eq!( + hex_protocol_data, + String::from(concat!( + // group in token + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // group out token + "6982508145454ce325ddbe47a25d4ec3d2311933", + // zero for one + "00", + // executor address + "f62849f9a0b5bf2913b396098f7c7019b51a820a", + // first pool intermediary token (ETH) + "0000000000000000000000000000000000000000", + // fee + "000bb8", + // tick spacing + "00003c", + // second pool intermediary token (PEPE) + "6982508145454ce325ddbe47a25d4ec3d2311933", + // fee + "0061a8", + // tick spacing + "0001f4" + )) + ); + } + } +} diff --git a/src/encoding/tycho_encoder.rs b/src/encoding/tycho_encoder.rs index 09a9d46..16e0bb0 100644 --- a/src/encoding/tycho_encoder.rs +++ b/src/encoding/tycho_encoder.rs @@ -14,8 +14,7 @@ pub trait TychoEncoder { /// /// # Returns /// * `Result, EncodingError>` - Vector of executable transactions - fn encode_router_calldata( - &self, - solutions: Vec, - ) -> Result, EncodingError>; + fn encode_calldata(&self, solutions: Vec) -> Result, EncodingError>; + + fn validate_solution(&self, solution: &Solution) -> Result<(), EncodingError>; }