From 4f7fe3b96d767c5df3757607777f3caec0d61d5b Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 5 Feb 2025 16:53:20 +0530 Subject: [PATCH 01/19] feat: add encoder bin --- Cargo.toml | 4 + src/bin/tycho-encode.rs | 346 ++++++++++++++++++ .../strategy_encoder_registry.rs | 6 +- .../evm/swap_encoder/swap_encoder_registry.rs | 19 + 4 files changed, 374 insertions(+), 1 deletion(-) create mode 100644 src/bin/tycho-encode.rs diff --git a/Cargo.toml b/Cargo.toml index 1c7e8f7..90a9356 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,10 @@ name = "tycho-execution" version = "0.30.1" edition = "2021" +[[bin]] +name = "tycho-encode" +path = "src/bin/tycho-encode.rs" + [dependencies] dotenv = "0.15.0" lazy_static = "1.4.0" diff --git a/src/bin/tycho-encode.rs b/src/bin/tycho-encode.rs new file mode 100644 index 0000000..8a619a6 --- /dev/null +++ b/src/bin/tycho-encode.rs @@ -0,0 +1,346 @@ +use std::{ + io::{self, Read}, + str::FromStr, +}; + +use chrono::Utc; +use hex; +use num_bigint::BigUint; +use serde_json::Value; +use tycho_core::{ + dto::{Chain as DtoChain, ProtocolComponent}, + Bytes, +}; +use tycho_execution::encoding::{ + evm::{ + strategy_encoder::strategy_encoder_registry::EVMStrategyEncoderRegistry, + tycho_encoder::EVMTychoEncoder, + }, + models::{Solution, Swap}, + strategy_encoder::StrategyEncoderRegistry, + tycho_encoder::TychoEncoder, +}; + +const DEFAULT_ROUTER_ADDRESS: &str = "0x1234567890123456789012345678901234567890"; +const HELP_TEXT: &str = "\ +USAGE: + tycho-encode [ROUTER_ADDRESS] + +ARGS: + ROUTER_ADDRESS The address of the router contract [default: 0x1234567890123456789012345678901234567890] + +The program reads a JSON object from stdin containing the swap details and outputs the encoded transaction. +The JSON object should have the following structure: +{ + \"sender\": \"0x...\", + \"receiver\": \"0x...\", + \"given_token\": \"0x...\", + \"given_amount\": \"123...\", + \"checked_token\": \"0x...\", + \"exact_out\": false, + \"slippage\": 0.01, + \"expected_amount\": \"123...\", + \"check_amount\": \"123...\", + \"swaps\": [{ + \"component\": { + \"id\": \"...\", + \"protocol_system\": \"...\", + \"protocol_type_name\": \"...\", + \"chain\": \"ethereum\", + \"tokens\": [\"0x...\"], + \"contract_ids\": [\"0x...\"], + \"static_attributes\": {\"key\": \"0x...\"} + }, + \"token_in\": \"0x...\", + \"token_out\": \"0x...\", + \"split\": 1.0 + }], + \"router_address\": \"0x...\", + \"direct_execution\": false +}"; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let args: Vec = std::env::args().collect(); + + // Show help text if requested + if args.len() > 1 && (args[1] == "-h" || args[1] == "--help") { + println!("{}", HELP_TEXT); + return Ok(()); + } + + let router_address = args + .get(1) + .map(|s| s.as_str()) + .unwrap_or(DEFAULT_ROUTER_ADDRESS); + + // Read from stdin until EOF + let mut buffer = String::new(); + io::stdin() + .read_to_string(&mut buffer) + .map_err(|e| format!("Failed to read from stdin: {}", e))?; + + if buffer.trim().is_empty() { + eprintln!("Error: No input provided"); + eprintln!("{}", HELP_TEXT); + std::process::exit(1); + } + + // Parse the JSON input to verify it's valid + serde_json::from_str::(&buffer) + .map_err(|e| format!("Failed to parse JSON input: {}", e))?; + + // Encode the solution + let encoded = encode_swaps(&buffer, router_address)?; + + // Output the encoded result as JSON to stdout + println!( + "{}", + serde_json::to_string(&encoded) + .map_err(|e| format!("Failed to serialize output: {}", e))? + ); + + Ok(()) +} + +fn parse_solution(input: Value) -> Result> { + let obj = input + .as_object() + .ok_or("Input must be a JSON object")?; + + Ok(Solution { + sender: parse_bytes( + obj.get("sender") + .ok_or("sender is required")?, + )?, + receiver: parse_bytes( + obj.get("receiver") + .ok_or("receiver is required")?, + )?, + given_token: parse_bytes( + obj.get("given_token") + .ok_or("given_token is required")?, + )?, + given_amount: parse_biguint( + obj.get("given_amount") + .ok_or("given_amount is required")?, + )?, + checked_token: parse_bytes( + obj.get("checked_token") + .ok_or("checked_token is required")?, + )?, + exact_out: obj + .get("exact_out") + .and_then(|v| v.as_bool()) + .unwrap_or(false), + slippage: obj + .get("slippage") + .and_then(|v| v.as_f64()), + expected_amount: obj + .get("expected_amount") + .map(parse_biguint) + .transpose()?, + check_amount: obj + .get("check_amount") + .map(parse_biguint) + .transpose()?, + swaps: parse_swaps( + obj.get("swaps") + .ok_or("swaps is required")?, + )?, + router_address: obj + .get("router_address") + .map(parse_bytes) + .transpose()?, + native_action: None, // TODO: Implement if needed + direct_execution: obj + .get("direct_execution") + .and_then(|v| v.as_bool()) + .unwrap_or(false), + }) +} + +fn parse_bytes(value: &Value) -> Result> { + let s = value + .as_str() + .ok_or("Expected string for bytes")?; + Ok(Bytes::from_str(s)?) +} + +fn parse_biguint(value: &Value) -> Result> { + let s = value + .as_str() + .ok_or("Expected string for BigUint")?; + Ok(BigUint::from_str(s)?) +} + +fn parse_swaps(value: &Value) -> Result, Box> { + let arr = value + .as_array() + .ok_or("Expected array for swaps")?; + let mut swaps = Vec::new(); + + for swap in arr { + let obj = swap + .as_object() + .ok_or("Swap must be an object")?; + swaps.push(Swap { + component: parse_protocol_component( + obj.get("component") + .ok_or("component is required")?, + )?, + token_in: parse_bytes( + obj.get("token_in") + .ok_or("token_in is required")?, + )?, + token_out: parse_bytes( + obj.get("token_out") + .ok_or("token_out is required")?, + )?, + split: obj + .get("split") + .and_then(|v| v.as_f64()) + .unwrap_or(0.0), + }); + } + + Ok(swaps) +} + +fn parse_protocol_component(obj: &Value) -> Result> { + let obj = obj + .as_object() + .ok_or("Expected object for ProtocolComponent")?; + + Ok(ProtocolComponent { + id: obj + .get("id") + .and_then(|v| v.as_str()) + .ok_or("id is required")? + .to_string(), + protocol_system: obj + .get("protocol_system") + .and_then(|v| v.as_str()) + .ok_or("protocol_system is required")? + .to_string(), + protocol_type_name: obj + .get("protocol_type_name") + .and_then(|v| v.as_str()) + .ok_or("protocol_type_name is required")? + .to_string(), + chain: obj + .get("chain") + .and_then(|v| v.as_str()) + .map(|s| match s.to_lowercase().as_str() { + "ethereum" => DtoChain::Ethereum, + "starknet" => DtoChain::Starknet, + "zksync" => DtoChain::ZkSync, + "arbitrum" => DtoChain::Arbitrum, + _ => DtoChain::Ethereum, // Default to Ethereum + }) + .unwrap_or(DtoChain::Ethereum), + tokens: obj + .get("tokens") + .and_then(|v| v.as_array()) + .ok_or("tokens is required")? + .iter() + .map(|v| { + v.as_str() + .map(|s| Bytes::from_str(s).unwrap()) + }) + .collect::>>() + .ok_or("Invalid token address format")?, + contract_ids: obj + .get("contract_ids") + .and_then(|v| v.as_array()) + .ok_or("contract_ids is required")? + .iter() + .map(|v| { + v.as_str() + .map(|s| Bytes::from_str(s).unwrap()) + }) + .collect::>>() + .ok_or("Invalid contract address format")?, + static_attributes: obj + .get("static_attributes") + .and_then(|v| v.as_object()) + .ok_or("static_attributes is required")? + .iter() + .map(|(k, v)| { + Ok(( + k.clone(), + Bytes::from_str( + v.as_str() + .ok_or("Invalid attribute value")?, + )?, + )) + }) + .collect::>>()?, + change: obj + .get("change") + .and_then(|v| v.as_str()) + .map(|s| match s.to_lowercase().as_str() { + "update" => tycho_core::dto::ChangeType::Update, + "deletion" => tycho_core::dto::ChangeType::Deletion, + "creation" => tycho_core::dto::ChangeType::Creation, + _ => tycho_core::dto::ChangeType::Unspecified, + }) + .unwrap_or(tycho_core::dto::ChangeType::Update), + creation_tx: Bytes::from_str( + obj.get("creation_tx") + .and_then(|v| v.as_str()) + .unwrap_or("0x"), + )?, + created_at: obj + .get("created_at") + .and_then(|v| v.as_str()) + .map(|s| { + chrono::DateTime::parse_from_rfc3339(s) + .unwrap() + .naive_utc() + }) + .unwrap_or_else(|| Utc::now().naive_utc()), + }) +} + +fn encode_swaps(input: &str, router_address: &str) -> Result> { + // Parse the input JSON + let input_json: Value = serde_json::from_str(input)?; + + // Extract the chain from the input JSON + let chain = input_json + .get("chain") + .and_then(|v| v.as_str()) + .map(|s| match s.to_lowercase().as_str() { + "ethereum" => DtoChain::Ethereum, + "starknet" => DtoChain::Starknet, + "zksync" => DtoChain::ZkSync, + "arbitrum" => DtoChain::Arbitrum, + _ => DtoChain::Ethereum, // Default to Ethereum + }) + .unwrap_or(DtoChain::Ethereum); + + // Parse the solution from the input JSON + let mut solution = parse_solution(input_json)?; + solution.direct_execution = true; + + // Create the strategy encoder based on the chain + let strategy_encoder: Value = match chain { + DtoChain::Ethereum => { + // Create encoder and encode the solution with empty executors file path + let strategy_selector = EVMStrategyEncoderRegistry::new(DtoChain::Ethereum, "", None)?; + let encoder = EVMTychoEncoder::new(strategy_selector, router_address.to_string())?; + let transactions = encoder.encode_router_calldata(vec![solution])?; + + let result: Result> = Ok(serde_json::json!({ + "to": format!("0x{}", hex::encode(&transactions[0].to)), + "value": format!("0x{}", hex::encode(transactions[0].value.to_bytes_be())), + "data": format!("0x{}", hex::encode(&transactions[0].data)), + })); + result + } + _ => Err(format!("Unsupported chain: {:?}", chain).into()), + }?; + + Ok(strategy_encoder) +} diff --git a/src/encoding/evm/strategy_encoder/strategy_encoder_registry.rs b/src/encoding/evm/strategy_encoder/strategy_encoder_registry.rs index 94a8253..6e9712d 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoder_registry.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoder_registry.rs @@ -22,7 +22,11 @@ impl StrategyEncoderRegistry for EVMStrategyEncoderRegistry { executors_file_path: &str, signer_pk: Option, ) -> Result { - let swap_encoder_registry = SwapEncoderRegistry::new(executors_file_path, Chain::Ethereum)?; + let swap_encoder_registry = if executors_file_path.is_empty() { + SwapEncoderRegistry::new_direct_execution() + } else { + SwapEncoderRegistry::new(executors_file_path, Chain::Ethereum)? + }; let mut strategies: HashMap> = HashMap::new(); strategies.insert( diff --git a/src/encoding/evm/swap_encoder/swap_encoder_registry.rs b/src/encoding/evm/swap_encoder/swap_encoder_registry.rs index b04e580..7ac2207 100644 --- a/src/encoding/evm/swap_encoder/swap_encoder_registry.rs +++ b/src/encoding/evm/swap_encoder/swap_encoder_registry.rs @@ -29,6 +29,25 @@ impl SwapEncoderRegistry { Ok(Self { encoders }) } + pub fn new_direct_execution() -> Self { + let mut encoders = HashMap::new(); + + // Add default encoders with their respective executor addresses + let default_encoders = [ + ("uniswap_v2", "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"), + ("vm:balancer_v2", "0xBA12222222228d8Ba445958a75a0704d566BF2C8"), + ]; + + for (protocol, executor_address) in default_encoders { + let builder = SwapEncoderBuilder::new(protocol, executor_address); + if let Ok(encoder) = builder.build() { + encoders.insert(protocol.to_string(), encoder); + } + } + + Self { encoders } + } + #[allow(clippy::borrowed_box)] pub fn get_encoder(&self, protocol_system: &str) -> Option<&Box> { self.encoders.get(protocol_system) From ae6b1ed658721a067d55e5b64e3a93b15f2b66af Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 5 Feb 2025 17:05:06 +0530 Subject: [PATCH 02/19] feat: remove direct execution hardcode --- src/bin/tycho-encode.rs | 61 ++++++------------- .../strategy_encoder_registry.rs | 6 +- .../evm/swap_encoder/swap_encoder_registry.rs | 19 ------ 3 files changed, 20 insertions(+), 66 deletions(-) diff --git a/src/bin/tycho-encode.rs b/src/bin/tycho-encode.rs index 8a619a6..c613cc5 100644 --- a/src/bin/tycho-encode.rs +++ b/src/bin/tycho-encode.rs @@ -8,7 +8,7 @@ use hex; use num_bigint::BigUint; use serde_json::Value; use tycho_core::{ - dto::{Chain as DtoChain, ProtocolComponent}, + dto::{Chain, ProtocolComponent}, Bytes, }; use tycho_execution::encoding::{ @@ -22,6 +22,7 @@ use tycho_execution::encoding::{ }; const DEFAULT_ROUTER_ADDRESS: &str = "0x1234567890123456789012345678901234567890"; +const DEFAULT_EXECUTORS_FILE_PATH: &str = "src/encoding/config/executor_addresses.json"; const HELP_TEXT: &str = "\ USAGE: tycho-encode [ROUTER_ADDRESS] @@ -232,13 +233,13 @@ fn parse_protocol_component(obj: &Value) -> Result DtoChain::Ethereum, - "starknet" => DtoChain::Starknet, - "zksync" => DtoChain::ZkSync, - "arbitrum" => DtoChain::Arbitrum, - _ => DtoChain::Ethereum, // Default to Ethereum + "ethereum" => Chain::Ethereum, + "starknet" => Chain::Starknet, + "zksync" => Chain::ZkSync, + "arbitrum" => Chain::Arbitrum, + _ => Chain::Ethereum, // Default to Ethereum }) - .unwrap_or(DtoChain::Ethereum), + .unwrap_or(Chain::Ethereum), tokens: obj .get("tokens") .and_then(|v| v.as_array()) @@ -306,41 +307,17 @@ fn parse_protocol_component(obj: &Value) -> Result Result> { // Parse the input JSON let input_json: Value = serde_json::from_str(input)?; + let solution = parse_solution(input_json)?; - // Extract the chain from the input JSON - let chain = input_json - .get("chain") - .and_then(|v| v.as_str()) - .map(|s| match s.to_lowercase().as_str() { - "ethereum" => DtoChain::Ethereum, - "starknet" => DtoChain::Starknet, - "zksync" => DtoChain::ZkSync, - "arbitrum" => DtoChain::Arbitrum, - _ => DtoChain::Ethereum, // Default to Ethereum - }) - .unwrap_or(DtoChain::Ethereum); + // Create encoder and encode the solution + let strategy_selector = + EVMStrategyEncoderRegistry::new(Chain::Ethereum, DEFAULT_EXECUTORS_FILE_PATH, None)?; + let encoder = EVMTychoEncoder::new(strategy_selector, router_address.to_string())?; + let transactions = encoder.encode_router_calldata(vec![solution])?; - // Parse the solution from the input JSON - let mut solution = parse_solution(input_json)?; - solution.direct_execution = true; - - // Create the strategy encoder based on the chain - let strategy_encoder: Value = match chain { - DtoChain::Ethereum => { - // Create encoder and encode the solution with empty executors file path - let strategy_selector = EVMStrategyEncoderRegistry::new(DtoChain::Ethereum, "", None)?; - let encoder = EVMTychoEncoder::new(strategy_selector, router_address.to_string())?; - let transactions = encoder.encode_router_calldata(vec![solution])?; - - let result: Result> = Ok(serde_json::json!({ - "to": format!("0x{}", hex::encode(&transactions[0].to)), - "value": format!("0x{}", hex::encode(transactions[0].value.to_bytes_be())), - "data": format!("0x{}", hex::encode(&transactions[0].data)), - })); - result - } - _ => Err(format!("Unsupported chain: {:?}", chain).into()), - }?; - - Ok(strategy_encoder) + Ok(serde_json::json!({ + "to": format!("0x{}", hex::encode(&transactions[0].to)), + "value": format!("0x{}", hex::encode(transactions[0].value.to_bytes_be())), + "data": format!("0x{}", hex::encode(&transactions[0].data)), + })) } diff --git a/src/encoding/evm/strategy_encoder/strategy_encoder_registry.rs b/src/encoding/evm/strategy_encoder/strategy_encoder_registry.rs index 6e9712d..94a8253 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoder_registry.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoder_registry.rs @@ -22,11 +22,7 @@ impl StrategyEncoderRegistry for EVMStrategyEncoderRegistry { executors_file_path: &str, signer_pk: Option, ) -> Result { - let swap_encoder_registry = if executors_file_path.is_empty() { - SwapEncoderRegistry::new_direct_execution() - } else { - SwapEncoderRegistry::new(executors_file_path, Chain::Ethereum)? - }; + let swap_encoder_registry = SwapEncoderRegistry::new(executors_file_path, Chain::Ethereum)?; let mut strategies: HashMap> = HashMap::new(); strategies.insert( diff --git a/src/encoding/evm/swap_encoder/swap_encoder_registry.rs b/src/encoding/evm/swap_encoder/swap_encoder_registry.rs index 7ac2207..b04e580 100644 --- a/src/encoding/evm/swap_encoder/swap_encoder_registry.rs +++ b/src/encoding/evm/swap_encoder/swap_encoder_registry.rs @@ -29,25 +29,6 @@ impl SwapEncoderRegistry { Ok(Self { encoders }) } - pub fn new_direct_execution() -> Self { - let mut encoders = HashMap::new(); - - // Add default encoders with their respective executor addresses - let default_encoders = [ - ("uniswap_v2", "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"), - ("vm:balancer_v2", "0xBA12222222228d8Ba445958a75a0704d566BF2C8"), - ]; - - for (protocol, executor_address) in default_encoders { - let builder = SwapEncoderBuilder::new(protocol, executor_address); - if let Ok(encoder) = builder.build() { - encoders.insert(protocol.to_string(), encoder); - } - } - - Self { encoders } - } - #[allow(clippy::borrowed_box)] pub fn get_encoder(&self, protocol_system: &str) -> Option<&Box> { self.encoders.get(protocol_system) From 8d97f73ec7f034fa84908eec41087f3212520b33 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 5 Feb 2025 17:26:02 +0530 Subject: [PATCH 03/19] feat: simplify --- src/bin/lib/help.rs | 36 ++++++ src/bin/lib/parse.rs | 210 ++++++++++++++++++++++++++++++++ src/bin/tycho-encode.rs | 262 ++-------------------------------------- 3 files changed, 256 insertions(+), 252 deletions(-) create mode 100644 src/bin/lib/help.rs create mode 100644 src/bin/lib/parse.rs diff --git a/src/bin/lib/help.rs b/src/bin/lib/help.rs new file mode 100644 index 0000000..250e5aa --- /dev/null +++ b/src/bin/lib/help.rs @@ -0,0 +1,36 @@ +pub const HELP_TEXT: &str = "\ +USAGE: + tycho-encode [ROUTER_ADDRESS] + +ARGS: + ROUTER_ADDRESS The address of the router contract [default: 0x1234567890123456789012345678901234567890] + +The program reads a JSON object from stdin containing the swap details and outputs the encoded transaction. +The JSON object should have the following structure: +{ + \"sender\": \"0x...\", + \"receiver\": \"0x...\", + \"given_token\": \"0x...\", + \"given_amount\": \"123...\", + \"checked_token\": \"0x...\", + \"exact_out\": false, + \"slippage\": 0.01, + \"expected_amount\": \"123...\", + \"check_amount\": \"123...\", + \"swaps\": [{ + \"component\": { + \"id\": \"...\", + \"protocol_system\": \"...\", + \"protocol_type_name\": \"...\", + \"chain\": \"ethereum\", + \"tokens\": [\"0x...\"], + \"contract_ids\": [\"0x...\"], + \"static_attributes\": {\"key\": \"0x...\"} + }, + \"token_in\": \"0x...\", + \"token_out\": \"0x...\", + \"split\": 1.0 + }], + \"router_address\": \"0x...\", + \"direct_execution\": false +}"; diff --git a/src/bin/lib/parse.rs b/src/bin/lib/parse.rs new file mode 100644 index 0000000..99f24cb --- /dev/null +++ b/src/bin/lib/parse.rs @@ -0,0 +1,210 @@ +use std::str::FromStr; + +use chrono::Utc; +use num_bigint::BigUint; +use serde_json::Value; +use tycho_core::{ + dto::{Chain, ProtocolComponent}, + Bytes, +}; +use tycho_execution::encoding::models::{Solution, Swap}; + +pub fn parse_solution(input: Value) -> Result> { + let obj = input + .as_object() + .ok_or("Input must be a JSON object")?; + + Ok(Solution { + sender: parse_bytes( + obj.get("sender") + .ok_or("sender is required")?, + )?, + receiver: parse_bytes( + obj.get("receiver") + .ok_or("receiver is required")?, + )?, + given_token: parse_bytes( + obj.get("given_token") + .ok_or("given_token is required")?, + )?, + given_amount: parse_biguint( + obj.get("given_amount") + .ok_or("given_amount is required")?, + )?, + checked_token: parse_bytes( + obj.get("checked_token") + .ok_or("checked_token is required")?, + )?, + exact_out: obj + .get("exact_out") + .and_then(|v| v.as_bool()) + .unwrap_or(false), + slippage: obj + .get("slippage") + .and_then(|v| v.as_f64()), + expected_amount: obj + .get("expected_amount") + .map(parse_biguint) + .transpose()?, + check_amount: obj + .get("check_amount") + .map(parse_biguint) + .transpose()?, + swaps: parse_swaps( + obj.get("swaps") + .ok_or("swaps is required")?, + )?, + router_address: obj + .get("router_address") + .map(parse_bytes) + .transpose()?, + native_action: None, // TODO: Implement if needed + direct_execution: obj + .get("direct_execution") + .and_then(|v| v.as_bool()) + .unwrap_or(false), + }) +} + +fn parse_bytes(value: &Value) -> Result> { + let s = value + .as_str() + .ok_or("Expected string for bytes")?; + Ok(Bytes::from_str(s)?) +} + +fn parse_biguint(value: &Value) -> Result> { + let s = value + .as_str() + .ok_or("Expected string for BigUint")?; + Ok(BigUint::from_str(s)?) +} + +fn parse_swaps(value: &Value) -> Result, Box> { + let arr = value + .as_array() + .ok_or("Expected array for swaps")?; + let mut swaps = Vec::new(); + + for swap in arr { + let obj = swap + .as_object() + .ok_or("Swap must be an object")?; + swaps.push(Swap { + component: parse_protocol_component( + obj.get("component") + .ok_or("component is required")?, + )?, + token_in: parse_bytes( + obj.get("token_in") + .ok_or("token_in is required")?, + )?, + token_out: parse_bytes( + obj.get("token_out") + .ok_or("token_out is required")?, + )?, + split: obj + .get("split") + .and_then(|v| v.as_f64()) + .unwrap_or(0.0), + }); + } + + Ok(swaps) +} + +fn parse_protocol_component(obj: &Value) -> Result> { + let obj = obj + .as_object() + .ok_or("Expected object for ProtocolComponent")?; + + Ok(ProtocolComponent { + id: obj + .get("id") + .and_then(|v| v.as_str()) + .ok_or("id is required")? + .to_string(), + protocol_system: obj + .get("protocol_system") + .and_then(|v| v.as_str()) + .ok_or("protocol_system is required")? + .to_string(), + protocol_type_name: obj + .get("protocol_type_name") + .and_then(|v| v.as_str()) + .ok_or("protocol_type_name is required")? + .to_string(), + chain: obj + .get("chain") + .and_then(|v| v.as_str()) + .map(|s| match s.to_lowercase().as_str() { + "ethereum" => Chain::Ethereum, + "starknet" => Chain::Starknet, + "zksync" => Chain::ZkSync, + "arbitrum" => Chain::Arbitrum, + _ => Chain::Ethereum, // Default to Ethereum + }) + .unwrap_or(Chain::Ethereum), + tokens: obj + .get("tokens") + .and_then(|v| v.as_array()) + .ok_or("tokens is required")? + .iter() + .map(|v| { + v.as_str() + .map(|s| Bytes::from_str(s).unwrap()) + }) + .collect::>>() + .ok_or("Invalid token address format")?, + contract_ids: obj + .get("contract_ids") + .and_then(|v| v.as_array()) + .ok_or("contract_ids is required")? + .iter() + .map(|v| { + v.as_str() + .map(|s| Bytes::from_str(s).unwrap()) + }) + .collect::>>() + .ok_or("Invalid contract address format")?, + static_attributes: obj + .get("static_attributes") + .and_then(|v| v.as_object()) + .ok_or("static_attributes is required")? + .iter() + .map(|(k, v)| { + Ok(( + k.clone(), + Bytes::from_str( + v.as_str() + .ok_or("Invalid attribute value")?, + )?, + )) + }) + .collect::>>()?, + change: obj + .get("change") + .and_then(|v| v.as_str()) + .map(|s| match s.to_lowercase().as_str() { + "update" => tycho_core::dto::ChangeType::Update, + "deletion" => tycho_core::dto::ChangeType::Deletion, + "creation" => tycho_core::dto::ChangeType::Creation, + _ => tycho_core::dto::ChangeType::Unspecified, + }) + .unwrap_or(tycho_core::dto::ChangeType::Update), + creation_tx: Bytes::from_str( + obj.get("creation_tx") + .and_then(|v| v.as_str()) + .unwrap_or("0x"), + )?, + created_at: obj + .get("created_at") + .and_then(|v| v.as_str()) + .map(|s| { + chrono::DateTime::parse_from_rfc3339(s) + .unwrap() + .naive_utc() + }) + .unwrap_or_else(|| Utc::now().naive_utc()), + }) +} diff --git a/src/bin/tycho-encode.rs b/src/bin/tycho-encode.rs index c613cc5..11959e9 100644 --- a/src/bin/tycho-encode.rs +++ b/src/bin/tycho-encode.rs @@ -1,64 +1,24 @@ -use std::{ - io::{self, Read}, - str::FromStr, -}; +use std::io::{self, Read}; -use chrono::Utc; use hex; -use num_bigint::BigUint; use serde_json::Value; -use tycho_core::{ - dto::{Chain, ProtocolComponent}, - Bytes, -}; +use tycho_core::dto::Chain; use tycho_execution::encoding::{ evm::{ strategy_encoder::strategy_encoder_registry::EVMStrategyEncoderRegistry, tycho_encoder::EVMTychoEncoder, }, - models::{Solution, Swap}, strategy_encoder::StrategyEncoderRegistry, tycho_encoder::TychoEncoder, }; +mod lib { + pub mod help; + pub mod parse; +} + const DEFAULT_ROUTER_ADDRESS: &str = "0x1234567890123456789012345678901234567890"; const DEFAULT_EXECUTORS_FILE_PATH: &str = "src/encoding/config/executor_addresses.json"; -const HELP_TEXT: &str = "\ -USAGE: - tycho-encode [ROUTER_ADDRESS] - -ARGS: - ROUTER_ADDRESS The address of the router contract [default: 0x1234567890123456789012345678901234567890] - -The program reads a JSON object from stdin containing the swap details and outputs the encoded transaction. -The JSON object should have the following structure: -{ - \"sender\": \"0x...\", - \"receiver\": \"0x...\", - \"given_token\": \"0x...\", - \"given_amount\": \"123...\", - \"checked_token\": \"0x...\", - \"exact_out\": false, - \"slippage\": 0.01, - \"expected_amount\": \"123...\", - \"check_amount\": \"123...\", - \"swaps\": [{ - \"component\": { - \"id\": \"...\", - \"protocol_system\": \"...\", - \"protocol_type_name\": \"...\", - \"chain\": \"ethereum\", - \"tokens\": [\"0x...\"], - \"contract_ids\": [\"0x...\"], - \"static_attributes\": {\"key\": \"0x...\"} - }, - \"token_in\": \"0x...\", - \"token_out\": \"0x...\", - \"split\": 1.0 - }], - \"router_address\": \"0x...\", - \"direct_execution\": false -}"; #[tokio::main] async fn main() -> Result<(), Box> { @@ -66,7 +26,7 @@ async fn main() -> Result<(), Box> { // Show help text if requested if args.len() > 1 && (args[1] == "-h" || args[1] == "--help") { - println!("{}", HELP_TEXT); + println!("{}", lib::help::HELP_TEXT); return Ok(()); } @@ -83,7 +43,7 @@ async fn main() -> Result<(), Box> { if buffer.trim().is_empty() { eprintln!("Error: No input provided"); - eprintln!("{}", HELP_TEXT); + eprintln!("{}", lib::help::HELP_TEXT); std::process::exit(1); } @@ -104,212 +64,10 @@ async fn main() -> Result<(), Box> { Ok(()) } -fn parse_solution(input: Value) -> Result> { - let obj = input - .as_object() - .ok_or("Input must be a JSON object")?; - - Ok(Solution { - sender: parse_bytes( - obj.get("sender") - .ok_or("sender is required")?, - )?, - receiver: parse_bytes( - obj.get("receiver") - .ok_or("receiver is required")?, - )?, - given_token: parse_bytes( - obj.get("given_token") - .ok_or("given_token is required")?, - )?, - given_amount: parse_biguint( - obj.get("given_amount") - .ok_or("given_amount is required")?, - )?, - checked_token: parse_bytes( - obj.get("checked_token") - .ok_or("checked_token is required")?, - )?, - exact_out: obj - .get("exact_out") - .and_then(|v| v.as_bool()) - .unwrap_or(false), - slippage: obj - .get("slippage") - .and_then(|v| v.as_f64()), - expected_amount: obj - .get("expected_amount") - .map(parse_biguint) - .transpose()?, - check_amount: obj - .get("check_amount") - .map(parse_biguint) - .transpose()?, - swaps: parse_swaps( - obj.get("swaps") - .ok_or("swaps is required")?, - )?, - router_address: obj - .get("router_address") - .map(parse_bytes) - .transpose()?, - native_action: None, // TODO: Implement if needed - direct_execution: obj - .get("direct_execution") - .and_then(|v| v.as_bool()) - .unwrap_or(false), - }) -} - -fn parse_bytes(value: &Value) -> Result> { - let s = value - .as_str() - .ok_or("Expected string for bytes")?; - Ok(Bytes::from_str(s)?) -} - -fn parse_biguint(value: &Value) -> Result> { - let s = value - .as_str() - .ok_or("Expected string for BigUint")?; - Ok(BigUint::from_str(s)?) -} - -fn parse_swaps(value: &Value) -> Result, Box> { - let arr = value - .as_array() - .ok_or("Expected array for swaps")?; - let mut swaps = Vec::new(); - - for swap in arr { - let obj = swap - .as_object() - .ok_or("Swap must be an object")?; - swaps.push(Swap { - component: parse_protocol_component( - obj.get("component") - .ok_or("component is required")?, - )?, - token_in: parse_bytes( - obj.get("token_in") - .ok_or("token_in is required")?, - )?, - token_out: parse_bytes( - obj.get("token_out") - .ok_or("token_out is required")?, - )?, - split: obj - .get("split") - .and_then(|v| v.as_f64()) - .unwrap_or(0.0), - }); - } - - Ok(swaps) -} - -fn parse_protocol_component(obj: &Value) -> Result> { - let obj = obj - .as_object() - .ok_or("Expected object for ProtocolComponent")?; - - Ok(ProtocolComponent { - id: obj - .get("id") - .and_then(|v| v.as_str()) - .ok_or("id is required")? - .to_string(), - protocol_system: obj - .get("protocol_system") - .and_then(|v| v.as_str()) - .ok_or("protocol_system is required")? - .to_string(), - protocol_type_name: obj - .get("protocol_type_name") - .and_then(|v| v.as_str()) - .ok_or("protocol_type_name is required")? - .to_string(), - chain: obj - .get("chain") - .and_then(|v| v.as_str()) - .map(|s| match s.to_lowercase().as_str() { - "ethereum" => Chain::Ethereum, - "starknet" => Chain::Starknet, - "zksync" => Chain::ZkSync, - "arbitrum" => Chain::Arbitrum, - _ => Chain::Ethereum, // Default to Ethereum - }) - .unwrap_or(Chain::Ethereum), - tokens: obj - .get("tokens") - .and_then(|v| v.as_array()) - .ok_or("tokens is required")? - .iter() - .map(|v| { - v.as_str() - .map(|s| Bytes::from_str(s).unwrap()) - }) - .collect::>>() - .ok_or("Invalid token address format")?, - contract_ids: obj - .get("contract_ids") - .and_then(|v| v.as_array()) - .ok_or("contract_ids is required")? - .iter() - .map(|v| { - v.as_str() - .map(|s| Bytes::from_str(s).unwrap()) - }) - .collect::>>() - .ok_or("Invalid contract address format")?, - static_attributes: obj - .get("static_attributes") - .and_then(|v| v.as_object()) - .ok_or("static_attributes is required")? - .iter() - .map(|(k, v)| { - Ok(( - k.clone(), - Bytes::from_str( - v.as_str() - .ok_or("Invalid attribute value")?, - )?, - )) - }) - .collect::>>()?, - change: obj - .get("change") - .and_then(|v| v.as_str()) - .map(|s| match s.to_lowercase().as_str() { - "update" => tycho_core::dto::ChangeType::Update, - "deletion" => tycho_core::dto::ChangeType::Deletion, - "creation" => tycho_core::dto::ChangeType::Creation, - _ => tycho_core::dto::ChangeType::Unspecified, - }) - .unwrap_or(tycho_core::dto::ChangeType::Update), - creation_tx: Bytes::from_str( - obj.get("creation_tx") - .and_then(|v| v.as_str()) - .unwrap_or("0x"), - )?, - created_at: obj - .get("created_at") - .and_then(|v| v.as_str()) - .map(|s| { - chrono::DateTime::parse_from_rfc3339(s) - .unwrap() - .naive_utc() - }) - .unwrap_or_else(|| Utc::now().naive_utc()), - }) -} - fn encode_swaps(input: &str, router_address: &str) -> Result> { - // Parse the input JSON let input_json: Value = serde_json::from_str(input)?; - let solution = parse_solution(input_json)?; + let solution = lib::parse::parse_solution(input_json)?; - // Create encoder and encode the solution let strategy_selector = EVMStrategyEncoderRegistry::new(Chain::Ethereum, DEFAULT_EXECUTORS_FILE_PATH, None)?; let encoder = EVMTychoEncoder::new(strategy_selector, router_address.to_string())?; From d3be9d1489121522333253ce9f420865f59fdf6a Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 5 Feb 2025 17:29:17 +0530 Subject: [PATCH 04/19] feat: add md --- src/bin/run.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/bin/run.md diff --git a/src/bin/run.md b/src/bin/run.md new file mode 100644 index 0000000..703749b --- /dev/null +++ b/src/bin/run.md @@ -0,0 +1,40 @@ +# Tycho Encode Usage Guide + +This document demonstrates how to use the `tycho-encode` binary to encode transaction data for executing swaps and strategies. + +## Basic Usage + +To encode a transaction, you can pipe a JSON payload to the `tycho-encode` binary: + +```bash +echo '' | cargo run --bin tycho-encode +``` + +## Example + +Here's a complete example that encodes a swap from WETH to DAI using Uniswap V2: + +```bash +echo '{"sender":"0x1234567890123456789012345678901234567890","receiver":"0x1234567890123456789012345678901234567890","given_token":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","given_amount":"1000000000000000000","checked_token":"0x6B175474E89094C44Da98b954EedeAC495271d0F","exact_out":false,"slippage":0.01,"expected_amount":"1000000000000000000","check_amount":"990000000000000000","swaps":[{"component":{"id":"0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640","protocol_system":"uniswap_v2","protocol_type_name":"UniswapV2Pool","chain":"ethereum","tokens":["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"],"contract_ids":["0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"],"static_attributes":{"factory":"0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"}},"token_in":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","token_out":"0x6B175474E89094C44Da98b954EedeAC495271d0F","split":1.0}],"router_address":"0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D","direct_execution":true}' | cargo run --bin tycho-encode +``` + +## JSON Payload Structure + +The input JSON payload requires the following fields: + +- `sender`: The address initiating the transaction +- `receiver`: The address receiving the output tokens +- `given_token`: The address of the input token (e.g., WETH) +- `given_amount`: The amount of input tokens (in wei) +- `checked_token`: The address of the output token (e.g., DAI) +- `exact_out`: Boolean indicating if this is an exact output swap +- `slippage`: The maximum allowed slippage (e.g., 0.01 for 1%) +- `expected_amount`: The expected output amount +- `check_amount`: The minimum acceptable output amount (accounting for slippage) +- `swaps`: Array of swap steps, each containing: + - `component`: Details about the DEX/protocol being used + - `token_in`: Input token address for this step + - `token_out`: Output token address for this step + - `split`: Proportion of tokens to route through this step (1.0 = 100%) +- `router_address`: The address of the protocol's router contract +- `direct_execution`: Boolean indicating if the transaction should be executed directly \ No newline at end of file From 6cec83fde57e64405499e2343179bf4bd2a40820 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 5 Feb 2025 17:36:27 +0530 Subject: [PATCH 05/19] fix: ci --- Cargo.toml | 1 + src/bin/tycho-encode.rs | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 90a9356..88db5f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [[bin]] name = "tycho-encode" path = "src/bin/tycho-encode.rs" +required-features = ["evm"] [dependencies] dotenv = "0.15.0" diff --git a/src/bin/tycho-encode.rs b/src/bin/tycho-encode.rs index 11959e9..13a9630 100644 --- a/src/bin/tycho-encode.rs +++ b/src/bin/tycho-encode.rs @@ -1,6 +1,5 @@ use std::io::{self, Read}; -use hex; use serde_json::Value; use tycho_core::dto::Chain; use tycho_execution::encoding::{ From 07288163d27a12218794d6952f9d46150e1f7dfb Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 5 Feb 2025 20:34:56 +0530 Subject: [PATCH 06/19] chore: merge run.md to readme.md --- README.md | 45 ++++++++++++++++++++++++++++++++++++++++++--- src/bin/run.md | 40 ---------------------------------------- 2 files changed, 42 insertions(+), 43 deletions(-) delete mode 100644 src/bin/run.md diff --git a/README.md b/README.md index 49b15fe..b34c2e5 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,55 @@ TODO: add banner Tycho Execution makes it easy to trade on different DEXs by handling the complex encoding for you. Instead of creating -custom code for each DEX, you get a simple, ready-to-use tool that generates the necessary data to execute trades. It’s +custom code for each DEX, you get a simple, ready-to-use tool that generates the necessary data to execute trades. It's designed to be safe, straightforward, and quick to set up, so anyone can start trading without extra effort. -# Contract Analysis +## Usage Guide + +### Encoding Transactions + +To encode a transaction, you can pipe a JSON payload to the `tycho-encode` binary: + +```bash +echo '' | cargo run --bin tycho-encode +``` + +#### Example + +Here's a complete example that encodes a swap from WETH to DAI using Uniswap V2: + +```bash +echo '{"sender":"0x1234567890123456789012345678901234567890","receiver":"0x1234567890123456789012345678901234567890","given_token":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","given_amount":"1000000000000000000","checked_token":"0x6B175474E89094C44Da98b954EedeAC495271d0F","exact_out":false,"slippage":0.01,"expected_amount":"1000000000000000000","check_amount":"990000000000000000","swaps":[{"component":{"id":"0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640","protocol_system":"uniswap_v2","protocol_type_name":"UniswapV2Pool","chain":"ethereum","tokens":["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"],"contract_ids":["0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"],"static_attributes":{"factory":"0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"}},"token_in":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","token_out":"0x6B175474E89094C44Da98b954EedeAC495271d0F","split":1.0}],"router_address":"0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D","direct_execution":true}' | cargo run --bin tycho-encode +``` + +#### JSON Payload Structure + +The input JSON payload requires the following fields: + +- `sender`: The address initiating the transaction +- `receiver`: The address receiving the output tokens +- `given_token`: The address of the input token (e.g., WETH) +- `given_amount`: The amount of input tokens (in wei) +- `checked_token`: The address of the output token (e.g., DAI) +- `exact_out`: Boolean indicating if this is an exact output swap +- `slippage`: The maximum allowed slippage (e.g., 0.01 for 1%) +- `expected_amount`: The expected output amount +- `check_amount`: The minimum acceptable output amount (accounting for slippage) +- `swaps`: Array of swap steps, each containing: + - `component`: Details about the DEX/protocol being used + - `token_in`: Input token address for this step + - `token_out`: Output token address for this step + - `split`: Proportion of tokens to route through this step (1.0 = 100%) +- `router_address`: The address of the protocol's router contract +- `direct_execution`: Boolean indicating if the transaction should be executed directly + +## Contract Analysis We use [Slither](https://github.com/crytic/slither) to detect any potential vulnerabilities in our contracts. To run locally, simply install Slither in your conda env and run it inside the foundry directory. -``` +```bash conda create --name tycho-execution python=3.10 conda activate tycho-execution diff --git a/src/bin/run.md b/src/bin/run.md deleted file mode 100644 index 703749b..0000000 --- a/src/bin/run.md +++ /dev/null @@ -1,40 +0,0 @@ -# Tycho Encode Usage Guide - -This document demonstrates how to use the `tycho-encode` binary to encode transaction data for executing swaps and strategies. - -## Basic Usage - -To encode a transaction, you can pipe a JSON payload to the `tycho-encode` binary: - -```bash -echo '' | cargo run --bin tycho-encode -``` - -## Example - -Here's a complete example that encodes a swap from WETH to DAI using Uniswap V2: - -```bash -echo '{"sender":"0x1234567890123456789012345678901234567890","receiver":"0x1234567890123456789012345678901234567890","given_token":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","given_amount":"1000000000000000000","checked_token":"0x6B175474E89094C44Da98b954EedeAC495271d0F","exact_out":false,"slippage":0.01,"expected_amount":"1000000000000000000","check_amount":"990000000000000000","swaps":[{"component":{"id":"0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640","protocol_system":"uniswap_v2","protocol_type_name":"UniswapV2Pool","chain":"ethereum","tokens":["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"],"contract_ids":["0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"],"static_attributes":{"factory":"0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"}},"token_in":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","token_out":"0x6B175474E89094C44Da98b954EedeAC495271d0F","split":1.0}],"router_address":"0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D","direct_execution":true}' | cargo run --bin tycho-encode -``` - -## JSON Payload Structure - -The input JSON payload requires the following fields: - -- `sender`: The address initiating the transaction -- `receiver`: The address receiving the output tokens -- `given_token`: The address of the input token (e.g., WETH) -- `given_amount`: The amount of input tokens (in wei) -- `checked_token`: The address of the output token (e.g., DAI) -- `exact_out`: Boolean indicating if this is an exact output swap -- `slippage`: The maximum allowed slippage (e.g., 0.01 for 1%) -- `expected_amount`: The expected output amount -- `check_amount`: The minimum acceptable output amount (accounting for slippage) -- `swaps`: Array of swap steps, each containing: - - `component`: Details about the DEX/protocol being used - - `token_in`: Input token address for this step - - `token_out`: Output token address for this step - - `split`: Proportion of tokens to route through this step (1.0 = 100%) -- `router_address`: The address of the protocol's router contract -- `direct_execution`: Boolean indicating if the transaction should be executed directly \ No newline at end of file From 80f1ca913b9ca74f6dd3739d9472533dcda59892 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 5 Feb 2025 20:39:59 +0530 Subject: [PATCH 07/19] feat: default native action --- src/bin/lib/parse.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/bin/lib/parse.rs b/src/bin/lib/parse.rs index 99f24cb..17f42f2 100644 --- a/src/bin/lib/parse.rs +++ b/src/bin/lib/parse.rs @@ -7,7 +7,7 @@ use tycho_core::{ dto::{Chain, ProtocolComponent}, Bytes, }; -use tycho_execution::encoding::models::{Solution, Swap}; +use tycho_execution::encoding::models::{NativeAction, Solution, Swap}; pub fn parse_solution(input: Value) -> Result> { let obj = input @@ -58,7 +58,15 @@ pub fn parse_solution(input: Value) -> Result Some(NativeAction::Wrap), + "unwrap" => Some(NativeAction::Unwrap), + _ => None, // Default to None + }) + .flatten(), direct_execution: obj .get("direct_execution") .and_then(|v| v.as_bool()) From d3ad0ba5bfd50cc3db50b8d341fca2c1c5fcdad3 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 5 Feb 2025 20:47:51 +0530 Subject: [PATCH 08/19] feat: add default private key --- src/bin/lib/help.rs | 3 ++- src/bin/tycho-encode.rs | 38 +++++++++++++++++++++++++++++++------- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/bin/lib/help.rs b/src/bin/lib/help.rs index 250e5aa..a34bd99 100644 --- a/src/bin/lib/help.rs +++ b/src/bin/lib/help.rs @@ -1,9 +1,10 @@ pub const HELP_TEXT: &str = "\ USAGE: - tycho-encode [ROUTER_ADDRESS] + tycho-encode [ROUTER_ADDRESS] [PRIVATE_KEY] ARGS: ROUTER_ADDRESS The address of the router contract [default: 0x1234567890123456789012345678901234567890] + PRIVATE_KEY The private key for signing Permit2 approvals (required when direct_execution is false) The program reads a JSON object from stdin containing the swap details and outputs the encoded transaction. The JSON object should have the following structure: diff --git a/src/bin/tycho-encode.rs b/src/bin/tycho-encode.rs index 13a9630..60d2a51 100644 --- a/src/bin/tycho-encode.rs +++ b/src/bin/tycho-encode.rs @@ -18,9 +18,10 @@ mod lib { const DEFAULT_ROUTER_ADDRESS: &str = "0x1234567890123456789012345678901234567890"; const DEFAULT_EXECUTORS_FILE_PATH: &str = "src/encoding/config/executor_addresses.json"; +const DEFAULT_PRIVATE_KEY: &str = + "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234"; -#[tokio::main] -async fn main() -> Result<(), Box> { +fn main() -> Result<(), Box> { let args: Vec = std::env::args().collect(); // Show help text if requested @@ -34,6 +35,11 @@ async fn main() -> Result<(), Box> { .map(|s| s.as_str()) .unwrap_or(DEFAULT_ROUTER_ADDRESS); + let private_key = args + .get(2) + .map(|s| s.to_string()) + .or_else(|| Some(DEFAULT_PRIVATE_KEY.to_string())); + // Read from stdin until EOF let mut buffer = String::new(); io::stdin() @@ -47,11 +53,25 @@ async fn main() -> Result<(), Box> { } // Parse the JSON input to verify it's valid - serde_json::from_str::(&buffer) - .map_err(|e| format!("Failed to parse JSON input: {}", e))?; + let input_json: Value = + serde_json::from_str(&buffer).map_err(|e| format!("Failed to parse JSON input: {}", e))?; + + // Check if direct_execution is false and private_key is missing + if let Some(obj) = input_json.as_object() { + if let Some(direct_execution) = obj + .get("direct_execution") + .and_then(|v| v.as_bool()) + { + if !direct_execution && private_key.is_none() { + eprintln!("Error: Private key is required when direct_execution is false"); + eprintln!("{}", lib::help::HELP_TEXT); + std::process::exit(1); + } + } + } // Encode the solution - let encoded = encode_swaps(&buffer, router_address)?; + let encoded = encode_swaps(&buffer, router_address, private_key)?; // Output the encoded result as JSON to stdout println!( @@ -63,12 +83,16 @@ async fn main() -> Result<(), Box> { Ok(()) } -fn encode_swaps(input: &str, router_address: &str) -> Result> { +fn encode_swaps( + input: &str, + router_address: &str, + private_key: Option, +) -> Result> { let input_json: Value = serde_json::from_str(input)?; let solution = lib::parse::parse_solution(input_json)?; let strategy_selector = - EVMStrategyEncoderRegistry::new(Chain::Ethereum, DEFAULT_EXECUTORS_FILE_PATH, None)?; + EVMStrategyEncoderRegistry::new(Chain::Ethereum, DEFAULT_EXECUTORS_FILE_PATH, private_key)?; let encoder = EVMTychoEncoder::new(strategy_selector, router_address.to_string())?; let transactions = encoder.encode_router_calldata(vec![solution])?; From c4f9fd0fa6e1ee4d32ec0cd7e4d833f18681eec1 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 5 Feb 2025 20:50:44 +0530 Subject: [PATCH 09/19] fix: remove redundant parse checks --- src/bin/tycho-encode.rs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/bin/tycho-encode.rs b/src/bin/tycho-encode.rs index 60d2a51..dadf53b 100644 --- a/src/bin/tycho-encode.rs +++ b/src/bin/tycho-encode.rs @@ -52,24 +52,6 @@ fn main() -> Result<(), Box> { std::process::exit(1); } - // Parse the JSON input to verify it's valid - let input_json: Value = - serde_json::from_str(&buffer).map_err(|e| format!("Failed to parse JSON input: {}", e))?; - - // Check if direct_execution is false and private_key is missing - if let Some(obj) = input_json.as_object() { - if let Some(direct_execution) = obj - .get("direct_execution") - .and_then(|v| v.as_bool()) - { - if !direct_execution && private_key.is_none() { - eprintln!("Error: Private key is required when direct_execution is false"); - eprintln!("{}", lib::help::HELP_TEXT); - std::process::exit(1); - } - } - } - // Encode the solution let encoded = encode_swaps(&buffer, router_address, private_key)?; From a3cf4430563f88490a62a7e7a0051c3c64ea6d81 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 5 Feb 2025 20:54:30 +0530 Subject: [PATCH 10/19] fix: ci --- src/bin/lib/parse.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/bin/lib/parse.rs b/src/bin/lib/parse.rs index 17f42f2..a4f4801 100644 --- a/src/bin/lib/parse.rs +++ b/src/bin/lib/parse.rs @@ -61,12 +61,11 @@ pub fn parse_solution(input: Value) -> Result Some(NativeAction::Wrap), "unwrap" => Some(NativeAction::Unwrap), _ => None, // Default to None - }) - .flatten(), + }), direct_execution: obj .get("direct_execution") .and_then(|v| v.as_bool()) From fd4045e6fe9c2379df2c278282db1e26e4b83c20 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 5 Feb 2025 22:57:38 +0530 Subject: [PATCH 11/19] feat: remove manual parsing --- Cargo.lock | 1 + Cargo.toml | 2 +- input.json | 1 + src/bin/lib/parse.rs | 217 ---------------------------------------- src/bin/tycho-encode.rs | 5 +- src/encoding/models.rs | 69 ++++++++++++- 6 files changed, 71 insertions(+), 224 deletions(-) create mode 100644 input.json delete mode 100644 src/bin/lib/parse.rs diff --git a/Cargo.lock b/Cargo.lock index e35c23e..5446eda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2585,6 +2585,7 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 88db5f1..4110639 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ required-features = ["evm"] [dependencies] dotenv = "0.15.0" lazy_static = "1.4.0" -num-bigint = "0.4.6" +num-bigint = { version = "0.4.6", features = ["serde"] } hex = "0.4.3" num-traits = "0.2.19" serde = { version = "1.0.217", features = ["derive"] } diff --git a/input.json b/input.json new file mode 100644 index 0000000..8fc6b07 --- /dev/null +++ b/input.json @@ -0,0 +1 @@ +{"sender":"0x1234567890123456789012345678901234567890","receiver":"0x1234567890123456789012345678901234567890","given_token":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","given_amount":"1000000000000000000","checked_token":"0x6B175474E89094C44Da98b954EedeAC495271d0F","exact_out":false,"slippage":0.01,"expected_amount":"1000000000000000000","check_amount":"990000000000000000","swaps":[{"component":{"id":"0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640","protocol_system":"uniswap_v2","protocol_type_name":"UniswapV2Pool","chain":"ethereum","tokens":["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"],"contract_ids":["0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"],"static_attributes":{"factory":"0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f"},"change":"Update","creation_tx":"0x0000000000000000000000000000000000000000000000000000000000000000","created_at":"2024-02-28T12:00:00Z"},"token_in":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","token_out":"0x6B175474E89094C44Da98b954EedeAC495271d0F","split":1.0}],"router_address":"0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D","direct_execution":true} \ No newline at end of file diff --git a/src/bin/lib/parse.rs b/src/bin/lib/parse.rs deleted file mode 100644 index a4f4801..0000000 --- a/src/bin/lib/parse.rs +++ /dev/null @@ -1,217 +0,0 @@ -use std::str::FromStr; - -use chrono::Utc; -use num_bigint::BigUint; -use serde_json::Value; -use tycho_core::{ - dto::{Chain, ProtocolComponent}, - Bytes, -}; -use tycho_execution::encoding::models::{NativeAction, Solution, Swap}; - -pub fn parse_solution(input: Value) -> Result> { - let obj = input - .as_object() - .ok_or("Input must be a JSON object")?; - - Ok(Solution { - sender: parse_bytes( - obj.get("sender") - .ok_or("sender is required")?, - )?, - receiver: parse_bytes( - obj.get("receiver") - .ok_or("receiver is required")?, - )?, - given_token: parse_bytes( - obj.get("given_token") - .ok_or("given_token is required")?, - )?, - given_amount: parse_biguint( - obj.get("given_amount") - .ok_or("given_amount is required")?, - )?, - checked_token: parse_bytes( - obj.get("checked_token") - .ok_or("checked_token is required")?, - )?, - exact_out: obj - .get("exact_out") - .and_then(|v| v.as_bool()) - .unwrap_or(false), - slippage: obj - .get("slippage") - .and_then(|v| v.as_f64()), - expected_amount: obj - .get("expected_amount") - .map(parse_biguint) - .transpose()?, - check_amount: obj - .get("check_amount") - .map(parse_biguint) - .transpose()?, - swaps: parse_swaps( - obj.get("swaps") - .ok_or("swaps is required")?, - )?, - router_address: obj - .get("router_address") - .map(parse_bytes) - .transpose()?, - native_action: obj - .get("native_action") - .and_then(|v| v.as_str()) - .and_then(|s| match s.to_lowercase().as_str() { - "wrap" => Some(NativeAction::Wrap), - "unwrap" => Some(NativeAction::Unwrap), - _ => None, // Default to None - }), - direct_execution: obj - .get("direct_execution") - .and_then(|v| v.as_bool()) - .unwrap_or(false), - }) -} - -fn parse_bytes(value: &Value) -> Result> { - let s = value - .as_str() - .ok_or("Expected string for bytes")?; - Ok(Bytes::from_str(s)?) -} - -fn parse_biguint(value: &Value) -> Result> { - let s = value - .as_str() - .ok_or("Expected string for BigUint")?; - Ok(BigUint::from_str(s)?) -} - -fn parse_swaps(value: &Value) -> Result, Box> { - let arr = value - .as_array() - .ok_or("Expected array for swaps")?; - let mut swaps = Vec::new(); - - for swap in arr { - let obj = swap - .as_object() - .ok_or("Swap must be an object")?; - swaps.push(Swap { - component: parse_protocol_component( - obj.get("component") - .ok_or("component is required")?, - )?, - token_in: parse_bytes( - obj.get("token_in") - .ok_or("token_in is required")?, - )?, - token_out: parse_bytes( - obj.get("token_out") - .ok_or("token_out is required")?, - )?, - split: obj - .get("split") - .and_then(|v| v.as_f64()) - .unwrap_or(0.0), - }); - } - - Ok(swaps) -} - -fn parse_protocol_component(obj: &Value) -> Result> { - let obj = obj - .as_object() - .ok_or("Expected object for ProtocolComponent")?; - - Ok(ProtocolComponent { - id: obj - .get("id") - .and_then(|v| v.as_str()) - .ok_or("id is required")? - .to_string(), - protocol_system: obj - .get("protocol_system") - .and_then(|v| v.as_str()) - .ok_or("protocol_system is required")? - .to_string(), - protocol_type_name: obj - .get("protocol_type_name") - .and_then(|v| v.as_str()) - .ok_or("protocol_type_name is required")? - .to_string(), - chain: obj - .get("chain") - .and_then(|v| v.as_str()) - .map(|s| match s.to_lowercase().as_str() { - "ethereum" => Chain::Ethereum, - "starknet" => Chain::Starknet, - "zksync" => Chain::ZkSync, - "arbitrum" => Chain::Arbitrum, - _ => Chain::Ethereum, // Default to Ethereum - }) - .unwrap_or(Chain::Ethereum), - tokens: obj - .get("tokens") - .and_then(|v| v.as_array()) - .ok_or("tokens is required")? - .iter() - .map(|v| { - v.as_str() - .map(|s| Bytes::from_str(s).unwrap()) - }) - .collect::>>() - .ok_or("Invalid token address format")?, - contract_ids: obj - .get("contract_ids") - .and_then(|v| v.as_array()) - .ok_or("contract_ids is required")? - .iter() - .map(|v| { - v.as_str() - .map(|s| Bytes::from_str(s).unwrap()) - }) - .collect::>>() - .ok_or("Invalid contract address format")?, - static_attributes: obj - .get("static_attributes") - .and_then(|v| v.as_object()) - .ok_or("static_attributes is required")? - .iter() - .map(|(k, v)| { - Ok(( - k.clone(), - Bytes::from_str( - v.as_str() - .ok_or("Invalid attribute value")?, - )?, - )) - }) - .collect::>>()?, - change: obj - .get("change") - .and_then(|v| v.as_str()) - .map(|s| match s.to_lowercase().as_str() { - "update" => tycho_core::dto::ChangeType::Update, - "deletion" => tycho_core::dto::ChangeType::Deletion, - "creation" => tycho_core::dto::ChangeType::Creation, - _ => tycho_core::dto::ChangeType::Unspecified, - }) - .unwrap_or(tycho_core::dto::ChangeType::Update), - creation_tx: Bytes::from_str( - obj.get("creation_tx") - .and_then(|v| v.as_str()) - .unwrap_or("0x"), - )?, - created_at: obj - .get("created_at") - .and_then(|v| v.as_str()) - .map(|s| { - chrono::DateTime::parse_from_rfc3339(s) - .unwrap() - .naive_utc() - }) - .unwrap_or_else(|| Utc::now().naive_utc()), - }) -} diff --git a/src/bin/tycho-encode.rs b/src/bin/tycho-encode.rs index dadf53b..9acf27a 100644 --- a/src/bin/tycho-encode.rs +++ b/src/bin/tycho-encode.rs @@ -7,13 +7,13 @@ use tycho_execution::encoding::{ strategy_encoder::strategy_encoder_registry::EVMStrategyEncoderRegistry, tycho_encoder::EVMTychoEncoder, }, + models::Solution, strategy_encoder::StrategyEncoderRegistry, tycho_encoder::TychoEncoder, }; mod lib { pub mod help; - pub mod parse; } const DEFAULT_ROUTER_ADDRESS: &str = "0x1234567890123456789012345678901234567890"; @@ -70,8 +70,7 @@ fn encode_swaps( router_address: &str, private_key: Option, ) -> Result> { - let input_json: Value = serde_json::from_str(input)?; - let solution = lib::parse::parse_solution(input_json)?; + let solution: Solution = serde_json::from_str(input)?; let strategy_selector = EVMStrategyEncoderRegistry::new(Chain::Ethereum, DEFAULT_EXECUTORS_FILE_PATH, private_key)?; diff --git a/src/encoding/models.rs b/src/encoding/models.rs index 24c7c9d..598b5bf 100644 --- a/src/encoding/models.rs +++ b/src/encoding/models.rs @@ -1,7 +1,9 @@ use num_bigint::BigUint; +use serde::{Deserialize, Serialize}; use tycho_core::{dto::ProtocolComponent, Bytes}; -#[derive(Clone, Default, Debug)] +#[derive(Clone, Default, Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] pub struct Solution { /// Address of the sender. pub sender: Bytes, @@ -10,18 +12,22 @@ pub struct Solution { /// The token being sold (exact in) or bought (exact out). pub given_token: Bytes, /// Amount of the given token. + #[serde(with = "biguint_string")] pub given_amount: BigUint, /// The token being bought (exact in) or sold (exact out). pub checked_token: Bytes, /// False if the solution is an exact input solution. Currently only exact input solutions are /// supported. + #[serde(default)] pub exact_out: bool, // If set, it will be applied to expected_amount pub slippage: Option, /// Expected amount of the bought token (exact in) or sold token (exact out). + #[serde(with = "biguint_string_option")] pub expected_amount: Option, /// Minimum amount to be checked for the solution to be valid. /// If not set, the check will not be performed. + #[serde(with = "biguint_string_option")] pub check_amount: Option, /// List of swaps to fulfill the solution. pub swaps: Vec, @@ -32,16 +38,18 @@ pub struct Solution { /// If set to true, the solution will be encoded to be sent directly to the Executor and /// skip the router. The user is responsible for managing necessary approvals and token /// transfers. + #[serde(default)] pub direct_execution: bool, } -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] pub enum NativeAction { Wrap, Unwrap, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct Swap { /// Protocol component from tycho indexer pub component: ProtocolComponent, @@ -50,6 +58,7 @@ pub struct Swap { /// Token being output from the pool. pub token_out: Bytes, /// Percentage of the amount to be swapped in this operation (for example, 0.5 means 50%) + #[serde(default)] pub split: f64, } @@ -68,3 +77,57 @@ pub struct EncodingContext { pub exact_out: bool, pub router_address: Bytes, } + +// Custom serialization for BigUint as string +mod biguint_string { + use std::str::FromStr; + + use num_bigint::BigUint; + use serde::{self, Deserialize, Deserializer, Serializer}; + + pub fn serialize(value: &BigUint, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&value.to_string()) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + BigUint::from_str(&s).map_err(serde::de::Error::custom) + } +} + +// Custom serialization for Option as string +mod biguint_string_option { + use std::str::FromStr; + + use num_bigint::BigUint; + use serde::{self, Deserialize, Deserializer, Serializer}; + + pub fn serialize(value: &Option, serializer: S) -> Result + where + S: Serializer, + { + match value { + Some(v) => serializer.serialize_str(&v.to_string()), + None => serializer.serialize_none(), + } + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let opt = Option::::deserialize(deserializer)?; + match opt { + Some(s) => BigUint::from_str(&s) + .map(Some) + .map_err(serde::de::Error::custom), + None => Ok(None), + } + } +} From 38ce6e5c79b8873179cf96f34b534f6c115ab145 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 5 Feb 2025 22:57:56 +0530 Subject: [PATCH 12/19] chore: rm json --- input.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 input.json diff --git a/input.json b/input.json deleted file mode 100644 index 8fc6b07..0000000 --- a/input.json +++ /dev/null @@ -1 +0,0 @@ -{"sender":"0x1234567890123456789012345678901234567890","receiver":"0x1234567890123456789012345678901234567890","given_token":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","given_amount":"1000000000000000000","checked_token":"0x6B175474E89094C44Da98b954EedeAC495271d0F","exact_out":false,"slippage":0.01,"expected_amount":"1000000000000000000","check_amount":"990000000000000000","swaps":[{"component":{"id":"0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640","protocol_system":"uniswap_v2","protocol_type_name":"UniswapV2Pool","chain":"ethereum","tokens":["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"],"contract_ids":["0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"],"static_attributes":{"factory":"0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f"},"change":"Update","creation_tx":"0x0000000000000000000000000000000000000000000000000000000000000000","created_at":"2024-02-28T12:00:00Z"},"token_in":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","token_out":"0x6B175474E89094C44Da98b954EedeAC495271d0F","split":1.0}],"router_address":"0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D","direct_execution":true} \ No newline at end of file From b93856073cad79564190a43007f33a2cf4f3dbd7 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Thu, 6 Feb 2025 00:16:35 +0530 Subject: [PATCH 13/19] feat: add serde primitive, update command --- README.md | 2 +- src/bin/tycho-encode.rs | 4 +-- src/encoding/mod.rs | 1 + src/encoding/models.rs | 56 ++--------------------------- src/encoding/serde_primitives.rs | 60 ++++++++++++++++++++++++++++++++ 5 files changed, 66 insertions(+), 57 deletions(-) create mode 100644 src/encoding/serde_primitives.rs diff --git a/README.md b/README.md index b34c2e5..acb8368 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ echo '' | cargo run --bin tycho-encode Here's a complete example that encodes a swap from WETH to DAI using Uniswap V2: ```bash -echo '{"sender":"0x1234567890123456789012345678901234567890","receiver":"0x1234567890123456789012345678901234567890","given_token":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","given_amount":"1000000000000000000","checked_token":"0x6B175474E89094C44Da98b954EedeAC495271d0F","exact_out":false,"slippage":0.01,"expected_amount":"1000000000000000000","check_amount":"990000000000000000","swaps":[{"component":{"id":"0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640","protocol_system":"uniswap_v2","protocol_type_name":"UniswapV2Pool","chain":"ethereum","tokens":["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"],"contract_ids":["0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"],"static_attributes":{"factory":"0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"}},"token_in":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","token_out":"0x6B175474E89094C44Da98b954EedeAC495271d0F","split":1.0}],"router_address":"0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D","direct_execution":true}' | cargo run --bin tycho-encode +echo '{"sender":"0x1234567890123456789012345678901234567890","receiver":"0x1234567890123456789012345678901234567890","given_token":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","given_amount":"1000000000000000000","checked_token":"0x6B175474E89094C44Da98b954EedeAC495271d0F","exact_out":false,"slippage":0.01,"expected_amount":"1000000000000000000","check_amount":"990000000000000000","router_address":"0xaa820C29648D5EA543d712cC928377Bd7206a0E7","swaps":[{"component":{"id":"0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640","protocol_system":"uniswap_v2","protocol_type_name":"UniswapV2Pool","chain":"ethereum","tokens":["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"],"contract_ids":["0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"],"static_attributes":{"factory":"0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f"},"change":"Update","creation_tx":"0x0000000000000000000000000000000000000000000000000000000000000000","created_at":"2024-02-28T12:00:00"},"token_in":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","token_out":"0x6B175474E89094C44Da98b954EedeAC495271d0F","split":1.0}],"direct_execution":false}' | cargo run ``` #### JSON Payload Structure diff --git a/src/bin/tycho-encode.rs b/src/bin/tycho-encode.rs index 9acf27a..e038115 100644 --- a/src/bin/tycho-encode.rs +++ b/src/bin/tycho-encode.rs @@ -16,10 +16,10 @@ mod lib { pub mod help; } -const DEFAULT_ROUTER_ADDRESS: &str = "0x1234567890123456789012345678901234567890"; +const DEFAULT_ROUTER_ADDRESS: &str = "0xaa820C29648D5EA543d712cC928377Bd7206a0E7"; const DEFAULT_EXECUTORS_FILE_PATH: &str = "src/encoding/config/executor_addresses.json"; const DEFAULT_PRIVATE_KEY: &str = - "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234"; + "0x938f4da9d3a947a4a6c53cfd8fcdd876641d6a4519243820b648af0bc3e67f7c"; fn main() -> Result<(), Box> { let args: Vec = std::env::args().collect(); diff --git a/src/encoding/mod.rs b/src/encoding/mod.rs index 502e336..be17c4a 100644 --- a/src/encoding/mod.rs +++ b/src/encoding/mod.rs @@ -2,6 +2,7 @@ mod errors; #[cfg(feature = "evm")] pub mod evm; pub mod models; +pub mod serde_primitives; pub mod strategy_encoder; mod swap_encoder; pub mod tycho_encoder; diff --git a/src/encoding/models.rs b/src/encoding/models.rs index 598b5bf..44504b5 100644 --- a/src/encoding/models.rs +++ b/src/encoding/models.rs @@ -2,6 +2,8 @@ use num_bigint::BigUint; use serde::{Deserialize, Serialize}; use tycho_core::{dto::ProtocolComponent, Bytes}; +use crate::encoding::serde_primitives::{biguint_string, biguint_string_option}; + #[derive(Clone, Default, Debug, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub struct Solution { @@ -77,57 +79,3 @@ pub struct EncodingContext { pub exact_out: bool, pub router_address: Bytes, } - -// Custom serialization for BigUint as string -mod biguint_string { - use std::str::FromStr; - - use num_bigint::BigUint; - use serde::{self, Deserialize, Deserializer, Serializer}; - - pub fn serialize(value: &BigUint, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&value.to_string()) - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - BigUint::from_str(&s).map_err(serde::de::Error::custom) - } -} - -// Custom serialization for Option as string -mod biguint_string_option { - use std::str::FromStr; - - use num_bigint::BigUint; - use serde::{self, Deserialize, Deserializer, Serializer}; - - pub fn serialize(value: &Option, serializer: S) -> Result - where - S: Serializer, - { - match value { - Some(v) => serializer.serialize_str(&v.to_string()), - None => serializer.serialize_none(), - } - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - let opt = Option::::deserialize(deserializer)?; - match opt { - Some(s) => BigUint::from_str(&s) - .map(Some) - .map_err(serde::de::Error::custom), - None => Ok(None), - } - } -} diff --git a/src/encoding/serde_primitives.rs b/src/encoding/serde_primitives.rs new file mode 100644 index 0000000..9898815 --- /dev/null +++ b/src/encoding/serde_primitives.rs @@ -0,0 +1,60 @@ +use std::str::FromStr; + +use num_bigint::BigUint; +use serde::{self, Deserialize, Deserializer, Serializer}; + +fn serialize_biguint(value: &BigUint, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&value.to_string()) +} + +fn deserialize_biguint<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + BigUint::from_str(&s).map_err(serde::de::Error::custom) +} + +pub mod biguint_string { + use super::*; + + pub fn serialize(value: &BigUint, serializer: S) -> Result + where + S: Serializer, + { + serialize_biguint(value, serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserialize_biguint(deserializer) + } +} + +pub mod biguint_string_option { + use super::*; + + pub fn serialize(value: &Option, serializer: S) -> Result + where + S: Serializer, + { + match value { + Some(v) => serialize_biguint(v, serializer), + None => serializer.serialize_none(), + } + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + Option::::deserialize(deserializer)? + .map(|s| BigUint::from_str(&s).map_err(serde::de::Error::custom)) + .transpose() + } +} From a5166f282dfbcd6c161a84dee7b9dac0efe01ba3 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Thu, 6 Feb 2025 18:30:52 +0530 Subject: [PATCH 14/19] feat: use clap for cli and resolve pr comments --- Cargo.lock | 115 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + README.md | 19 ++++--- src/bin/lib/cli.rs | 45 ++++++++++++++++ src/bin/lib/help.rs | 37 ------------- src/bin/tycho-encode.rs | 32 +++-------- src/encoding/models.rs | 1 - 7 files changed, 181 insertions(+), 69 deletions(-) create mode 100644 src/bin/lib/cli.rs delete mode 100644 src/bin/lib/help.rs diff --git a/Cargo.lock b/Cargo.lock index 5446eda..201af18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -647,6 +647,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.95" @@ -1298,6 +1348,46 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "clap" +version = "4.5.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + [[package]] name = "coins-ledger" version = "0.12.0" @@ -1321,6 +1411,12 @@ dependencies = [ "wasm-bindgen-futures", ] +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "const-hex" version = "1.14.0" @@ -2342,6 +2438,12 @@ version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.10.5" @@ -3752,6 +3854,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "strum" version = "0.25.0" @@ -4238,6 +4346,7 @@ dependencies = [ "alloy-primitives", "alloy-sol-types", "chrono", + "clap", "dotenv", "hex", "lazy_static", @@ -4358,6 +4467,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "utoipa" version = "4.2.3" diff --git a/Cargo.toml b/Cargo.toml index 4110639..da0f0e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ serde_json = "1.0.135" thiserror = "1.0.69" tokio = { version = "1.38.0", features = ["full"] } chrono = "0.4.39" +clap = { version = "4.5.3", features = ["derive"] } alloy = { version = "0.9.2", features = ["providers", "rpc-types-eth", "eip712", "signer-local"], optional = true } alloy-sol-types = { version = "0.8.14", optional = true } diff --git a/README.md b/README.md index acb8368..3cc3dc4 100644 --- a/README.md +++ b/README.md @@ -6,27 +6,34 @@ Tycho Execution makes it easy to trade on different DEXs by handling the complex custom code for each DEX, you get a simple, ready-to-use tool that generates the necessary data to execute trades. It's designed to be safe, straightforward, and quick to set up, so anyone can start trading without extra effort. -## Usage Guide +## Bin Usage Guide ### Encoding Transactions -To encode a transaction, you can pipe a JSON payload to the `tycho-encode` binary: +To encode a transaction, you can pipe a JSON payload to the binary: ```bash -echo '' | cargo run --bin tycho-encode +echo '' | cargo run ``` #### Example Here's a complete example that encodes a swap from WETH to DAI using Uniswap V2: +First build the project: ```bash -echo '{"sender":"0x1234567890123456789012345678901234567890","receiver":"0x1234567890123456789012345678901234567890","given_token":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","given_amount":"1000000000000000000","checked_token":"0x6B175474E89094C44Da98b954EedeAC495271d0F","exact_out":false,"slippage":0.01,"expected_amount":"1000000000000000000","check_amount":"990000000000000000","router_address":"0xaa820C29648D5EA543d712cC928377Bd7206a0E7","swaps":[{"component":{"id":"0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640","protocol_system":"uniswap_v2","protocol_type_name":"UniswapV2Pool","chain":"ethereum","tokens":["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"],"contract_ids":["0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"],"static_attributes":{"factory":"0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f"},"change":"Update","creation_tx":"0x0000000000000000000000000000000000000000000000000000000000000000","created_at":"2024-02-28T12:00:00"},"token_in":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","token_out":"0x6B175474E89094C44Da98b954EedeAC495271d0F","split":1.0}],"direct_execution":false}' | cargo run +cargo build --release ``` -#### JSON Payload Structure +After that, you can use the binary directly: +```bash +echo '{"sender":"0x1234567890123456789012345678901234567890","receiver":"0x1234567890123456789012345678901234567890","given_token":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","given_amount":"1000000000000000000","checked_token":"0x6B175474E89094C44Da98b954EedeAC495271d0F","exact_out":false,"slippage":0.01,"expected_amount":"1000000000000000000","check_amount":"990000000000000000","router_address":"0xaa820C29648D5EA543d712cC928377Bd7206a0E7","swaps":[{"component":{"id":"0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640","protocol_system":"uniswap_v2","protocol_type_name":"UniswapV2Pool","chain":"ethereum","tokens":["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"],"contract_ids":["0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"],"static_attributes":{"factory":"0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f"},"change":"Update","creation_tx":"0x0000000000000000000000000000000000000000000000000000000000000000","created_at":"2024-02-28T12:00:00"},"token_in":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","token_out":"0x6B175474E89094C44Da98b954EedeAC495271d0F","split":1.0}],"direct_execution":true}' | cargo run --release +``` -The input JSON payload requires the following fields: + +#### JSON Payload Structure: Solution struct + +The `Solution` struct is composed of the following fields: - `sender`: The address initiating the transaction - `receiver`: The address receiving the output tokens diff --git a/src/bin/lib/cli.rs b/src/bin/lib/cli.rs new file mode 100644 index 0000000..1194ca5 --- /dev/null +++ b/src/bin/lib/cli.rs @@ -0,0 +1,45 @@ +pub use clap::Parser; +pub const DEFAULT_ROUTER_ADDRESS: &str = "0xaa820C29648D5EA543d712cC928377Bd7206a0E7"; + +#[derive(Parser)] +/// Encode swap transactions for the Tycho router +/// +/// Reads a JSON object from stdin with the following structure: +/// ```json +/// { +/// "sender": "0x...", +/// "receiver": "0x...", +/// "given_token": "0x...", +/// "given_amount": "123...", +/// "checked_token": "0x...", +/// "exact_out": false, +/// "slippage": 0.01, +/// "expected_amount": "123...", +/// "check_amount": "123...", +/// "swaps": [{ +/// "component": { +/// "id": "...", +/// "protocol_system": "...", +/// "protocol_type_name": "...", +/// "chain": "ethereum", +/// "tokens": ["0x..."], +/// "contract_ids": ["0x..."], +/// "static_attributes": {"key": "0x..."} +/// }, +/// "token_in": "0x...", +/// "token_out": "0x...", +/// "split": 1.0 +/// }], +/// "router_address": "0x...", +/// "direct_execution": false +/// } +/// ``` +pub struct Cli { + /// Router contract address to use for encoding transactions + #[arg(default_value = DEFAULT_ROUTER_ADDRESS)] + pub router_address: String, + + /// Private key for signing approvals (required when direct_execution is false) + #[arg(short, long)] + pub private_key: Option, +} diff --git a/src/bin/lib/help.rs b/src/bin/lib/help.rs deleted file mode 100644 index a34bd99..0000000 --- a/src/bin/lib/help.rs +++ /dev/null @@ -1,37 +0,0 @@ -pub const HELP_TEXT: &str = "\ -USAGE: - tycho-encode [ROUTER_ADDRESS] [PRIVATE_KEY] - -ARGS: - ROUTER_ADDRESS The address of the router contract [default: 0x1234567890123456789012345678901234567890] - PRIVATE_KEY The private key for signing Permit2 approvals (required when direct_execution is false) - -The program reads a JSON object from stdin containing the swap details and outputs the encoded transaction. -The JSON object should have the following structure: -{ - \"sender\": \"0x...\", - \"receiver\": \"0x...\", - \"given_token\": \"0x...\", - \"given_amount\": \"123...\", - \"checked_token\": \"0x...\", - \"exact_out\": false, - \"slippage\": 0.01, - \"expected_amount\": \"123...\", - \"check_amount\": \"123...\", - \"swaps\": [{ - \"component\": { - \"id\": \"...\", - \"protocol_system\": \"...\", - \"protocol_type_name\": \"...\", - \"chain\": \"ethereum\", - \"tokens\": [\"0x...\"], - \"contract_ids\": [\"0x...\"], - \"static_attributes\": {\"key\": \"0x...\"} - }, - \"token_in\": \"0x...\", - \"token_out\": \"0x...\", - \"split\": 1.0 - }], - \"router_address\": \"0x...\", - \"direct_execution\": false -}"; diff --git a/src/bin/tycho-encode.rs b/src/bin/tycho-encode.rs index e038115..8bd518f 100644 --- a/src/bin/tycho-encode.rs +++ b/src/bin/tycho-encode.rs @@ -1,5 +1,6 @@ use std::io::{self, Read}; +use clap::Parser; use serde_json::Value; use tycho_core::dto::Chain; use tycho_execution::encoding::{ @@ -13,32 +14,15 @@ use tycho_execution::encoding::{ }; mod lib { - pub mod help; + pub mod cli; } -const DEFAULT_ROUTER_ADDRESS: &str = "0xaa820C29648D5EA543d712cC928377Bd7206a0E7"; +use lib::cli::Cli; + const DEFAULT_EXECUTORS_FILE_PATH: &str = "src/encoding/config/executor_addresses.json"; -const DEFAULT_PRIVATE_KEY: &str = - "0x938f4da9d3a947a4a6c53cfd8fcdd876641d6a4519243820b648af0bc3e67f7c"; fn main() -> Result<(), Box> { - let args: Vec = std::env::args().collect(); - - // Show help text if requested - if args.len() > 1 && (args[1] == "-h" || args[1] == "--help") { - println!("{}", lib::help::HELP_TEXT); - return Ok(()); - } - - let router_address = args - .get(1) - .map(|s| s.as_str()) - .unwrap_or(DEFAULT_ROUTER_ADDRESS); - - let private_key = args - .get(2) - .map(|s| s.to_string()) - .or_else(|| Some(DEFAULT_PRIVATE_KEY.to_string())); + let cli = Cli::parse(); // Read from stdin until EOF let mut buffer = String::new(); @@ -47,13 +31,11 @@ fn main() -> Result<(), Box> { .map_err(|e| format!("Failed to read from stdin: {}", e))?; if buffer.trim().is_empty() { - eprintln!("Error: No input provided"); - eprintln!("{}", lib::help::HELP_TEXT); - std::process::exit(1); + return Err("No input provided. Expected JSON input on stdin.".into()); } // Encode the solution - let encoded = encode_swaps(&buffer, router_address, private_key)?; + let encoded = encode_swaps(&buffer, &cli.router_address, cli.private_key)?; // Output the encoded result as JSON to stdout println!( diff --git a/src/encoding/models.rs b/src/encoding/models.rs index 44504b5..dcc7c50 100644 --- a/src/encoding/models.rs +++ b/src/encoding/models.rs @@ -5,7 +5,6 @@ use tycho_core::{dto::ProtocolComponent, Bytes}; use crate::encoding::serde_primitives::{biguint_string, biguint_string_option}; #[derive(Clone, Default, Debug, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] pub struct Solution { /// Address of the sender. pub sender: Bytes, From 32c3bd22202c250d9c7de4d971e39ef7169c02a6 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Thu, 6 Feb 2025 20:55:27 +0530 Subject: [PATCH 15/19] feat: update cli params and docs --- README.md | 35 +++++++++++++++++++++++++++-------- foundry/lib/v4-core | 1 + src/bin/lib/cli.rs | 7 ++++++- src/bin/tycho-encode.rs | 7 +++---- 4 files changed, 37 insertions(+), 13 deletions(-) create mode 160000 foundry/lib/v4-core diff --git a/README.md b/README.md index 3cc3dc4..f440786 100644 --- a/README.md +++ b/README.md @@ -8,26 +8,45 @@ designed to be safe, straightforward, and quick to set up, so anyone can start t ## Bin Usage Guide +### Installation + +First, build and install the binary: +```bash +# Build the project +cargo build --release + +# Install the binary to your system +cargo install --path . +``` + +After installation, the `tycho-encode` command will be available to use from any directory in your terminal. The command accepts the following options: + +- `-c`: Path to the executor addresses configuration file (defaults to `src/encoding/config/executor_addresses.json`) +- `-p`: Private key for signing approvals (required when direct_execution is false) +- `ROUTER_ADDRESS`: Router contract address (defaults to `0xaa820C29648D5EA543d712cC928377Bd7206a0E7`) + + ### Encoding Transactions To encode a transaction, you can pipe a JSON payload to the binary: ```bash -echo '' | cargo run +# Using default config path +echo '' | tycho-encode + +# Using custom config path +echo '' | tycho-encode -c /path/to/your/config.json + +# Using custom router address and config path +echo '' | tycho-encode -c /path/to/your/config.json 0x1234...5678 ``` #### Example Here's a complete example that encodes a swap from WETH to DAI using Uniswap V2: -First build the project: ```bash -cargo build --release -``` - -After that, you can use the binary directly: -```bash -echo '{"sender":"0x1234567890123456789012345678901234567890","receiver":"0x1234567890123456789012345678901234567890","given_token":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","given_amount":"1000000000000000000","checked_token":"0x6B175474E89094C44Da98b954EedeAC495271d0F","exact_out":false,"slippage":0.01,"expected_amount":"1000000000000000000","check_amount":"990000000000000000","router_address":"0xaa820C29648D5EA543d712cC928377Bd7206a0E7","swaps":[{"component":{"id":"0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640","protocol_system":"uniswap_v2","protocol_type_name":"UniswapV2Pool","chain":"ethereum","tokens":["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"],"contract_ids":["0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"],"static_attributes":{"factory":"0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f"},"change":"Update","creation_tx":"0x0000000000000000000000000000000000000000000000000000000000000000","created_at":"2024-02-28T12:00:00"},"token_in":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","token_out":"0x6B175474E89094C44Da98b954EedeAC495271d0F","split":1.0}],"direct_execution":true}' | cargo run --release +echo '{"sender":"0x1234567890123456789012345678901234567890","receiver":"0x1234567890123456789012345678901234567890","given_token":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","given_amount":"1000000000000000000","checked_token":"0x6B175474E89094C44Da98b954EedeAC495271d0F","exact_out":false,"slippage":0.01,"expected_amount":"1000000000000000000","check_amount":"990000000000000000","router_address":"0xaa820C29648D5EA543d712cC928377Bd7206a0E7","swaps":[{"component":{"id":"0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640","protocol_system":"uniswap_v2","protocol_type_name":"UniswapV2Pool","chain":"ethereum","tokens":["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"],"contract_ids":["0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"],"static_attributes":{"factory":"0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f"},"change":"Update","creation_tx":"0x0000000000000000000000000000000000000000000000000000000000000000","created_at":"2024-02-28T12:00:00"},"token_in":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","token_out":"0x6B175474E89094C44Da98b954EedeAC495271d0F","split":1.0}],"direct_execution":true}' | tycho-encode ``` diff --git a/foundry/lib/v4-core b/foundry/lib/v4-core new file mode 160000 index 0000000..e50237c --- /dev/null +++ b/foundry/lib/v4-core @@ -0,0 +1 @@ +Subproject commit e50237c43811bd9b526eff40f26772152a42daba diff --git a/src/bin/lib/cli.rs b/src/bin/lib/cli.rs index 1194ca5..25e8786 100644 --- a/src/bin/lib/cli.rs +++ b/src/bin/lib/cli.rs @@ -1,5 +1,6 @@ pub use clap::Parser; pub const DEFAULT_ROUTER_ADDRESS: &str = "0xaa820C29648D5EA543d712cC928377Bd7206a0E7"; +pub const DEFAULT_CONFIG_PATH: &str = "src/encoding/config/executor_addresses.json"; #[derive(Parser)] /// Encode swap transactions for the Tycho router @@ -40,6 +41,10 @@ pub struct Cli { pub router_address: String, /// Private key for signing approvals (required when direct_execution is false) - #[arg(short, long)] + #[arg(short)] pub private_key: Option, + + /// Path to the executor addresses configuration file + #[arg(short, default_value = DEFAULT_CONFIG_PATH)] + pub config_path: String, } diff --git a/src/bin/tycho-encode.rs b/src/bin/tycho-encode.rs index 8bd518f..9e97eca 100644 --- a/src/bin/tycho-encode.rs +++ b/src/bin/tycho-encode.rs @@ -19,8 +19,6 @@ mod lib { use lib::cli::Cli; -const DEFAULT_EXECUTORS_FILE_PATH: &str = "src/encoding/config/executor_addresses.json"; - fn main() -> Result<(), Box> { let cli = Cli::parse(); @@ -35,7 +33,7 @@ fn main() -> Result<(), Box> { } // Encode the solution - let encoded = encode_swaps(&buffer, &cli.router_address, cli.private_key)?; + let encoded = encode_swaps(&buffer, &cli.router_address, &cli.config_path, cli.private_key)?; // Output the encoded result as JSON to stdout println!( @@ -50,12 +48,13 @@ fn main() -> Result<(), Box> { fn encode_swaps( input: &str, router_address: &str, + config_path: &str, private_key: Option, ) -> Result> { let solution: Solution = serde_json::from_str(input)?; let strategy_selector = - EVMStrategyEncoderRegistry::new(Chain::Ethereum, DEFAULT_EXECUTORS_FILE_PATH, private_key)?; + EVMStrategyEncoderRegistry::new(Chain::Ethereum, config_path, private_key)?; let encoder = EVMTychoEncoder::new(strategy_selector, router_address.to_string())?; let transactions = encoder.encode_router_calldata(vec![solution])?; From 1dad36d7a84120aecfcb0867c9733c12efb72fe6 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Thu, 6 Feb 2025 20:57:43 +0530 Subject: [PATCH 16/19] fix: rm v4-core --- foundry/lib/v4-core | 1 - 1 file changed, 1 deletion(-) delete mode 160000 foundry/lib/v4-core diff --git a/foundry/lib/v4-core b/foundry/lib/v4-core deleted file mode 160000 index e50237c..0000000 --- a/foundry/lib/v4-core +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e50237c43811bd9b526eff40f26772152a42daba From 520bee5a5d8d383cdca99c96d41022183827ffb0 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Thu, 6 Feb 2025 22:28:02 +0530 Subject: [PATCH 17/19] fix: chain.into() --- src/bin/tycho-encode.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/bin/tycho-encode.rs b/src/bin/tycho-encode.rs index 9e97eca..62bb74d 100644 --- a/src/bin/tycho-encode.rs +++ b/src/bin/tycho-encode.rs @@ -52,10 +52,12 @@ fn encode_swaps( private_key: Option, ) -> Result> { let solution: Solution = serde_json::from_str(input)?; + let chain = Chain::Ethereum; let strategy_selector = - EVMStrategyEncoderRegistry::new(Chain::Ethereum, config_path, private_key)?; - let encoder = EVMTychoEncoder::new(strategy_selector, router_address.to_string())?; + EVMStrategyEncoderRegistry::new(chain.into(), config_path, private_key)?; + let encoder = + EVMTychoEncoder::new(strategy_selector, router_address.to_string(), chain.into())?; let transactions = encoder.encode_router_calldata(vec![solution])?; Ok(serde_json::json!({ From 7df1995655e55f1ad62a32c265ba1fd7174562db Mon Sep 17 00:00:00 2001 From: royvardhan Date: Thu, 6 Feb 2025 22:30:23 +0530 Subject: [PATCH 18/19] fix: fmt --- src/encoding/models.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/encoding/models.rs b/src/encoding/models.rs index 359fd4e..c6b3432 100644 --- a/src/encoding/models.rs +++ b/src/encoding/models.rs @@ -6,10 +6,10 @@ use tycho_core::{ Bytes, }; -use crate::encoding::errors::EncodingError; - - -use crate::encoding::serde_primitives::{biguint_string, biguint_string_option}; +use crate::encoding::{ + errors::EncodingError, + serde_primitives::{biguint_string, biguint_string_option}, +}; #[derive(Clone, Default, Debug, Deserialize, Serialize)] pub struct Solution { From 3bb5b0c7c69179c8b087bc42bbd722bda5cc89a7 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Thu, 6 Feb 2025 22:44:18 +0530 Subject: [PATCH 19/19] fix: fmt --- src/encoding/models.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/encoding/models.rs b/src/encoding/models.rs index 0abe8e1..6a0e937 100644 --- a/src/encoding/models.rs +++ b/src/encoding/models.rs @@ -11,7 +11,6 @@ use crate::encoding::{ serde_primitives::{biguint_string, biguint_string_option}, }; - /// Represents a solution containing details describing an order, and instructions for filling /// the order. #[derive(Clone, Default, Debug, Deserialize, Serialize)] @@ -53,7 +52,6 @@ pub struct Solution { pub direct_execution: bool, } - /// Represents an action to be performed on the native token either before or after the swap. /// /// `Wrap` means that the native token will be wrapped before the first swap, and `Unwrap` @@ -66,8 +64,6 @@ pub enum NativeAction { Unwrap, } - - /// Represents a swap operation to be performed on a pool. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Swap {