odos_sdk/
assemble.rs

1use std::fmt::Display;
2
3use alloy_network::TransactionBuilder;
4use alloy_primitives::{hex, Address, U256};
5use alloy_rpc_types::TransactionRequest;
6use serde::{Deserialize, Serialize};
7
8/// The URL for the Odos Assemble API
9pub const ASSEMBLE_URL: &str = "https://api.odos.xyz/sor/assemble";
10
11/// Request to the Odos Assemble API: <https://docs.odos.xyz/build/api-docs>
12#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
13#[serde(rename_all = "camelCase")]
14pub struct AssembleRequest {
15    pub user_addr: String,
16    pub path_id: String,
17    pub simulate: bool,
18    pub receiver: Option<Address>,
19}
20
21impl Display for AssembleRequest {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        write!(
24            f,
25            "AssembleRequest {{ user_addr: {}, path_id: {}, simulate: {}, receiver: {} }}",
26            self.user_addr,
27            self.path_id,
28            self.simulate,
29            self.receiver
30                .as_ref()
31                .map_or("None".to_string(), |s| s.to_string())
32        )
33    }
34}
35
36/// Response from the Odos Assemble API: <https://docs.odos.xyz/build/api-docs>
37#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
38#[serde(rename_all = "camelCase")]
39pub struct AssemblyResponse {
40    pub transaction: TransactionData,
41    pub simulation: Option<Simulation>,
42}
43
44impl Display for AssemblyResponse {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        write!(
47            f,
48            "AssemblyResponse {{ transaction: {}, simulation: {} }}",
49            self.transaction,
50            self.simulation
51                .as_ref()
52                .map_or("None".to_string(), |s| s.to_string())
53        )
54    }
55}
56
57/// Transaction data from the Odos Assemble API: <https://docs.odos.xyz/build/api-docs>
58#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
59#[serde(rename_all = "camelCase")]
60pub struct TransactionData {
61    pub to: Address,
62    pub from: Address,
63    pub data: String,
64    pub value: String,
65    pub gas: i128,
66    pub gas_price: u128,
67    pub chain_id: u64,
68    pub nonce: u64,
69}
70
71/// Convert [`TransactionData`] to a [`TransactionRequest`].
72impl TryFrom<TransactionData> for TransactionRequest {
73    type Error = crate::OdosError;
74
75    fn try_from(data: TransactionData) -> Result<Self, Self::Error> {
76        let input = hex::decode(&data.data)?;
77        let value = parse_value(&data.value)?;
78
79        Ok(TransactionRequest::default()
80            .with_input(input)
81            .with_value(value))
82    }
83}
84
85impl Display for TransactionData {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        write!(
88            f,
89            "TransactionData {{ to: {}, from: {}, data: {}, value: {}, gas: {}, gas_price: {}, chain_id: {}, nonce: {} }}",
90            self.to,
91            self.from,
92            self.data,
93            self.value,
94            self.gas,
95            self.gas_price,
96            self.chain_id,
97            self.nonce
98        )
99    }
100}
101
102/// Simulation from the Odos Assemble API: <https://docs.odos.xyz/build/api-docs>
103#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
104#[serde(rename_all = "camelCase")]
105pub struct Simulation {
106    is_success: bool,
107    amounts_out: Vec<String>,
108    gas_estimate: i64,
109    simulation_error: SimulationError,
110}
111
112impl Simulation {
113    pub fn is_success(&self) -> bool {
114        self.is_success
115    }
116
117    pub fn error_message(&self) -> &str {
118        &self.simulation_error.error_message
119    }
120}
121
122impl Display for Simulation {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        write!(
125            f,
126            "Simulation {{ is_success: {}, amounts_out: {:?}, gas_estimate: {}, simulation_error: {} }}",
127            self.is_success,
128            self.amounts_out,
129            self.gas_estimate,
130            self.simulation_error.error_message
131        )
132    }
133}
134
135/// Simulation error from the Odos Assemble API: <https://docs.odos.xyz/build/api-docs>
136#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
137#[serde(rename_all = "camelCase")]
138pub struct SimulationError {
139    r#type: String,
140    error_message: String,
141}
142
143impl SimulationError {
144    pub fn error_message(&self) -> &str {
145        &self.error_message
146    }
147}
148
149impl Display for SimulationError {
150    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151        write!(f, "Simulation error: {}", self.error_message)
152    }
153}
154
155/// Parse a value string as U256, supporting both decimal and hexadecimal formats
156///
157/// This function attempts to parse the value as decimal first, then as hexadecimal
158/// (with optional "0x" prefix) if decimal parsing fails.
159///
160/// # Arguments
161///
162/// * `value` - The string value to parse
163///
164/// # Returns
165///
166/// * `Ok(U256)` - The parsed value
167/// * `Err(OdosError)` - If the value cannot be parsed in either format
168///
169/// # Examples
170///
171/// ```rust
172/// # use odos_sdk::parse_value;
173/// # use alloy_primitives::U256;
174/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
175/// // Decimal format
176/// let val = parse_value("1000")?;
177/// assert_eq!(val, U256::from(1000));
178///
179/// // Hexadecimal with 0x prefix
180/// let val = parse_value("0xff")?;
181/// assert_eq!(val, U256::from(255));
182///
183/// // Hexadecimal without prefix
184/// let val = parse_value("ff")?;
185/// assert_eq!(val, U256::from(255));
186/// # Ok(())
187/// # }
188/// ```
189pub fn parse_value(value: &str) -> crate::Result<U256> {
190    use crate::OdosError;
191
192    if value == "0" {
193        return Ok(U256::ZERO);
194    }
195
196    // Try parsing as decimal first
197    U256::from_str_radix(value, 10).or_else(|decimal_err| {
198        // If decimal fails, try hexadecimal (with optional "0x" prefix)
199        let hex_value = value.strip_prefix("0x").unwrap_or(value);
200        U256::from_str_radix(hex_value, 16).map_err(|hex_err| {
201            OdosError::invalid_input(format!(
202                "Failed to parse value '{}' as decimal ({}) or hexadecimal ({})",
203                value, decimal_err, hex_err
204            ))
205        })
206    })
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn test_parse_value_zero() {
215        let result = parse_value("0").unwrap();
216        assert_eq!(result, U256::ZERO);
217    }
218
219    #[test]
220    fn test_parse_value_decimal() {
221        // Small values
222        assert_eq!(parse_value("1").unwrap(), U256::from(1));
223        assert_eq!(parse_value("123").unwrap(), U256::from(123));
224        assert_eq!(parse_value("1000").unwrap(), U256::from(1000));
225
226        // Large values
227        assert_eq!(
228            parse_value("1000000000000000000").unwrap(),
229            U256::from(1000000000000000000u64)
230        );
231
232        // Very large values (beyond u64)
233        let large_decimal = "123456789012345678901234567890";
234        let result = parse_value(large_decimal);
235        assert!(result.is_ok(), "Should parse large decimal values");
236    }
237
238    #[test]
239    fn test_parse_value_hex_with_prefix() {
240        // With 0x prefix
241        assert_eq!(parse_value("0x0").unwrap(), U256::ZERO);
242        assert_eq!(parse_value("0xff").unwrap(), U256::from(255));
243        assert_eq!(parse_value("0xFF").unwrap(), U256::from(255));
244        assert_eq!(parse_value("0x1234").unwrap(), U256::from(0x1234));
245        assert_eq!(parse_value("0xabcdef").unwrap(), U256::from(0xabcdef));
246    }
247
248    #[test]
249    fn test_parse_value_hex_without_prefix() {
250        // Pure hex letters (no decimal interpretation) - falls back to hex parsing
251        assert_eq!(parse_value("ff").unwrap(), U256::from(255));
252        assert_eq!(parse_value("FF").unwrap(), U256::from(255));
253        assert_eq!(parse_value("abcdef").unwrap(), U256::from(0xabcdef));
254        assert_eq!(parse_value("ABCDEF").unwrap(), U256::from(0xabcdef));
255
256        // Ambiguous: "1234" can be decimal or hex
257        // Decimal parsing takes precedence, so this is 1234 not 0x1234
258        assert_eq!(parse_value("1234").unwrap(), U256::from(1234));
259        assert_ne!(parse_value("1234").unwrap(), U256::from(0x1234));
260    }
261
262    #[test]
263    fn test_parse_value_invalid() {
264        // Invalid characters (not valid decimal or hex)
265        let result = parse_value("xyz");
266        assert!(result.is_err(), "Invalid characters should fail");
267
268        // Mixed invalid
269        let result = parse_value("0xGHI");
270        assert!(result.is_err(), "Invalid hex characters should fail");
271
272        // Special characters
273        let result = parse_value("12@34");
274        assert!(result.is_err(), "Special characters should fail");
275
276        // Note: Empty string "" actually succeeds with from_str_radix(10) -> returns 0
277        // This is standard Rust behavior, so we accept it
278        let result = parse_value("");
279        assert_eq!(
280            result.unwrap(),
281            U256::ZERO,
282            "Empty string parses to zero (standard Rust behavior)"
283        );
284    }
285
286    #[test]
287    fn test_parse_value_edge_cases() {
288        // Leading zeros
289        assert_eq!(parse_value("00123").unwrap(), U256::from(123));
290        assert_eq!(parse_value("0x00ff").unwrap(), U256::from(255));
291
292        // Max u64
293        let max_u64_str = u64::MAX.to_string();
294        let result = parse_value(&max_u64_str).unwrap();
295        assert_eq!(result, U256::from(u64::MAX));
296
297        // Max u128
298        let max_u128_str = u128::MAX.to_string();
299        let result = parse_value(&max_u128_str);
300        assert!(result.is_ok(), "Should handle u128::MAX");
301    }
302
303    #[test]
304    fn test_parse_value_realistic_transaction_values() {
305        // 1 ETH in wei
306        let one_eth = "1000000000000000000";
307        assert_eq!(
308            parse_value(one_eth).unwrap(),
309            U256::from(1000000000000000000u64)
310        );
311
312        // 100 ETH in wei (typical transaction)
313        let hundred_eth = "100000000000000000000";
314        let result = parse_value(hundred_eth);
315        assert!(result.is_ok(), "Should parse 100 ETH");
316
317        // Gas price in hex (common format)
318        let gas_price_hex = "0x2540be400"; // 10 gwei
319        let result = parse_value(gas_price_hex);
320        assert!(result.is_ok(), "Should parse hex gas price");
321    }
322
323    #[test]
324    fn test_parse_value_error_messages() {
325        // Verify error messages contain useful info
326        let result = parse_value("invalid");
327        match result {
328            Err(e) => {
329                let error_msg = e.to_string();
330                assert!(
331                    error_msg.contains("invalid"),
332                    "Error should mention the invalid value"
333                );
334                assert!(
335                    error_msg.contains("decimal") || error_msg.contains("hexadecimal"),
336                    "Error should mention attempted parsing formats"
337                );
338            }
339            Ok(_) => panic!("Should have failed to parse 'invalid'"),
340        }
341    }
342}