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())?;