1use alloy::{
2 primitives::{keccak256, U256},
3 sol_types::SolValue,
4};
5use bytes::Bytes;
6use num_bigint::BigUint;
7
8use crate::{error::FyndError, mapping::biguint_to_u256};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum UserTransferType {
17 #[default]
19 TransferFrom,
20 TransferFromPermit2,
22 UseVaultsFunds,
24}
25
26#[derive(Debug, Clone)]
28pub struct PermitDetails {
29 pub(crate) token: bytes::Bytes,
30 pub(crate) amount: num_bigint::BigUint,
31 pub(crate) expiration: num_bigint::BigUint,
32 pub(crate) nonce: num_bigint::BigUint,
33}
34
35impl PermitDetails {
36 pub fn new(
44 token: bytes::Bytes,
45 amount: num_bigint::BigUint,
46 expiration: num_bigint::BigUint,
47 nonce: num_bigint::BigUint,
48 ) -> Self {
49 Self { token, amount, expiration, nonce }
50 }
51}
52
53#[derive(Debug, Clone)]
55pub struct PermitSingle {
56 pub(crate) details: PermitDetails,
57 pub(crate) spender: bytes::Bytes,
58 pub(crate) sig_deadline: num_bigint::BigUint,
59}
60
61impl PermitSingle {
62 pub fn new(
68 details: PermitDetails,
69 spender: bytes::Bytes,
70 sig_deadline: num_bigint::BigUint,
71 ) -> Self {
72 Self { details, spender, sig_deadline }
73 }
74
75 pub fn eip712_signing_hash(
88 &self,
89 chain_id: u64,
90 permit2_address: &bytes::Bytes,
91 ) -> Result<[u8; 32], crate::error::FyndError> {
92 use alloy::sol_types::{eip712_domain, SolStruct};
93
94 let permit2_addr = p2_bytes_to_address(permit2_address, "permit2_address")?;
95 let token = p2_bytes_to_address(&self.details.token, "token")?;
96 let spender = p2_bytes_to_address(&self.spender, "spender")?;
97
98 let amount = p2_biguint_to_uint160(&self.details.amount)?;
99 let expiration = p2_biguint_to_uint48(&self.details.expiration)?;
100 let nonce = p2_biguint_to_uint48(&self.details.nonce)?;
101 let sig_deadline = crate::mapping::biguint_to_u256(&self.sig_deadline);
102
103 let domain = eip712_domain! {
104 name: "Permit2",
105 chain_id: chain_id,
106 verifying_contract: permit2_addr,
107 };
108 #[allow(non_snake_case)]
109 let permit = permit2_sol::PermitSingle {
110 details: permit2_sol::PermitDetails { token, amount, expiration, nonce },
111 spender,
112 sigDeadline: sig_deadline,
113 };
114 Ok(permit.eip712_signing_hash(&domain).0)
115 }
116}
117
118const CLIENT_FEE_UNITS_PER_BPS: u64 = 10_000;
124
125#[derive(Debug, Clone)]
132pub struct ClientFeeParams {
133 pub(crate) bps: u16,
134 pub(crate) receiver: Bytes,
135 pub(crate) max_contribution: BigUint,
136 pub(crate) deadline: u64,
137 pub(crate) signature: Option<Bytes>,
138}
139
140impl ClientFeeParams {
141 pub fn new(bps: u16, receiver: Bytes, max_contribution: BigUint, deadline: u64) -> Self {
145 Self { bps, receiver, max_contribution, deadline, signature: None }
146 }
147
148 pub fn with_signature(mut self, signature: Bytes) -> Self {
150 self.signature = Some(signature);
151 self
152 }
153
154 #[allow(clippy::too_many_arguments)]
173 pub fn eip712_signing_hash(
174 &self,
175 chain_id: u64,
176 router_address: &Bytes,
177 amount_in: &num_bigint::BigUint,
178 token_in: &Bytes,
179 token_out: &Bytes,
180 expected_amount_out: &num_bigint::BigUint,
181 min_amount_out: &num_bigint::BigUint,
182 receiver: &Bytes,
183 swaps_hash: &[u8; 32],
184 ) -> Result<[u8; 32], crate::error::FyndError> {
185 let router_addr = p2_bytes_to_address(router_address, "router_address")?;
186 let fee_receiver = p2_bytes_to_address(&self.receiver, "receiver")?;
187 let max_contrib = biguint_to_u256(&self.max_contribution);
188 let dl = U256::from(self.deadline);
189 let amount_in_u256 = biguint_to_u256(amount_in);
190 let token_in_addr = p2_bytes_to_address(token_in, "token_in")?;
191 let token_out_addr = p2_bytes_to_address(token_out, "token_out")?;
192 let expected_amount_out_u256 = biguint_to_u256(expected_amount_out);
193 let min_amount_out_u256 = biguint_to_u256(min_amount_out);
194 let receiver_addr = p2_bytes_to_address(receiver, "receiver")?;
195 let swaps_b256 = alloy::primitives::B256::from(*swaps_hash);
196
197 let type_hash = keccak256(
198 b"ClientFee(uint32 clientFeeBps,address clientFeeReceiver,\
199uint256 maxClientContribution,uint256 deadline,\
200uint256 amountIn,address tokenIn,address tokenOut,\
201uint256 expectedAmountOut,uint256 minAmountOut,address receiver,bytes swaps)",
202 );
203
204 let domain_type_hash = keccak256(
205 b"EIP712Domain(string name,string version,\
206uint256 chainId,address verifyingContract)",
207 );
208 let domain_separator = keccak256(
209 (
210 domain_type_hash,
211 keccak256(b"TychoRouter"),
212 keccak256(b"1"),
213 U256::from(chain_id),
214 router_addr,
215 )
216 .abi_encode(),
217 );
218
219 let struct_hash = keccak256(
220 (
221 type_hash,
222 U256::from(self.bps as u64 * CLIENT_FEE_UNITS_PER_BPS),
223 fee_receiver,
224 max_contrib,
225 dl,
226 amount_in_u256,
227 token_in_addr,
228 token_out_addr,
229 expected_amount_out_u256,
230 min_amount_out_u256,
231 receiver_addr,
232 swaps_b256,
233 )
234 .abi_encode(),
235 );
236
237 let mut data = [0u8; 66];
238 data[0] = 0x19;
239 data[1] = 0x01;
240 data[2..34].copy_from_slice(domain_separator.as_ref());
241 data[34..66].copy_from_slice(struct_hash.as_ref());
242 Ok(keccak256(data).0)
243 }
244}
245
246mod permit2_sol {
251 use alloy::sol;
252
253 sol! {
254 struct PermitDetails {
255 address token;
256 uint160 amount;
257 uint48 expiration;
258 uint48 nonce;
259 }
260 struct PermitSingle {
261 PermitDetails details;
262 address spender;
263 uint256 sigDeadline;
264 }
265 }
266}
267
268fn p2_bytes_to_address(
269 b: &bytes::Bytes,
270 field: &str,
271) -> Result<alloy::primitives::Address, crate::error::FyndError> {
272 let arr: [u8; 20] = b.as_ref().try_into().map_err(|_| {
273 crate::error::FyndError::Protocol(format!(
274 "expected 20-byte address for {field}, got {} bytes",
275 b.len()
276 ))
277 })?;
278 Ok(alloy::primitives::Address::from(arr))
279}
280
281fn p2_biguint_to_uint160(
282 n: &num_bigint::BigUint,
283) -> Result<alloy::primitives::Uint<160, 3>, crate::error::FyndError> {
284 let bytes = n.to_bytes_be();
285 if bytes.len() > 20 {
286 return Err(crate::error::FyndError::Protocol(format!(
287 "permit amount exceeds uint160 ({} bytes)",
288 bytes.len()
289 )));
290 }
291 let mut arr = [0u8; 20];
292 arr[20 - bytes.len()..].copy_from_slice(&bytes);
293 Ok(alloy::primitives::Uint::<160, 3>::from_be_bytes(arr))
294}
295
296fn p2_biguint_to_uint48(
297 n: &num_bigint::BigUint,
298) -> Result<alloy::primitives::Uint<48, 1>, crate::error::FyndError> {
299 let bytes = n.to_bytes_be();
300 if bytes.len() > 6 {
301 return Err(crate::error::FyndError::Protocol(format!(
302 "permit value exceeds uint48 ({} bytes)",
303 bytes.len()
304 )));
305 }
306 let mut arr = [0u8; 6];
307 arr[6 - bytes.len()..].copy_from_slice(&bytes);
308 Ok(alloy::primitives::Uint::<48, 1>::from_be_bytes(arr))
309}
310
311#[derive(Debug, Clone)]
316pub struct EncodingOptions {
317 pub(crate) slippage: f64,
318 pub(crate) transfer_type: UserTransferType,
319 pub(crate) permit: Option<PermitSingle>,
320 pub(crate) permit2_signature: Option<Bytes>,
321 pub(crate) client_fee_params: Option<ClientFeeParams>,
322 pub(crate) price_guard: Option<PriceGuardConfig>,
323}
324
325impl EncodingOptions {
326 pub fn new(slippage: f64) -> Self {
331 Self {
332 slippage,
333 transfer_type: UserTransferType::TransferFrom,
334 permit: None,
335 permit2_signature: None,
336 client_fee_params: None,
337 price_guard: None,
338 }
339 }
340
341 pub fn with_permit2(
350 mut self,
351 permit: PermitSingle,
352 signature: bytes::Bytes,
353 ) -> Result<Self, crate::error::FyndError> {
354 if signature.len() != 65 {
355 return Err(crate::error::FyndError::Protocol(format!(
356 "Permit2 signature must be exactly 65 bytes, got {}",
357 signature.len()
358 )));
359 }
360 self.transfer_type = UserTransferType::TransferFromPermit2;
361 self.permit = Some(permit);
362 self.permit2_signature = Some(signature);
363 Ok(self)
364 }
365
366 pub fn with_vault_funds(mut self) -> Self {
368 self.transfer_type = UserTransferType::UseVaultsFunds;
369 self
370 }
371
372 pub fn with_client_fee(mut self, params: ClientFeeParams) -> Self {
374 self.client_fee_params = Some(params);
375 self
376 }
377
378 pub fn with_price_guard(mut self, config: PriceGuardConfig) -> Self {
382 self.price_guard = Some(config);
383 self
384 }
385}
386
387#[derive(Debug, Clone)]
391pub struct Transaction {
392 to: Bytes,
393 value: BigUint,
394 pub(crate) data: Vec<u8>,
395 pub(crate) client_fee_signature_offset: Option<usize>,
396}
397
398impl Transaction {
399 pub fn new(to: Bytes, value: BigUint, data: Vec<u8>) -> Self {
405 Self { to, value, data, client_fee_signature_offset: None }
406 }
407
408 pub fn to(&self) -> &Bytes {
410 &self.to
411 }
412
413 pub fn value(&self) -> &BigUint {
415 &self.value
416 }
417
418 pub fn data(&self) -> &[u8] {
420 &self.data
421 }
422
423 pub fn client_fee_signature_offset(&self) -> Option<usize> {
425 self.client_fee_signature_offset
426 }
427}
428
429#[non_exhaustive]
437#[derive(Debug, Clone, Copy, PartialEq, Eq)]
438pub enum OrderSide {
439 Sell,
441}
442
443#[derive(Debug, Clone)]
452pub struct Order {
453 token_in: Bytes,
454 token_out: Bytes,
455 amount: BigUint,
456 side: OrderSide,
457 sender: Bytes,
458 receiver: Option<Bytes>,
459}
460
461impl Order {
462 pub fn new(
471 token_in: Bytes,
472 token_out: Bytes,
473 amount: BigUint,
474 side: OrderSide,
475 sender: Bytes,
476 receiver: Option<Bytes>,
477 ) -> Self {
478 Self { token_in, token_out, amount, side, sender, receiver }
479 }
480
481 pub fn token_in(&self) -> &Bytes {
483 &self.token_in
484 }
485
486 pub fn token_out(&self) -> &Bytes {
488 &self.token_out
489 }
490
491 pub fn amount(&self) -> &BigUint {
493 &self.amount
494 }
495
496 pub fn side(&self) -> OrderSide {
498 self.side
499 }
500
501 pub fn sender(&self) -> &Bytes {
503 &self.sender
504 }
505
506 pub fn receiver(&self) -> Option<&Bytes> {
509 self.receiver.as_ref()
510 }
511}
512
513pub use fynd_rpc_types::PriceGuardConfig;
518
519#[derive(Debug, Clone, Default)]
523pub struct QuoteOptions {
524 pub(crate) timeout_ms: Option<u64>,
525 pub(crate) min_responses: Option<usize>,
526 pub(crate) max_gas: Option<BigUint>,
527 pub(crate) encoding_options: Option<EncodingOptions>,
528}
529
530impl QuoteOptions {
531 pub fn with_timeout_ms(mut self, ms: u64) -> Self {
533 self.timeout_ms = Some(ms);
534 self
535 }
536
537 pub fn with_min_responses(mut self, n: usize) -> Self {
542 self.min_responses = Some(n);
543 self
544 }
545
546 pub fn with_max_gas(mut self, gas: BigUint) -> Self {
548 self.max_gas = Some(gas);
549 self
550 }
551
552 pub fn with_encoding_options(mut self, opts: EncodingOptions) -> Self {
555 self.encoding_options = Some(opts);
556 self
557 }
558
559 pub fn timeout_ms(&self) -> Option<u64> {
561 self.timeout_ms
562 }
563
564 pub fn min_responses(&self) -> Option<usize> {
566 self.min_responses
567 }
568
569 pub fn max_gas(&self) -> Option<&BigUint> {
571 self.max_gas.as_ref()
572 }
573}
574
575#[derive(Debug, Clone)]
577pub struct QuoteParams {
578 pub(crate) order: Order,
579 pub(crate) options: QuoteOptions,
580}
581
582impl QuoteParams {
583 pub fn new(order: Order, options: QuoteOptions) -> Self {
585 Self { order, options }
586 }
587}
588
589#[derive(Debug, Clone)]
594pub struct BatchQuoteParams {
595 pub(crate) orders: Vec<Order>,
596 pub(crate) options: QuoteOptions,
597}
598
599impl BatchQuoteParams {
600 pub fn new(orders: Vec<Order>, options: QuoteOptions) -> Self {
605 Self { orders, options }
606 }
607}
608
609#[derive(Debug, Clone, Copy, PartialEq, Eq)]
615pub enum BackendKind {
616 Fynd,
618 Turbine,
620}
621
622#[derive(Debug, Clone, Copy, PartialEq, Eq)]
624pub enum QuoteStatus {
625 Success,
627 NoRouteFound,
629 InsufficientLiquidity,
631 Timeout,
633 NotReady,
635 PriceCheckFailed,
637}
638
639#[derive(Debug, Clone)]
644pub struct BlockInfo {
645 number: u64,
646 hash: String,
647 timestamp: u64,
648}
649
650impl BlockInfo {
651 pub fn number(&self) -> u64 {
653 self.number
654 }
655
656 pub fn hash(&self) -> &str {
658 &self.hash
659 }
660
661 pub fn timestamp(&self) -> u64 {
663 self.timestamp
664 }
665
666 pub fn new(number: u64, hash: String, timestamp: u64) -> Self {
668 Self { number, hash, timestamp }
669 }
670}
671
672#[derive(Debug, Clone)]
674pub struct Swap {
675 component_id: String,
676 protocol: String,
677 token_in: Bytes,
678 token_out: Bytes,
679 amount_in: BigUint,
680 amount_out: BigUint,
681 gas_estimate: BigUint,
682 #[allow(dead_code)]
683 split: f64,
684}
685
686impl Swap {
687 pub fn component_id(&self) -> &str {
689 &self.component_id
690 }
691
692 pub fn protocol(&self) -> &str {
694 &self.protocol
695 }
696
697 pub fn token_in(&self) -> &Bytes {
699 &self.token_in
700 }
701
702 pub fn token_out(&self) -> &Bytes {
704 &self.token_out
705 }
706
707 pub fn amount_in(&self) -> &BigUint {
709 &self.amount_in
710 }
711
712 pub fn amount_out(&self) -> &BigUint {
714 &self.amount_out
715 }
716
717 pub fn gas_estimate(&self) -> &BigUint {
719 &self.gas_estimate
720 }
721
722 #[allow(clippy::too_many_arguments)]
724 pub fn new(
725 component_id: String,
726 protocol: String,
727 token_in: Bytes,
728 token_out: Bytes,
729 amount_in: BigUint,
730 amount_out: BigUint,
731 gas_estimate: BigUint,
732 split: f64,
733 ) -> Self {
734 Self {
735 component_id,
736 protocol,
737 token_in,
738 token_out,
739 amount_in,
740 amount_out,
741 gas_estimate,
742 split,
743 }
744 }
745}
746
747#[derive(Debug, Clone)]
751pub struct Route {
752 swaps: Vec<Swap>,
753}
754
755impl Route {
756 pub fn swaps(&self) -> &[Swap] {
758 &self.swaps
759 }
760
761 pub fn new(swaps: Vec<Swap>) -> Self {
763 Self { swaps }
764 }
765}
766
767#[derive(Debug, Clone)]
771pub struct FeeBreakdown {
772 router_fee: BigUint,
773 client_fee: BigUint,
774 max_slippage: BigUint,
775 min_amount_received: BigUint,
776 swaps_hash: Option<[u8; 32]>,
778}
779
780impl FeeBreakdown {
781 pub(crate) fn new(
782 router_fee: BigUint,
783 client_fee: BigUint,
784 max_slippage: BigUint,
785 min_amount_received: BigUint,
786 swaps_hash: Option<[u8; 32]>,
787 ) -> Self {
788 Self { router_fee, client_fee, max_slippage, min_amount_received, swaps_hash }
789 }
790
791 pub fn router_fee(&self) -> &BigUint {
793 &self.router_fee
794 }
795
796 pub fn client_fee(&self) -> &BigUint {
798 &self.client_fee
799 }
800
801 pub fn max_slippage(&self) -> &BigUint {
803 &self.max_slippage
804 }
805
806 pub fn min_amount_received(&self) -> &BigUint {
809 &self.min_amount_received
810 }
811
812 pub fn swaps_hash(&self) -> Option<&[u8; 32]> {
818 self.swaps_hash.as_ref()
819 }
820}
821
822#[derive(Debug, Clone)]
824pub struct Quote {
825 order_id: String,
826 status: QuoteStatus,
827 backend: BackendKind,
828 route: Option<Route>,
829 amount_in: BigUint,
830 amount_out: BigUint,
831 gas_estimate: BigUint,
832 amount_out_net_gas: BigUint,
833 price_impact_bps: Option<i32>,
834 block: BlockInfo,
835 token_out: Bytes,
838 receiver: Bytes,
842 transaction: Option<Transaction>,
845 fee_breakdown: Option<FeeBreakdown>,
847 pub(crate) solve_time_ms: u64,
850}
851
852impl Quote {
853 pub fn order_id(&self) -> &str {
855 &self.order_id
856 }
857
858 pub fn status(&self) -> QuoteStatus {
860 self.status
861 }
862
863 pub fn backend(&self) -> BackendKind {
865 self.backend
866 }
867
868 pub fn route(&self) -> Option<&Route> {
870 self.route.as_ref()
871 }
872
873 pub fn amount_in(&self) -> &BigUint {
875 &self.amount_in
876 }
877
878 pub fn amount_out(&self) -> &BigUint {
880 &self.amount_out
881 }
882
883 pub fn gas_estimate(&self) -> &BigUint {
885 &self.gas_estimate
886 }
887
888 pub fn amount_out_net_gas(&self) -> &BigUint {
893 &self.amount_out_net_gas
894 }
895
896 pub fn price_impact_bps(&self) -> Option<i32> {
898 self.price_impact_bps
899 }
900
901 pub fn block(&self) -> &BlockInfo {
903 &self.block
904 }
905
906 pub fn token_out(&self) -> &Bytes {
911 &self.token_out
912 }
913
914 pub fn receiver(&self) -> &Bytes {
921 &self.receiver
922 }
923
924 pub fn transaction(&self) -> Option<&Transaction> {
929 self.transaction.as_ref()
930 }
931
932 pub fn fee_breakdown(&self) -> Option<&FeeBreakdown> {
937 self.fee_breakdown.as_ref()
938 }
939
940 pub fn solve_time_ms(&self) -> u64 {
944 self.solve_time_ms
945 }
946
947 pub fn with_client_fee_signature(mut self, signature: &[u8]) -> Result<Self, FyndError> {
963 let tx = self
964 .transaction
965 .as_mut()
966 .ok_or_else(|| {
967 FyndError::Protocol("transaction required for signature patching".into())
968 })?;
969 let offset = tx
970 .client_fee_signature_offset()
971 .ok_or_else(|| {
972 FyndError::Protocol(
973 "client_fee_signature_offset required for signature patching".into(),
974 )
975 })?;
976 tx.data[offset..offset + signature.len()].copy_from_slice(signature);
977 Ok(self)
978 }
979
980 #[allow(clippy::too_many_arguments)]
982 pub fn new(
983 order_id: String,
984 status: QuoteStatus,
985 backend: BackendKind,
986 route: Option<Route>,
987 amount_in: BigUint,
988 amount_out: BigUint,
989 gas_estimate: BigUint,
990 amount_out_net_gas: BigUint,
991 price_impact_bps: Option<i32>,
992 block: BlockInfo,
993 token_out: Bytes,
994 receiver: Bytes,
995 transaction: Option<Transaction>,
996 fee_breakdown: Option<FeeBreakdown>,
997 ) -> Self {
998 Self {
999 order_id,
1000 status,
1001 backend,
1002 route,
1003 amount_in,
1004 amount_out,
1005 gas_estimate,
1006 amount_out_net_gas,
1007 price_impact_bps,
1008 block,
1009 token_out,
1010 receiver,
1011 transaction,
1012 fee_breakdown,
1013 solve_time_ms: 0,
1014 }
1015 }
1016}
1017
1018#[derive(Debug, Clone)]
1020pub struct InstanceInfo {
1021 router_address: Option<bytes::Bytes>,
1023 permit2_address: bytes::Bytes,
1025 chain_id: u64,
1027 version: String,
1029}
1030
1031impl InstanceInfo {
1032 pub(crate) fn new(
1033 router_address: Option<bytes::Bytes>,
1034 permit2_address: bytes::Bytes,
1035 chain_id: u64,
1036 version: String,
1037 ) -> Self {
1038 Self { router_address, permit2_address, chain_id, version }
1039 }
1040
1041 pub fn router_address(&self) -> Option<&bytes::Bytes> {
1043 self.router_address.as_ref()
1044 }
1045
1046 pub fn permit2_address(&self) -> &bytes::Bytes {
1048 &self.permit2_address
1049 }
1050
1051 pub fn chain_id(&self) -> u64 {
1053 self.chain_id
1054 }
1055
1056 pub fn version(&self) -> &str {
1058 &self.version
1059 }
1060}
1061
1062#[derive(Debug, Clone)]
1064pub struct HealthStatus {
1065 healthy: bool,
1066 last_update_ms: u64,
1067 num_solver_pools: usize,
1068 derived_data_ready: bool,
1069 gas_price_age_ms: Option<u64>,
1070}
1071
1072impl HealthStatus {
1073 pub fn healthy(&self) -> bool {
1075 self.healthy
1076 }
1077
1078 pub fn last_update_ms(&self) -> u64 {
1080 self.last_update_ms
1081 }
1082
1083 pub fn num_solver_pools(&self) -> usize {
1085 self.num_solver_pools
1086 }
1087
1088 pub fn derived_data_ready(&self) -> bool {
1094 self.derived_data_ready
1095 }
1096
1097 pub fn gas_price_age_ms(&self) -> Option<u64> {
1099 self.gas_price_age_ms
1100 }
1101
1102 pub(crate) fn new(
1103 healthy: bool,
1104 last_update_ms: u64,
1105 num_solver_pools: usize,
1106 derived_data_ready: bool,
1107 gas_price_age_ms: Option<u64>,
1108 ) -> Self {
1109 Self { healthy, last_update_ms, num_solver_pools, derived_data_ready, gas_price_age_ms }
1110 }
1111}
1112
1113#[cfg(test)]
1114mod tests {
1115 use num_bigint::BigUint;
1116
1117 use super::*;
1118
1119 fn addr(bytes: &[u8; 20]) -> Bytes {
1120 Bytes::copy_from_slice(bytes)
1121 }
1122
1123 #[test]
1124 fn order_new_and_getters() {
1125 let token_in = addr(&[0xaa; 20]);
1126 let token_out = addr(&[0xbb; 20]);
1127 let amount = BigUint::from(1_000_000u64);
1128 let sender = addr(&[0xcc; 20]);
1129
1130 let order = Order::new(
1131 token_in.clone(),
1132 token_out.clone(),
1133 amount.clone(),
1134 OrderSide::Sell,
1135 sender.clone(),
1136 None,
1137 );
1138
1139 assert_eq!(order.token_in(), &token_in);
1140 assert_eq!(order.token_out(), &token_out);
1141 assert_eq!(order.amount(), &amount);
1142 assert_eq!(order.sender(), &sender);
1143 assert!(order.receiver().is_none());
1144 assert_eq!(order.side(), OrderSide::Sell);
1145 }
1146
1147 #[test]
1148 fn order_with_explicit_receiver() {
1149 let receiver = Bytes::copy_from_slice(&[0xdd; 20]);
1150 let order = Order::new(
1151 Bytes::copy_from_slice(&[0xaa; 20]),
1152 Bytes::copy_from_slice(&[0xbb; 20]),
1153 BigUint::from(1u32),
1154 OrderSide::Sell,
1155 Bytes::copy_from_slice(&[0xcc; 20]),
1156 Some(receiver.clone()),
1157 );
1158 assert_eq!(order.receiver(), Some(&receiver));
1159 }
1160
1161 #[test]
1162 fn quote_options_builder() {
1163 let opts = QuoteOptions::default()
1164 .with_timeout_ms(500)
1165 .with_min_responses(2)
1166 .with_max_gas(BigUint::from(1_000_000u64));
1167
1168 assert_eq!(opts.timeout_ms(), Some(500));
1169 assert_eq!(opts.min_responses(), Some(2));
1170 assert_eq!(opts.max_gas(), Some(&BigUint::from(1_000_000u64)));
1171 }
1172
1173 #[test]
1174 fn quote_options_default_all_none() {
1175 let opts = QuoteOptions::default();
1176 assert!(opts.timeout_ms().is_none());
1177 assert!(opts.min_responses().is_none());
1178 assert!(opts.max_gas().is_none());
1179 }
1180
1181 #[test]
1182 fn encoding_options_with_permit2_sets_fields() {
1183 let token = Bytes::copy_from_slice(&[0xaa; 20]);
1184 let spender = Bytes::copy_from_slice(&[0xbb; 20]);
1185 let sig = Bytes::copy_from_slice(&[0xcc; 65]);
1186 let details = PermitDetails::new(
1187 token,
1188 BigUint::from(1_000u32),
1189 BigUint::from(9_999_999u32),
1190 BigUint::from(0u32),
1191 );
1192 let permit = PermitSingle::new(details, spender, BigUint::from(9_999_999u32));
1193
1194 let opts = EncodingOptions::new(0.005)
1195 .with_permit2(permit, sig.clone())
1196 .unwrap();
1197
1198 assert_eq!(opts.transfer_type, UserTransferType::TransferFromPermit2);
1199 assert!(opts.permit.is_some());
1200 assert_eq!(opts.permit2_signature.as_ref().unwrap(), &sig);
1201 }
1202
1203 #[test]
1204 fn encoding_options_with_permit2_rejects_wrong_signature_length() {
1205 let details = PermitDetails::new(
1206 Bytes::copy_from_slice(&[0xaa; 20]),
1207 BigUint::from(1_000u32),
1208 BigUint::from(9_999_999u32),
1209 BigUint::from(0u32),
1210 );
1211 let permit = PermitSingle::new(
1212 details,
1213 Bytes::copy_from_slice(&[0xbb; 20]),
1214 BigUint::from(9_999_999u32),
1215 );
1216 let bad_sig = Bytes::copy_from_slice(&[0xcc; 64]); assert!(matches!(
1218 EncodingOptions::new(0.005).with_permit2(permit, bad_sig),
1219 Err(crate::error::FyndError::Protocol(_))
1220 ));
1221 }
1222
1223 #[test]
1224 fn encoding_options_with_vault_funds_sets_variant() {
1225 let opts = EncodingOptions::new(0.005).with_vault_funds();
1226 assert_eq!(opts.transfer_type, UserTransferType::UseVaultsFunds);
1227 assert!(opts.permit.is_none());
1228 assert!(opts.permit2_signature.is_none());
1229 }
1230
1231 fn sample_permit_single() -> PermitSingle {
1232 let details = PermitDetails::new(
1233 Bytes::copy_from_slice(&[0xaa; 20]),
1234 BigUint::from(1_000u32),
1235 BigUint::from(9_999_999u32),
1236 BigUint::from(0u32),
1237 );
1238 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(9_999_999u32))
1239 }
1240
1241 #[test]
1242 fn eip712_signing_hash_returns_32_bytes() {
1243 let permit = sample_permit_single();
1244 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1245 let hash = permit
1246 .eip712_signing_hash(1, &permit2_addr)
1247 .unwrap();
1248 assert_eq!(hash.len(), 32);
1249 assert_ne!(hash, [0u8; 32]);
1251 }
1252
1253 #[test]
1254 fn eip712_signing_hash_is_deterministic() {
1255 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1256 let h1 = sample_permit_single()
1257 .eip712_signing_hash(1, &permit2_addr)
1258 .unwrap();
1259 let h2 = sample_permit_single()
1260 .eip712_signing_hash(1, &permit2_addr)
1261 .unwrap();
1262 assert_eq!(h1, h2);
1263 }
1264
1265 #[test]
1266 fn eip712_signing_hash_differs_by_chain_id() {
1267 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1268 let h1 = sample_permit_single()
1269 .eip712_signing_hash(1, &permit2_addr)
1270 .unwrap();
1271 let h137 = sample_permit_single()
1272 .eip712_signing_hash(137, &permit2_addr)
1273 .unwrap();
1274 assert_ne!(h1, h137);
1275 }
1276
1277 #[test]
1278 fn eip712_signing_hash_invalid_permit2_address() {
1279 let permit = sample_permit_single();
1280 let bad_addr = Bytes::copy_from_slice(&[0xcc; 4]);
1281 assert!(matches!(
1282 permit.eip712_signing_hash(1, &bad_addr),
1283 Err(crate::error::FyndError::Protocol(_))
1284 ));
1285 }
1286
1287 #[test]
1288 fn eip712_signing_hash_invalid_token_address() {
1289 let details = PermitDetails::new(
1290 Bytes::copy_from_slice(&[0xaa; 4]), BigUint::from(1u32),
1292 BigUint::from(1u32),
1293 BigUint::from(0u32),
1294 );
1295 let permit =
1296 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(1u32));
1297 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1298 assert!(matches!(
1299 permit.eip712_signing_hash(1, &permit2_addr),
1300 Err(crate::error::FyndError::Protocol(_))
1301 ));
1302 }
1303
1304 #[test]
1305 fn eip712_signing_hash_amount_exceeds_uint160() {
1306 let oversized_amount = BigUint::from_bytes_be(&[0x01; 21]);
1308 let details = PermitDetails::new(
1309 Bytes::copy_from_slice(&[0xaa; 20]),
1310 oversized_amount,
1311 BigUint::from(1u32),
1312 BigUint::from(0u32),
1313 );
1314 let permit =
1315 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(1u32));
1316 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1317 assert!(matches!(
1318 permit.eip712_signing_hash(1, &permit2_addr),
1319 Err(crate::error::FyndError::Protocol(_))
1320 ));
1321 }
1322
1323 fn sample_fee_receiver() -> Bytes {
1328 Bytes::copy_from_slice(&[0x44; 20])
1329 }
1330
1331 fn sample_router_address() -> Bytes {
1332 Bytes::copy_from_slice(&[0x33; 20])
1333 }
1334
1335 fn sample_fee_params(bps: u16, receiver: Bytes) -> ClientFeeParams {
1336 ClientFeeParams::new(bps, receiver, BigUint::ZERO, 1_893_456_000)
1337 }
1338
1339 fn sample_token_in() -> Bytes {
1340 Bytes::copy_from_slice(&[0x11; 20])
1341 }
1342
1343 fn sample_token_out() -> Bytes {
1344 Bytes::copy_from_slice(&[0x22; 20])
1345 }
1346
1347 fn sample_swap_receiver() -> Bytes {
1348 Bytes::copy_from_slice(&[0xAA; 20])
1349 }
1350
1351 fn sample_min_amount_out() -> BigUint {
1352 BigUint::from(1_000_000u64)
1353 }
1354
1355 fn sample_expected_amount_out() -> BigUint {
1356 BigUint::from(1_010_000u64)
1357 }
1358
1359 fn sample_amount_in() -> BigUint {
1360 BigUint::from(1_000_000_000_000_000_000u64)
1361 }
1362
1363 fn sample_swaps_hash() -> [u8; 32] {
1364 [0xAB; 32]
1365 }
1366
1367 #[test]
1368 fn client_fee_with_client_fee_sets_fields() {
1369 let fee = ClientFeeParams::new(
1370 100,
1371 sample_fee_receiver(),
1372 BigUint::from(500_000u64),
1373 1_893_456_000,
1374 );
1375 let opts = EncodingOptions::new(0.01).with_client_fee(fee);
1376 assert!(opts.client_fee_params.is_some());
1377 let stored = opts.client_fee_params.as_ref().unwrap();
1378 assert_eq!(stored.bps, 100);
1379 assert_eq!(stored.max_contribution, BigUint::from(500_000u64));
1380 }
1381
1382 #[test]
1383 fn client_fee_signing_hash_returns_32_bytes() {
1384 let fee = sample_fee_params(100, sample_fee_receiver());
1385 let hash = fee
1386 .eip712_signing_hash(
1387 1,
1388 &sample_router_address(),
1389 &sample_amount_in(),
1390 &sample_token_in(),
1391 &sample_token_out(),
1392 &sample_expected_amount_out(),
1393 &sample_min_amount_out(),
1394 &sample_swap_receiver(),
1395 &sample_swaps_hash(),
1396 )
1397 .unwrap();
1398 assert_eq!(hash.len(), 32);
1399 assert_ne!(hash, [0u8; 32]);
1400 }
1401
1402 #[test]
1403 fn client_fee_signing_hash_is_deterministic() {
1404 let fee = sample_fee_params(100, sample_fee_receiver());
1405 let h1 = fee
1406 .eip712_signing_hash(
1407 1,
1408 &sample_router_address(),
1409 &sample_amount_in(),
1410 &sample_token_in(),
1411 &sample_token_out(),
1412 &sample_expected_amount_out(),
1413 &sample_min_amount_out(),
1414 &sample_swap_receiver(),
1415 &sample_swaps_hash(),
1416 )
1417 .unwrap();
1418 let h2 = fee
1419 .eip712_signing_hash(
1420 1,
1421 &sample_router_address(),
1422 &sample_amount_in(),
1423 &sample_token_in(),
1424 &sample_token_out(),
1425 &sample_expected_amount_out(),
1426 &sample_min_amount_out(),
1427 &sample_swap_receiver(),
1428 &sample_swaps_hash(),
1429 )
1430 .unwrap();
1431 assert_eq!(h1, h2);
1432 }
1433
1434 #[test]
1435 fn client_fee_signing_hash_differs_by_chain_id() {
1436 let fee = sample_fee_params(100, sample_fee_receiver());
1437 let h1 = fee
1438 .eip712_signing_hash(
1439 1,
1440 &sample_router_address(),
1441 &sample_amount_in(),
1442 &sample_token_in(),
1443 &sample_token_out(),
1444 &sample_expected_amount_out(),
1445 &sample_min_amount_out(),
1446 &sample_swap_receiver(),
1447 &sample_swaps_hash(),
1448 )
1449 .unwrap();
1450 let h137 = fee
1451 .eip712_signing_hash(
1452 137,
1453 &sample_router_address(),
1454 &sample_amount_in(),
1455 &sample_token_in(),
1456 &sample_token_out(),
1457 &sample_expected_amount_out(),
1458 &sample_min_amount_out(),
1459 &sample_swap_receiver(),
1460 &sample_swaps_hash(),
1461 )
1462 .unwrap();
1463 assert_ne!(h1, h137);
1464 }
1465
1466 #[test]
1467 fn client_fee_signing_hash_differs_by_bps() {
1468 let h100 = sample_fee_params(100, sample_fee_receiver())
1469 .eip712_signing_hash(
1470 1,
1471 &sample_router_address(),
1472 &sample_amount_in(),
1473 &sample_token_in(),
1474 &sample_token_out(),
1475 &sample_expected_amount_out(),
1476 &sample_min_amount_out(),
1477 &sample_swap_receiver(),
1478 &sample_swaps_hash(),
1479 )
1480 .unwrap();
1481 let h200 = sample_fee_params(200, sample_fee_receiver())
1482 .eip712_signing_hash(
1483 1,
1484 &sample_router_address(),
1485 &sample_amount_in(),
1486 &sample_token_in(),
1487 &sample_token_out(),
1488 &sample_expected_amount_out(),
1489 &sample_min_amount_out(),
1490 &sample_swap_receiver(),
1491 &sample_swaps_hash(),
1492 )
1493 .unwrap();
1494 assert_ne!(h100, h200);
1495 }
1496
1497 #[test]
1498 fn client_fee_signing_hash_differs_by_expected_amount_out() {
1499 let fee = sample_fee_params(100, sample_fee_receiver());
1500 let quoted = fee
1501 .eip712_signing_hash(
1502 1,
1503 &sample_router_address(),
1504 &sample_amount_in(),
1505 &sample_token_in(),
1506 &sample_token_out(),
1507 &sample_expected_amount_out(),
1508 &sample_min_amount_out(),
1509 &sample_swap_receiver(),
1510 &sample_swaps_hash(),
1511 )
1512 .unwrap();
1513 let higher = fee
1514 .eip712_signing_hash(
1515 1,
1516 &sample_router_address(),
1517 &sample_amount_in(),
1518 &sample_token_in(),
1519 &sample_token_out(),
1520 &(sample_expected_amount_out() + BigUint::from(1u32)),
1521 &sample_min_amount_out(),
1522 &sample_swap_receiver(),
1523 &sample_swaps_hash(),
1524 )
1525 .unwrap();
1526 assert_ne!(quoted, higher);
1527 }
1528
1529 #[test]
1530 fn client_fee_signing_hash_differs_by_receiver() {
1531 let other_receiver = Bytes::copy_from_slice(&[0x55; 20]);
1532 let h1 = sample_fee_params(100, sample_fee_receiver())
1533 .eip712_signing_hash(
1534 1,
1535 &sample_router_address(),
1536 &sample_amount_in(),
1537 &sample_token_in(),
1538 &sample_token_out(),
1539 &sample_expected_amount_out(),
1540 &sample_min_amount_out(),
1541 &sample_swap_receiver(),
1542 &sample_swaps_hash(),
1543 )
1544 .unwrap();
1545 let h2 = sample_fee_params(100, other_receiver)
1546 .eip712_signing_hash(
1547 1,
1548 &sample_router_address(),
1549 &sample_amount_in(),
1550 &sample_token_in(),
1551 &sample_token_out(),
1552 &sample_expected_amount_out(),
1553 &sample_min_amount_out(),
1554 &sample_swap_receiver(),
1555 &sample_swaps_hash(),
1556 )
1557 .unwrap();
1558 assert_ne!(h1, h2);
1559 }
1560
1561 #[test]
1562 fn client_fee_signing_hash_rejects_bad_receiver_address() {
1563 let bad_addr = Bytes::copy_from_slice(&[0x44; 4]);
1564 let fee = sample_fee_params(100, bad_addr);
1565 assert!(matches!(
1566 fee.eip712_signing_hash(
1567 1,
1568 &sample_router_address(),
1569 &sample_amount_in(),
1570 &sample_token_in(),
1571 &sample_token_out(),
1572 &sample_expected_amount_out(),
1573 &sample_min_amount_out(),
1574 &sample_swap_receiver(),
1575 &sample_swaps_hash(),
1576 ),
1577 Err(crate::error::FyndError::Protocol(_))
1578 ));
1579 }
1580
1581 #[test]
1582 fn client_fee_signing_hash_rejects_bad_router_address() {
1583 let bad_addr = Bytes::copy_from_slice(&[0x33; 4]);
1584 let fee = sample_fee_params(100, sample_fee_receiver());
1585 assert!(matches!(
1586 fee.eip712_signing_hash(
1587 1,
1588 &bad_addr,
1589 &sample_amount_in(),
1590 &sample_token_in(),
1591 &sample_token_out(),
1592 &sample_expected_amount_out(),
1593 &sample_min_amount_out(),
1594 &sample_swap_receiver(),
1595 &sample_swaps_hash(),
1596 ),
1597 Err(crate::error::FyndError::Protocol(_))
1598 ));
1599 }
1600}