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