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("e_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}