odos_sdk/
swap_builder.rs

1use alloy_primitives::{Address, U256};
2use alloy_rpc_types::TransactionRequest;
3
4use crate::{
5    AssemblyRequest, Chain, OdosChain, OdosClient, QuoteRequest, ReferralCode, Result,
6    SingleQuoteResponse, Slippage,
7};
8
9/// High-level swap builder for common use cases
10///
11/// Provides an ergonomic API for building swaps without needing to understand
12/// the low-level quote → assemble → build flow.
13///
14/// # Examples
15///
16/// ## Simple swap
17///
18/// ```rust,no_run
19/// use odos_sdk::{OdosClient, Slippage, Chain};
20/// use alloy_primitives::{address, U256};
21///
22/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
23/// let client = OdosClient::new()?;
24///
25/// let tx = client
26///     .swap()
27///     .chain(Chain::ethereum())
28///     .from_token(address!("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), U256::from(1_000_000))
29///     .to_token(address!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"))
30///     .slippage(Slippage::percent(0.5)?)
31///     .signer(address!("742d35Cc6634C0532925a3b8D35f3e7a5edD29c0"))
32///     .build_transaction()
33///     .await?;
34/// # Ok(())
35/// # }
36/// ```
37///
38/// ## With custom recipient
39///
40/// ```rust,no_run
41/// use odos_sdk::{OdosClient, Slippage, Chain};
42/// use alloy_primitives::{address, U256};
43///
44/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
45/// let client = OdosClient::new()?;
46///
47/// let tx = client
48///     .swap()
49///     .chain(Chain::arbitrum())
50///     .from_token(address!("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), U256::from(1_000_000))
51///     .to_token(address!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"))
52///     .slippage(Slippage::bps(50)?)
53///     .signer(address!("742d35Cc6634C0532925a3b8D35f3e7a5edD29c0"))
54///     .recipient(address!("0000000000000000000000000000000000000001"))
55///     .build_transaction()
56///     .await?;
57/// # Ok(())
58/// # }
59/// ```
60#[derive(Debug)]
61pub struct SwapBuilder<'a> {
62    client: &'a OdosClient,
63    chain: Option<Chain>,
64    input_token: Option<Address>,
65    input_amount: Option<U256>,
66    output_token: Option<Address>,
67    slippage: Option<Slippage>,
68    signer: Option<Address>,
69    recipient: Option<Address>,
70    referral: ReferralCode,
71    compact: bool,
72    simple: bool,
73    disable_rfqs: bool,
74}
75
76impl<'a> SwapBuilder<'a> {
77    /// Create a new swap builder
78    pub(crate) fn new(client: &'a OdosClient) -> Self {
79        Self {
80            client,
81            chain: None,
82            input_token: None,
83            input_amount: None,
84            output_token: None,
85            slippage: None,
86            signer: None,
87            recipient: None,
88            referral: ReferralCode::NONE,
89            compact: false,
90            simple: false,
91            disable_rfqs: false,
92        }
93    }
94
95    /// Set the blockchain to execute the swap on
96    ///
97    /// # Examples
98    ///
99    /// ```rust,no_run
100    /// use odos_sdk::{OdosClient, Chain};
101    ///
102    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
103    /// let client = OdosClient::new()?;
104    /// let builder = client.swap().chain(Chain::ethereum());
105    /// # Ok(())
106    /// # }
107    /// ```
108    pub fn chain(mut self, chain: Chain) -> Self {
109        self.chain = Some(chain);
110        self
111    }
112
113    /// Set the input token and amount
114    ///
115    /// # Arguments
116    ///
117    /// * `token` - Address of the token to swap from
118    /// * `amount` - Amount of input token (in token's base units)
119    ///
120    /// # Examples
121    ///
122    /// ```rust,no_run
123    /// use odos_sdk::OdosClient;
124    /// use alloy_primitives::{address, U256};
125    ///
126    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
127    /// let client = OdosClient::new()?;
128    /// let builder = client.swap()
129    ///     .input(address!("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), U256::from(1_000_000));
130    /// # Ok(())
131    /// # }
132    /// ```
133    pub fn input(mut self, token: Address, amount: U256) -> Self {
134        self.input_token = Some(token);
135        self.input_amount = Some(amount);
136        self
137    }
138
139    /// Alias for `input()` - set the token to swap from
140    ///
141    /// # Examples
142    ///
143    /// ```rust,no_run
144    /// use odos_sdk::OdosClient;
145    /// use alloy_primitives::{address, U256};
146    ///
147    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
148    /// let client = OdosClient::new()?;
149    /// let builder = client.swap()
150    ///     .from_token(address!("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), U256::from(1_000_000));
151    /// # Ok(())
152    /// # }
153    /// ```
154    pub fn from_token(self, token: Address, amount: U256) -> Self {
155        self.input(token, amount)
156    }
157
158    /// Set the output token (100% of output goes to this token)
159    ///
160    /// # Arguments
161    ///
162    /// * `token` - Address of the token to swap to
163    ///
164    /// # Examples
165    ///
166    /// ```rust,no_run
167    /// use odos_sdk::OdosClient;
168    /// use alloy_primitives::address;
169    ///
170    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
171    /// let client = OdosClient::new()?;
172    /// let builder = client.swap()
173    ///     .output(address!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"));
174    /// # Ok(())
175    /// # }
176    /// ```
177    pub fn output(mut self, token: Address) -> Self {
178        self.output_token = Some(token);
179        self
180    }
181
182    /// Alias for `output()` - set the token to swap to
183    ///
184    /// # Examples
185    ///
186    /// ```rust,no_run
187    /// use odos_sdk::OdosClient;
188    /// use alloy_primitives::address;
189    ///
190    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
191    /// let client = OdosClient::new()?;
192    /// let builder = client.swap()
193    ///     .to_token(address!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"));
194    /// # Ok(())
195    /// # }
196    /// ```
197    pub fn to_token(self, token: Address) -> Self {
198        self.output(token)
199    }
200
201    /// Set the slippage tolerance
202    ///
203    /// # Arguments
204    ///
205    /// * `slippage` - Maximum acceptable slippage
206    ///
207    /// # Examples
208    ///
209    /// ```rust,no_run
210    /// use odos_sdk::{OdosClient, Slippage};
211    ///
212    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
213    /// let client = OdosClient::new()?;
214    /// let builder = client.swap()
215    ///     .slippage(Slippage::percent(0.5)?);  // 0.5% slippage
216    /// # Ok(())
217    /// # }
218    /// ```
219    pub fn slippage(mut self, slippage: Slippage) -> Self {
220        self.slippage = Some(slippage);
221        self
222    }
223
224    /// Set the address that will sign and send the transaction
225    ///
226    /// # Arguments
227    ///
228    /// * `address` - The signer's address
229    ///
230    /// # Examples
231    ///
232    /// ```rust,no_run
233    /// use odos_sdk::OdosClient;
234    /// use alloy_primitives::address;
235    ///
236    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
237    /// let client = OdosClient::new()?;
238    /// let builder = client.swap()
239    ///     .signer(address!("742d35Cc6634C0532925a3b8D35f3e7a5edD29c0"));
240    /// # Ok(())
241    /// # }
242    /// ```
243    pub fn signer(mut self, address: Address) -> Self {
244        self.signer = Some(address);
245        self
246    }
247
248    /// Set the recipient address for output tokens
249    ///
250    /// If not set, defaults to the signer address.
251    ///
252    /// # Arguments
253    ///
254    /// * `address` - The recipient's address
255    ///
256    /// # Examples
257    ///
258    /// ```rust,no_run
259    /// use odos_sdk::OdosClient;
260    /// use alloy_primitives::address;
261    ///
262    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
263    /// let client = OdosClient::new()?;
264    /// let builder = client.swap()
265    ///     .signer(address!("742d35Cc6634C0532925a3b8D35f3e7a5edD29c0"))
266    ///     .recipient(address!("0000000000000000000000000000000000000001"));
267    /// # Ok(())
268    /// # }
269    /// ```
270    pub fn recipient(mut self, address: Address) -> Self {
271        self.recipient = Some(address);
272        self
273    }
274
275    /// Set the referral code
276    ///
277    /// # Arguments
278    ///
279    /// * `code` - Referral code for tracking
280    ///
281    /// # Examples
282    ///
283    /// ```rust,no_run
284    /// use odos_sdk::{OdosClient, ReferralCode};
285    ///
286    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
287    /// let client = OdosClient::new()?;
288    /// let builder = client.swap()
289    ///     .referral(ReferralCode::new(42));
290    /// # Ok(())
291    /// # }
292    /// ```
293    pub fn referral(mut self, code: ReferralCode) -> Self {
294        self.referral = code;
295        self
296    }
297
298    /// Enable compact mode
299    ///
300    /// # Examples
301    ///
302    /// ```rust,no_run
303    /// use odos_sdk::OdosClient;
304    ///
305    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
306    /// let client = OdosClient::new()?;
307    /// let builder = client.swap().compact(true);
308    /// # Ok(())
309    /// # }
310    /// ```
311    pub fn compact(mut self, compact: bool) -> Self {
312        self.compact = compact;
313        self
314    }
315
316    /// Enable simple mode
317    ///
318    /// # Examples
319    ///
320    /// ```rust,no_run
321    /// use odos_sdk::OdosClient;
322    ///
323    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
324    /// let client = OdosClient::new()?;
325    /// let builder = client.swap().simple(true);
326    /// # Ok(())
327    /// # }
328    /// ```
329    pub fn simple(mut self, simple: bool) -> Self {
330        self.simple = simple;
331        self
332    }
333
334    /// Disable RFQs (Request for Quotes)
335    ///
336    /// # Examples
337    ///
338    /// ```rust,no_run
339    /// use odos_sdk::OdosClient;
340    ///
341    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
342    /// let client = OdosClient::new()?;
343    /// let builder = client.swap().disable_rfqs(true);
344    /// # Ok(())
345    /// # }
346    /// ```
347    pub fn disable_rfqs(mut self, disable: bool) -> Self {
348        self.disable_rfqs = disable;
349        self
350    }
351
352    /// Get a quote for this swap without building the transaction
353    ///
354    /// This is useful if you want to inspect the quote before proceeding.
355    ///
356    /// # Returns
357    ///
358    /// Returns a `SingleQuoteResponse` with routing information, price impact, and gas estimates.
359    ///
360    /// # Errors
361    ///
362    /// Returns an error if:
363    /// - Required fields are missing
364    /// - The Odos API returns an error
365    /// - Network issues occur
366    ///
367    /// # Examples
368    ///
369    /// ```rust,no_run
370    /// use odos_sdk::{OdosClient, Chain, Slippage};
371    /// use alloy_primitives::{address, U256};
372    ///
373    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
374    /// let client = OdosClient::new()?;
375    ///
376    /// let quote = client
377    ///     .swap()
378    ///     .chain(Chain::ethereum())
379    ///     .from_token(address!("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), U256::from(1_000_000))
380    ///     .to_token(address!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"))
381    ///     .slippage(Slippage::percent(0.5)?)
382    ///     .signer(address!("742d35Cc6634C0532925a3b8D35f3e7a5edD29c0"))
383    ///     .quote()
384    ///     .await?;
385    ///
386    /// println!("Expected output: {} tokens", quote.out_amount().unwrap());
387    /// println!("Price impact: {}%", quote.price_impact());
388    /// # Ok(())
389    /// # }
390    /// ```
391    pub async fn quote(&self) -> Result<SingleQuoteResponse> {
392        let chain = self
393            .chain
394            .ok_or_else(|| crate::OdosError::missing_data("Chain is required for swap builder"))?;
395
396        let input_token = self.input_token.ok_or_else(|| {
397            crate::OdosError::missing_data("Input token is required for swap builder")
398        })?;
399
400        let input_amount = self.input_amount.ok_or_else(|| {
401            crate::OdosError::missing_data("Input amount is required for swap builder")
402        })?;
403
404        let output_token = self.output_token.ok_or_else(|| {
405            crate::OdosError::missing_data("Output token is required for swap builder")
406        })?;
407
408        let slippage = self.slippage.ok_or_else(|| {
409            crate::OdosError::missing_data("Slippage is required for swap builder")
410        })?;
411
412        let signer = self.signer.ok_or_else(|| {
413            crate::OdosError::missing_data("Signer address is required for swap builder")
414        })?;
415
416        let quote_request = QuoteRequest::builder()
417            .chain_id(chain.id())
418            .input_tokens(vec![(input_token, input_amount).into()])
419            .output_tokens(vec![(output_token, 1).into()])
420            .slippage_limit_percent(slippage.as_percent())
421            .user_addr(signer)
422            .compact(self.compact)
423            .simple(self.simple)
424            .referral_code(self.referral.code())
425            .disable_rfqs(self.disable_rfqs)
426            .build();
427
428        self.client.quote(&quote_request).await
429    }
430
431    /// Build the complete transaction for this swap
432    ///
433    /// This method:
434    /// 1. Gets a quote from the Odos API
435    /// 2. Assembles the transaction data
436    /// 3. Returns a `TransactionRequest` ready for signing
437    ///
438    /// The returned transaction still needs gas parameters set before signing.
439    ///
440    /// # Returns
441    ///
442    /// Returns a `TransactionRequest` with:
443    /// - `to`: Router contract address
444    /// - `from`: Signer address
445    /// - `data`: Encoded swap calldata
446    /// - `value`: ETH amount to send (if swapping from native token)
447    ///
448    /// # Errors
449    ///
450    /// Returns an error if:
451    /// - Required fields are missing
452    /// - The Odos API returns an error
453    /// - Transaction assembly fails
454    /// - Network issues occur
455    ///
456    /// # Examples
457    ///
458    /// ```rust,no_run
459    /// use odos_sdk::{OdosClient, Chain, Slippage};
460    /// use alloy_primitives::{address, U256};
461    ///
462    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
463    /// let client = OdosClient::new()?;
464    ///
465    /// let tx = client
466    ///     .swap()
467    ///     .chain(Chain::ethereum())
468    ///     .from_token(address!("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), U256::from(1_000_000))
469    ///     .to_token(address!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"))
470    ///     .slippage(Slippage::percent(0.5)?)
471    ///     .signer(address!("742d35Cc6634C0532925a3b8D35f3e7a5edD29c0"))
472    ///     .build_transaction()
473    ///     .await?;
474    ///
475    /// // Set gas parameters and sign
476    /// // let tx = tx.with_gas_limit(300_000);
477    /// // provider.send_transaction(tx).await?;
478    /// # Ok(())
479    /// # }
480    /// ```
481    pub async fn build_transaction(&self) -> Result<TransactionRequest> {
482        // Get quote
483        let quote = self.quote().await?;
484
485        let chain = self.chain.unwrap(); // Safe: validated in quote()
486        let signer = self.signer.unwrap(); // Safe: validated in quote()
487        let recipient = self.recipient.unwrap_or(signer);
488        let input_token = self.input_token.unwrap(); // Safe: validated in quote()
489        let input_amount = self.input_amount.unwrap(); // Safe: validated in quote()
490
491        // Get router address for this chain
492        let router_address = chain.v3_router_address()?;
493
494        // Build swap context
495        let swap_context = AssemblyRequest::builder()
496            .chain(chain.inner())
497            .router_address(router_address)
498            .signer_address(signer)
499            .output_recipient(recipient)
500            .token_address(input_token)
501            .token_amount(input_amount)
502            .path_id(quote.path_id().to_string())
503            .build();
504
505        // Build transaction
506        self.client.assemble(&swap_context).await
507    }
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513    use alloy_primitives::address;
514
515    #[test]
516    fn test_builder_construction() {
517        let client = OdosClient::new().unwrap();
518        let builder = client.swap();
519
520        assert!(builder.chain.is_none());
521        assert!(builder.input_token.is_none());
522        assert!(builder.output_token.is_none());
523        assert_eq!(builder.referral, ReferralCode::NONE);
524    }
525
526    #[test]
527    fn test_builder_chain_methods() {
528        let client = OdosClient::new().unwrap();
529
530        let builder = client
531            .swap()
532            .chain(Chain::ethereum())
533            .from_token(
534                address!("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"),
535                U256::from(1_000_000),
536            )
537            .to_token(address!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"))
538            .slippage(Slippage::standard())
539            .signer(address!("742d35Cc6634C0532925a3b8D35f3e7a5edD29c0"));
540
541        assert_eq!(builder.chain.unwrap(), Chain::ethereum());
542        assert_eq!(
543            builder.input_token.unwrap(),
544            address!("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")
545        );
546        assert_eq!(builder.input_amount.unwrap(), U256::from(1_000_000));
547        assert_eq!(
548            builder.output_token.unwrap(),
549            address!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2")
550        );
551        assert_eq!(builder.slippage.unwrap(), Slippage::standard());
552        assert_eq!(
553            builder.signer.unwrap(),
554            address!("742d35Cc6634C0532925a3b8D35f3e7a5edD29c0")
555        );
556    }
557
558    #[test]
559    fn test_builder_aliases() {
560        let client = OdosClient::new().unwrap();
561
562        // Test input() vs from_token()
563        let builder1 = client.swap().input(
564            address!("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"),
565            U256::from(1000),
566        );
567        let builder2 = client.swap().from_token(
568            address!("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"),
569            U256::from(1000),
570        );
571
572        assert_eq!(builder1.input_token, builder2.input_token);
573        assert_eq!(builder1.input_amount, builder2.input_amount);
574
575        // Test output() vs to_token()
576        let builder1 = client
577            .swap()
578            .output(address!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"));
579        let builder2 = client
580            .swap()
581            .to_token(address!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"));
582
583        assert_eq!(builder1.output_token, builder2.output_token);
584    }
585
586    #[test]
587    fn test_builder_recipient_defaults_to_signer() {
588        let client = OdosClient::new().unwrap();
589        let signer_addr = address!("742d35Cc6634C0532925a3b8D35f3e7a5edD29c0");
590
591        let builder = client.swap().signer(signer_addr);
592
593        assert_eq!(builder.signer.unwrap(), signer_addr);
594        assert!(builder.recipient.is_none()); // Not set, will default in build
595    }
596}