1use std::sync::Arc;
2
3use alloy::{
4 primitives::{aliases::U48, keccak256, Address, Keccak256, U160, U256},
5 sol_types::SolValue,
6};
7use num_bigint::BigUint;
8use tycho_execution::encoding::{
9 errors::EncodingError,
10 evm::{
11 approvals::permit2::{PermitDetails as SolPermitDetails, PermitSingle},
12 encoder_builders::TychoRouterEncoderBuilder,
13 get_router_address,
14 swap_encoder::swap_encoder_registry::SwapEncoderRegistry,
15 utils::{biguint_to_u256, bytes_to_address},
16 ROUTER_ETH_ADDRESS,
17 },
18 models::{EncodedSolution, Solution, Swap},
19 tycho_encoder::TychoEncoder,
20};
21use tycho_simulation::tycho_common::{models::Chain, Bytes};
22
23use crate::{
24 encoding::router_fees::{FeeRates, RouterFees, SharedRouterFees},
25 EncodingOptions, FeeBreakdown, OrderQuote, QuoteStatus, SolveError, Transaction,
26};
27
28pub const PERMIT2_ADDRESS: &str = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
30
31pub struct Encoder {
40 tycho_encoder: Box<dyn TychoEncoder>,
41 chain: Chain,
42 router_address: Bytes,
43 router_fees: SharedRouterFees,
44}
45
46impl TryFrom<&OrderQuote> for Solution {
47 type Error = SolveError;
48
49 fn try_from(quote: &OrderQuote) -> Result<Self, Self::Error> {
50 if quote.status() != QuoteStatus::Success {
51 return Err(SolveError::FailedEncoding(format!(
52 "cannot convert quote with status {:?} to Solution",
53 quote.status()
54 )));
55 }
56
57 let route = quote.route().ok_or_else(|| {
58 SolveError::FailedEncoding("successful quote must have a route".to_string())
59 })?;
60
61 let token_in = route
62 .input_token()
63 .ok_or_else(|| SolveError::FailedEncoding("route has no input token".to_string()))?;
64 let token_out = route
65 .output_token()
66 .ok_or_else(|| SolveError::FailedEncoding("route has no output token".to_string()))?;
67
68 let token_map = route.tokens();
69 let lookup_token = |addr: &Bytes| {
70 token_map
71 .get(addr)
72 .cloned()
73 .ok_or_else(|| {
74 SolveError::FailedEncoding(format!(
75 "token {addr:?} not found in route's token map; \
76 algorithm must populate Route::with_tokens for every swap token"
77 ))
78 })
79 };
80 let swaps = route
81 .swaps()
82 .iter()
83 .map(|s| {
84 let token_in = lookup_token(s.token_in())?;
85 let token_out = lookup_token(s.token_out())?;
86 Ok(Swap::new(
87 s.protocol_component().clone(),
88 token_in,
89 token_out,
90 s.gas_estimate().clone(),
91 )
92 .with_split(*s.split())
93 .with_protocol_state(Arc::from(s.protocol_state().clone_box()))
94 .with_estimated_amount_in(s.amount_in().clone()))
95 })
96 .collect::<Result<Vec<_>, SolveError>>()?;
97
98 Ok(Solution::new(
99 quote.sender().clone(),
100 quote.receiver().clone(),
101 Bytes::from(token_in.as_ref()),
102 Bytes::from(token_out.as_ref()),
103 quote.amount_in().clone(),
104 quote.amount_out().clone(),
105 swaps,
106 ))
107 }
108}
109
110impl Encoder {
111 pub fn new(
120 chain: Chain,
121 swap_encoder_registry: SwapEncoderRegistry,
122 ) -> Result<Self, SolveError> {
123 let router_address = get_router_address(&chain)
124 .map_err(|e| SolveError::FailedEncoding(e.to_string()))?
125 .clone();
126 Ok(Self {
127 tycho_encoder: TychoRouterEncoderBuilder::new()
128 .chain(chain)
129 .swap_encoder_registry(swap_encoder_registry)
130 .build()?,
131 chain,
132 router_address,
133 router_fees: SharedRouterFees::default(),
134 })
135 }
136
137 pub fn router_address(&self) -> &Bytes {
139 &self.router_address
140 }
141
142 pub fn router_fees(&self) -> SharedRouterFees {
147 self.router_fees.clone()
148 }
149
150 pub async fn encode(
159 &self,
160 mut quotes: Vec<OrderQuote>,
161 encoding_options: EncodingOptions,
162 ) -> Result<Vec<OrderQuote>, SolveError> {
163 let slippage = encoding_options.slippage();
164 if slippage == 0.0 {
165 tracing::warn!("slippage is 0, transaction will likely revert");
166 } else if slippage > 0.5 {
167 tracing::warn!(slippage, "slippage exceeds 50%, possible misconfiguration");
168 }
169
170 let mut to_encode: Vec<(usize, Solution)> = Vec::new();
171
172 for (i, quote) in quotes.iter().enumerate() {
173 if quote.status() != QuoteStatus::Success {
174 continue;
175 }
176
177 to_encode.push((
178 i,
179 Solution::try_from(quote)?
180 .with_user_transfer_type(encoding_options.transfer_type().clone()),
181 ));
182 }
183
184 let solutions: Vec<Solution> = to_encode
185 .iter()
186 .map(|(_, s)| s.clone())
187 .collect();
188 let encoded_solutions = self
189 .tycho_encoder
190 .encode_solutions(solutions)?;
191
192 let router_fees = self.router_fees.snapshot();
193 for (encoded_solution, (idx, solution)) in encoded_solutions
194 .into_iter()
195 .zip(to_encode)
196 {
197 quotes[idx].set_gas_estimate(encoded_solution.estimated_gas().clone());
198 let (transaction, fee_breakdown) = self.encode_tycho_router_call(
199 encoded_solution,
200 &solution,
201 &encoding_options,
202 &router_fees,
203 )?;
204 quotes[idx].set_transaction(transaction);
205 quotes[idx].set_fee_breakdown(fee_breakdown);
206 }
207
208 Ok(quotes)
209 }
210
211 fn encode_tycho_router_call(
220 &self,
221 encoded_solution: EncodedSolution,
222 solution: &Solution,
223 encoding_options: &EncodingOptions,
224 router_fees: &RouterFees,
225 ) -> Result<(Transaction, FeeBreakdown), EncodingError> {
226 let amount_in = biguint_to_u256(solution.amount_in());
227 let swap_output = solution.min_amount_out();
228 let fee_client = encoding_options
232 .client_fee_params()
233 .map_or_else(|| solution.sender(), |f| f.receiver());
234 let fee_breakdown = Self::calculate_fee_breakdown(
235 swap_output,
236 encoding_options
237 .client_fee_params()
238 .map_or(0, |f| f.bps()),
239 encoding_options.slippage(),
240 router_fees.fees_for(fee_client),
241 )?;
242 let min_amount_out = biguint_to_u256(fee_breakdown.min_amount_received());
243 let native_address = &self.chain.native_token().address;
244 let router_eth = Address::from_slice(ROUTER_ETH_ADDRESS.as_ref());
245 let to_router_address = |raw: Address| {
246 if raw.as_slice() == native_address.as_ref() {
247 router_eth
248 } else {
249 raw
250 }
251 };
252
253 let token_in = to_router_address(bytes_to_address(solution.token_in())?);
254 let token_out = to_router_address(bytes_to_address(solution.token_out())?);
255 let receiver = bytes_to_address(solution.receiver())?;
256
257 let (permit, permit2_sig) = if let Some(p) = encoding_options.permit() {
258 let d = p.details();
259 let permit = Some(PermitSingle {
260 details: SolPermitDetails {
261 token: bytes_to_address(d.token())?,
262 amount: U160::from(biguint_to_u256(d.amount())),
263 expiration: U48::from(biguint_to_u256(d.expiration())),
264 nonce: U48::from(biguint_to_u256(d.nonce())),
265 },
266 spender: bytes_to_address(p.spender())?,
267 sigDeadline: biguint_to_u256(p.sig_deadline()),
268 });
269 let sig = encoding_options
270 .permit2_signature()
271 .ok_or_else(|| {
272 EncodingError::FatalError("Signature must be provided for permit2".to_string())
273 })?
274 .to_vec();
275 (permit, sig)
276 } else {
277 (None, vec![])
278 };
279
280 let client_fee_params = if let Some(fee) = encoding_options.client_fee_params() {
281 (
282 fee.bps(),
283 bytes_to_address(fee.receiver())?,
284 biguint_to_u256(fee.max_contribution()),
285 U256::from(fee.deadline()),
286 {
289 let mut sig = fee.signature().to_vec();
290 sig.resize(65, 0);
291 sig
292 },
293 )
294 } else {
295 (0u16, Address::ZERO, U256::ZERO, U256::MAX, vec![])
296 };
297
298 let fn_sig = encoded_solution.function_signature();
299 let swaps = encoded_solution.swaps();
300 let fee_breakdown = if encoding_options
301 .client_fee_params()
302 .is_some()
303 {
304 fee_breakdown.with_swaps_hash(keccak256(swaps).0)
305 } else {
306 fee_breakdown
307 };
308
309 let method_calldata = if fn_sig.contains("Permit2") {
310 let permit = permit.ok_or(EncodingError::FatalError(
311 "permit2 object must be set to use permit2".to_string(),
312 ))?;
313 if fn_sig.contains("splitSwap") {
314 (
315 amount_in,
316 token_in,
317 token_out,
318 min_amount_out,
319 U256::from(encoded_solution.n_tokens()),
320 receiver,
321 client_fee_params,
322 permit,
323 permit2_sig,
324 swaps,
325 )
326 .abi_encode()
327 } else {
328 (
329 amount_in,
330 token_in,
331 token_out,
332 min_amount_out,
333 receiver,
334 client_fee_params,
335 permit,
336 permit2_sig,
337 swaps,
338 )
339 .abi_encode()
340 }
341 } else if fn_sig.contains("splitSwap") {
342 (
343 amount_in,
344 token_in,
345 token_out,
346 min_amount_out,
347 U256::from(encoded_solution.n_tokens()),
348 receiver,
349 client_fee_params,
350 swaps,
351 )
352 .abi_encode()
353 } else if fn_sig.contains("singleSwap") || fn_sig.contains("sequentialSwap") {
354 (amount_in, token_in, token_out, min_amount_out, receiver, client_fee_params, swaps)
355 .abi_encode()
356 } else {
357 return Err(EncodingError::FatalError(format!(
358 "unsupported function signature for Tycho router: {fn_sig}"
359 )));
360 };
361
362 let contract_interaction =
363 Self::encode_input(encoded_solution.function_signature(), method_calldata);
364
365 let value =
366 if token_in == router_eth { solution.amount_in().clone() } else { BigUint::ZERO };
367 let mut transaction = Transaction::new(
368 encoded_solution
369 .interacting_with()
370 .clone(),
371 value,
372 contract_interaction,
373 );
374 if encoding_options
375 .client_fee_params()
376 .is_some()
377 {
378 let offset = encoded_solution.client_fee_signature_offset();
379 transaction = transaction.with_client_fee_signature_offset(offset);
380 }
381 Ok((transaction, fee_breakdown))
382 }
383
384 fn encode_input(selector: &str, mut encoded_args: Vec<u8>) -> Vec<u8> {
386 let mut hasher = Keccak256::new();
387 hasher.update(selector.as_bytes());
388 let selector_bytes = &hasher.finalize()[..4];
389 let mut call_data = selector_bytes.to_vec();
390 if encoded_args.len() > 32 &&
394 encoded_args[..32] ==
395 [0u8; 31]
396 .into_iter()
397 .chain([32].to_vec())
398 .collect::<Vec<u8>>()
399 {
400 encoded_args = encoded_args[32..].to_vec();
401 }
402 call_data.extend(encoded_args);
403 call_data
404 }
405
406 fn calculate_fee_breakdown(
417 swap_output: &BigUint,
418 client_fee_bps: u16,
419 slippage: f64,
420 fee_rates: FeeRates,
421 ) -> Result<FeeBreakdown, EncodingError> {
422 let max_fee_units = fee_rates.max_fee_units();
423 let scaled_client_fee = client_fee_bps as u64 * fee_rates.fee_units_per_bps();
426 let fee_on_output = fee_rates.on_output() as u64;
427 let fee_on_client_fee = fee_rates.on_client_fee() as u64;
428
429 if scaled_client_fee + fee_on_output > max_fee_units {
430 return Err(EncodingError::FatalError(format!(
431 "client fee ({client_fee_bps} bps) plus router fee on output \
432 ({fee_on_output} fee units) exceed the {max_fee_units} fee-unit cap (100%); \
433 the router would revert"
434 )));
435 }
436 if fee_on_client_fee > max_fee_units {
437 return Err(EncodingError::FatalError(format!(
438 "router fee on client fee ({fee_on_client_fee} fee units) exceeds the \
439 {max_fee_units} fee-unit cap (100%); the router would revert"
440 )));
441 }
442
443 let mut router_fee_on_client = BigUint::ZERO;
444 let mut client_portion = BigUint::ZERO;
445
446 if scaled_client_fee > 0 {
447 let client_fee_numerator = swap_output * scaled_client_fee;
448 let total_client_fee = &client_fee_numerator / max_fee_units;
449
450 router_fee_on_client = client_fee_numerator * fee_on_client_fee /
451 BigUint::from(fee_rates.max_fee_units_squared());
452
453 client_portion = total_client_fee - &router_fee_on_client;
454 }
455
456 let router_fee_on_output = swap_output * fee_on_output / max_fee_units;
457 let total_router_fee = router_fee_on_client + router_fee_on_output;
458
459 let amount_after_fees = swap_output - &client_portion - &total_router_fee;
460
461 let precision = BigUint::from(1_000_000u64);
462 let slippage_amount =
463 &amount_after_fees * BigUint::from((slippage * 1_000_000.0) as u64) / &precision;
464
465 let min_amount_received = &amount_after_fees - &slippage_amount;
466
467 Ok(FeeBreakdown::new(
468 total_router_fee,
469 client_portion,
470 slippage_amount,
471 min_amount_received,
472 ))
473 }
474}
475
476impl From<EncodingError> for SolveError {
477 fn from(err: EncodingError) -> Self {
478 SolveError::FailedEncoding(err.to_string())
479 }
480}
481
482#[cfg(test)]
483mod tests {
484 use std::collections::HashMap;
485
486 use num_bigint::BigUint;
487 use tycho_execution::encoding::{
488 errors::EncodingError,
489 models::{EncodedSolution, Solution},
490 tycho_encoder::TychoEncoder,
491 };
492 use tycho_simulation::tycho_core::{
493 models::{token::Token, Address, Chain as SimChain},
494 Bytes,
495 };
496
497 use super::*;
498 use crate::{
499 algorithm::test_utils::{component, MockProtocolSim},
500 BlockInfo, OrderQuote, QuoteStatus,
501 };
502
503 fn make_token(addr: Address) -> Token {
504 Token {
505 address: addr,
506 symbol: "T".to_string(),
507 decimals: 18,
508 tax: Default::default(),
509 gas: vec![],
510 chain: SimChain::Ethereum,
511 quality: 100,
512 }
513 }
514
515 fn make_route_swap_addrs(token_in: Address, token_out: Address) -> crate::types::Swap {
516 let tin = make_token(token_in.clone());
517 let tout = make_token(token_out.clone());
518 let pool_addr = "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc";
520 crate::types::Swap::new(
521 pool_addr.to_string(),
522 "uniswap_v2".to_string(),
523 token_in,
524 token_out,
525 BigUint::from(1000u64),
526 BigUint::from(990u64),
527 BigUint::from(50_000u64),
528 component(pool_addr, &[tin, tout]),
529 Box::new(MockProtocolSim::default()),
530 )
531 }
532
533 fn make_route_with_tokens(pairs: &[(Address, Address)]) -> crate::types::Route {
536 let mut tokens = HashMap::new();
537 let swaps = pairs
538 .iter()
539 .map(|(tin, tout)| {
540 tokens
541 .entry(tin.clone())
542 .or_insert_with(|| make_token(tin.clone()));
543 tokens
544 .entry(tout.clone())
545 .or_insert_with(|| make_token(tout.clone()));
546 make_route_swap_addrs(tin.clone(), tout.clone())
547 })
548 .collect();
549 crate::types::Route::new(swaps, tokens).expect("non-empty route")
550 }
551
552 fn make_address(byte: u8) -> Address {
553 Address::from([byte; 20])
554 }
555
556 fn make_order_quote(amount_out: u64) -> OrderQuote {
557 OrderQuote::new(
558 "test-order".to_string(),
559 QuoteStatus::Success,
560 BigUint::from(1000u64),
561 BigUint::from(amount_out),
562 BigUint::from(100_000u64),
563 BigUint::from(amount_out),
564 BlockInfo::new(1, "0x123".to_string(), 1000),
565 "test".to_string(),
566 Bytes::from(make_address(0xAA).as_ref()),
567 Bytes::from(make_address(0xAA).as_ref()),
568 "1".to_string(),
569 )
570 }
571
572 struct MockTychoEncoder;
573
574 impl TychoEncoder for MockTychoEncoder {
575 fn encode_solutions(
576 &self,
577 _solutions: Vec<Solution>,
578 ) -> Result<Vec<EncodedSolution>, EncodingError> {
579 Ok(vec![])
580 }
581
582 fn validate_solution(&self, _solution: &Solution) -> Result<(), EncodingError> {
583 Ok(())
584 }
585 }
586
587 fn mock_encoder(chain: Chain) -> Encoder {
588 let router_fees = SharedRouterFees::default();
589 router_fees.set(RouterFees::new(FEE_SCALE, 100_000, 20_000_000, HashMap::new()));
590 Encoder {
591 tycho_encoder: Box::new(MockTychoEncoder),
592 chain,
593 router_address: Bytes::from([0u8; 20].as_ref()),
594 router_fees,
595 }
596 }
597
598 #[test]
599 fn test_encoder_new_fails_on_unsupported_chain() {
600 let registry =
604 tycho_execution::encoding::evm::swap_encoder::swap_encoder_registry::SwapEncoderRegistry::new(Chain::Ethereum)
605 .add_default_encoders(None)
606 .expect("registry should build for Ethereum");
607 let result = Encoder::new(Chain::Starknet, registry);
608 assert!(result.is_err(), "expected Err for chain without router address, got Ok");
609 }
610
611 #[test]
612 fn test_try_from_without_route_errors() {
613 let quote = make_order_quote(990);
614
615 let result = Solution::try_from("e);
616
617 assert!(result.is_err());
618 }
619
620 #[test]
621 fn test_try_from_non_success_errors() {
622 let quote = OrderQuote::new(
623 "test-order".to_string(),
624 QuoteStatus::NoRouteFound,
625 BigUint::from(1000u64),
626 BigUint::from(990u64),
627 BigUint::from(100_000u64),
628 BigUint::from(990u64),
629 BlockInfo::new(1, "0x123".to_string(), 1000),
630 "test".to_string(),
631 Bytes::from(make_address(0xAA).as_ref()),
632 Bytes::from(make_address(0xAA).as_ref()),
633 "1".to_string(),
634 );
635
636 let result = Solution::try_from("e);
637
638 assert!(result.is_err());
639 }
640
641 #[test]
642 fn test_try_from_maps_tokens_and_amounts() {
643 let quote = make_order_quote(990)
644 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
645
646 let solution = Solution::try_from("e).unwrap();
647
648 assert_eq!(*solution.token_in(), Bytes::from(make_address(0x01).as_ref()));
649 assert_eq!(*solution.token_out(), Bytes::from(make_address(0x02).as_ref()));
650 assert_eq!(*solution.amount_in(), *quote.amount_in());
651 assert_eq!(*solution.min_amount_out(), *quote.amount_out());
652 assert_eq!(solution.swaps().len(), 1);
653 }
654
655 #[test]
656 fn test_try_from_multi_hop_uses_boundary_swap_tokens() {
657 let quote = make_order_quote(990).with_route(make_route_with_tokens(&[
658 (make_address(0x01), make_address(0x02)),
659 (make_address(0x02), make_address(0x03)),
660 ]));
661
662 let solution = Solution::try_from("e).unwrap();
663
664 assert_eq!(*solution.token_in(), Bytes::from(make_address(0x01).as_ref()));
665 assert_eq!(*solution.token_out(), Bytes::from(make_address(0x03).as_ref()));
666 assert_eq!(solution.swaps().len(), 2);
667 }
668
669 #[tokio::test]
670 async fn test_encode_skips_non_successful_solutions() {
671 let encoder = mock_encoder(Chain::Ethereum);
672 let quote = OrderQuote::new(
673 "test-order".to_string(),
674 QuoteStatus::NoRouteFound,
675 BigUint::from(1000u64),
676 BigUint::from(990u64),
677 BigUint::from(100_000u64),
678 BigUint::from(990u64),
679 BlockInfo::new(1, "0x123".to_string(), 1000),
680 "test".to_string(),
681 Bytes::from(make_address(0xAA).as_ref()),
682 Bytes::from(make_address(0xAA).as_ref()),
683 "1".to_string(),
684 );
685
686 let encoding_options = EncodingOptions::new(0.01);
687
688 let result = encoder
689 .encode(vec![quote], encoding_options)
690 .await
691 .unwrap();
692
693 assert!(result[0].transaction().is_none());
694 }
695
696 fn real_encoder() -> Encoder {
697 let registry = SwapEncoderRegistry::new(Chain::Ethereum)
698 .add_default_encoders(None)
699 .unwrap();
700 let encoder = Encoder::new(Chain::Ethereum, registry).unwrap();
701 encoder
703 .router_fees()
704 .set(RouterFees::new(FEE_SCALE, 100_000, 20_000_000, HashMap::new()));
705 encoder
706 }
707
708 #[tokio::test]
709 async fn test_encode_sets_transaction_on_successful_solution() {
710 let encoder = real_encoder();
711 let quote = make_order_quote(990)
712 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
713
714 let encoding_options = EncodingOptions::new(0.01);
715
716 let result = encoder
717 .encode(vec![quote], encoding_options)
718 .await
719 .unwrap();
720
721 assert!(result[0].transaction().is_some());
722 let tx = result[0].transaction().unwrap();
723 assert!(!tx.data().is_empty());
724 assert!(tx.data().len() > 4);
726 }
727
728 #[tokio::test]
729 async fn test_encode_with_client_fee_params() {
730 let encoder = real_encoder();
731 let quote = make_order_quote(990)
732 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
733
734 let fee = crate::ClientFeeParams::new(
735 100,
736 Bytes::from(make_address(0xBB).as_ref()),
737 BigUint::from(0u64),
738 1_893_456_000u64,
739 Bytes::from(vec![0xAB; 65]),
740 );
741 let encoding_options = EncodingOptions::new(0.01).with_client_fee_params(fee);
742
743 let result = encoder
744 .encode(vec![quote], encoding_options)
745 .await
746 .unwrap();
747
748 assert!(result[0].transaction().is_some());
749 let tx = result[0].transaction().unwrap();
750 assert!(!tx.data().is_empty());
751 assert!(tx.data().len() > 4);
753 }
754
755 #[tokio::test]
756 async fn test_encode_without_client_fee_produces_transaction() {
757 let encoder = real_encoder();
758 let quote = make_order_quote(990)
759 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
760
761 let encoding_options = EncodingOptions::new(0.01);
762
763 let result = encoder
764 .encode(vec![quote], encoding_options)
765 .await
766 .unwrap();
767
768 assert!(result[0].transaction().is_some());
769 }
770
771 fn make_client_fee(bps: u16) -> crate::ClientFeeParams {
774 crate::ClientFeeParams::new(
775 bps,
776 Bytes::from(make_address(0xBB).as_ref()),
777 BigUint::from(0u64),
778 1_893_456_000u64,
779 Bytes::from(vec![]),
780 )
781 }
782
783 #[tokio::test]
784 async fn test_encode_with_client_fee_returns_signature_offset() {
785 let encoder = real_encoder();
786 let quote = make_order_quote(990)
787 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
788 let opts = EncodingOptions::new(0.01).with_client_fee_params(make_client_fee(100));
789
790 let result = encoder
791 .encode(vec![quote], opts)
792 .await
793 .unwrap();
794
795 let tx = result[0].transaction().unwrap();
796 tx.client_fee_signature_offset()
797 .expect("client_fee_signature_offset must be present with client fee");
798 }
799
800 #[tokio::test]
801 async fn test_encode_without_client_fee_has_no_signature_offset() {
802 let encoder = real_encoder();
803 let quote = make_order_quote(990)
804 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
805 let opts = EncodingOptions::new(0.01);
806
807 let result = encoder
808 .encode(vec![quote], opts)
809 .await
810 .unwrap();
811
812 let tx = result[0].transaction().unwrap();
813 assert!(tx
814 .client_fee_signature_offset()
815 .is_none());
816 }
817
818 #[tokio::test]
819 async fn test_signature_offset_allows_patching() {
820 let encoder = real_encoder();
821 let real_sig = vec![0xFF; 65];
822 let quote = make_order_quote(990)
823 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
824 let opts = EncodingOptions::new(0.01).with_client_fee_params(make_client_fee(100));
825
826 let result = encoder
827 .encode(vec![quote], opts)
828 .await
829 .unwrap();
830
831 let tx = result[0].transaction().unwrap();
832 let offset = tx
833 .client_fee_signature_offset()
834 .unwrap();
835
836 let mut calldata = tx.data().to_vec();
837 calldata[offset..offset + 65].copy_from_slice(&real_sig);
838 assert_eq!(&calldata[offset..offset + 65], &real_sig[..]);
839 }
840
841 const FEE_SCALE: u64 = 100_000_000;
845
846 #[test]
847 fn test_calculate_fee_breakdown() {
848 let rates = FeeRates::new(100_000, 20_000_000, FEE_SCALE);
850
851 let breakdown =
852 Encoder::calculate_fee_breakdown(&BigUint::from(1_000_000u64), 100, 0.0, rates)
853 .unwrap();
854
855 assert_eq!(*breakdown.client_fee(), BigUint::from(8_000u64));
858 assert_eq!(*breakdown.router_fee(), BigUint::from(3_000u64));
859 assert_eq!(*breakdown.min_amount_received(), BigUint::from(989_000u64));
860 }
861
862 #[test]
863 fn test_calculate_fee_breakdown_zero_fees() {
864 let rates = FeeRates::new(0, 0, FEE_SCALE);
865
866 let breakdown =
867 Encoder::calculate_fee_breakdown(&BigUint::from(1_000_000u64), 0, 0.0, rates).unwrap();
868
869 assert_eq!(*breakdown.client_fee(), BigUint::ZERO);
870 assert_eq!(*breakdown.router_fee(), BigUint::ZERO);
871 assert_eq!(*breakdown.min_amount_received(), BigUint::from(1_000_000u64));
872 }
873
874 #[test]
875 fn test_calculate_fee_breakdown_fee_too_high() {
876 let rates = FeeRates::new(1, 0, FEE_SCALE);
878
879 let result =
880 Encoder::calculate_fee_breakdown(&BigUint::from(1_000_000u64), 10_000, 0.0, rates);
881
882 assert!(result.is_err());
883 }
884
885 #[tokio::test]
886 async fn test_encode_uses_custom_fees_for_client_fee_receiver() {
887 let encoder = real_encoder();
888 let custom = HashMap::from([(Bytes::from(make_address(0xBB).as_ref()), (0u32, 0u32))]);
890 encoder
891 .router_fees()
892 .set(RouterFees::new(FEE_SCALE, 1_000_000, 20_000_000, custom));
893 let quote = make_order_quote(1_000_000_000)
894 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
895 let opts = EncodingOptions::new(0.0).with_client_fee_params(make_client_fee(100));
896
897 let result = encoder
898 .encode(vec![quote], opts)
899 .await
900 .unwrap();
901
902 let breakdown = result[0].fee_breakdown().unwrap();
903 assert_eq!(*breakdown.router_fee(), BigUint::ZERO);
904 assert_eq!(*breakdown.client_fee(), BigUint::from(10_000_000u64));
906 }
907
908 #[tokio::test]
909 async fn test_encode_falls_back_to_sender() {
910 let encoder = real_encoder();
911 let custom =
914 HashMap::from([(Bytes::from(make_address(0xAA).as_ref()), (0u32, 20_000_000u32))]);
915 encoder
916 .router_fees()
917 .set(RouterFees::new(FEE_SCALE, 1_000_000, 20_000_000, custom));
918 let quote = make_order_quote(1_000_000_000)
919 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
920
921 let result = encoder
922 .encode(vec![quote], EncodingOptions::new(0.0))
923 .await
924 .unwrap();
925
926 let breakdown = result[0].fee_breakdown().unwrap();
927 assert_eq!(*breakdown.router_fee(), BigUint::ZERO);
928 }
929
930 #[tokio::test]
931 async fn test_encode_unknown_client() {
932 let encoder = real_encoder();
933 encoder
934 .router_fees()
935 .set(RouterFees::new(FEE_SCALE, 1_000_000, 20_000_000, HashMap::new()));
936 let quote = make_order_quote(1_000_000_000)
937 .with_route(make_route_with_tokens(&[(make_address(0x01), make_address(0x02))]));
938
939 let result = encoder
940 .encode(vec![quote], EncodingOptions::new(0.0))
941 .await
942 .unwrap();
943
944 let breakdown = result[0].fee_breakdown().unwrap();
945 assert_eq!(*breakdown.router_fee(), BigUint::from(10_000_000u64));
947 }
948}