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