From fd4045e6fe9c2379df2c278282db1e26e4b83c20 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 5 Feb 2025 22:57:38 +0530 Subject: [PATCH] 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), + } + } +}