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
118#[derive(Debug, Clone)]
125pub struct ClientFeeParams {
126 pub(crate) bps: u16,
127 pub(crate) receiver: Bytes,
128 pub(crate) max_contribution: BigUint,
129 pub(crate) deadline: u64,
130 pub(crate) signature: Option<Bytes>,
131}
132
133impl ClientFeeParams {
134 pub fn new(bps: u16, receiver: Bytes, max_contribution: BigUint, deadline: u64) -> Self {
138 Self { bps, receiver, max_contribution, deadline, signature: None }
139 }
140
141 pub fn with_signature(mut self, signature: Bytes) -> Self {
143 self.signature = Some(signature);
144 self
145 }
146
147 #[allow(clippy::too_many_arguments)]
165 pub fn eip712_signing_hash(
166 &self,
167 chain_id: u64,
168 router_address: &Bytes,
169 amount_in: &num_bigint::BigUint,
170 token_in: &Bytes,
171 token_out: &Bytes,
172 min_amount_out: &num_bigint::BigUint,
173 receiver: &Bytes,
174 swaps_hash: &[u8; 32],
175 ) -> Result<[u8; 32], crate::error::FyndError> {
176 let router_addr = p2_bytes_to_address(router_address, "router_address")?;
177 let fee_receiver = p2_bytes_to_address(&self.receiver, "receiver")?;
178 let max_contrib = biguint_to_u256(&self.max_contribution);
179 let dl = U256::from(self.deadline);
180 let amount_in_u256 = biguint_to_u256(amount_in);
181 let token_in_addr = p2_bytes_to_address(token_in, "token_in")?;
182 let token_out_addr = p2_bytes_to_address(token_out, "token_out")?;
183 let min_amount_out_u256 = biguint_to_u256(min_amount_out);
184 let receiver_addr = p2_bytes_to_address(receiver, "receiver")?;
185 let swaps_b256 = alloy::primitives::B256::from(*swaps_hash);
186
187 let type_hash = keccak256(
188 b"ClientFee(uint16 clientFeeBps,address clientFeeReceiver,\
189uint256 maxClientContribution,uint256 deadline,\
190uint256 amountIn,address tokenIn,address tokenOut,\
191uint256 minAmountOut,address receiver,bytes swaps)",
192 );
193
194 let domain_type_hash = keccak256(
195 b"EIP712Domain(string name,string version,\
196uint256 chainId,address verifyingContract)",
197 );
198 let domain_separator = keccak256(
199 (
200 domain_type_hash,
201 keccak256(b"TychoRouter"),
202 keccak256(b"1"),
203 U256::from(chain_id),
204 router_addr,
205 )
206 .abi_encode(),
207 );
208
209 let struct_hash = keccak256(
210 (
211 type_hash,
212 U256::from(self.bps),
213 fee_receiver,
214 max_contrib,
215 dl,
216 amount_in_u256,
217 token_in_addr,
218 token_out_addr,
219 min_amount_out_u256,
220 receiver_addr,
221 swaps_b256,
222 )
223 .abi_encode(),
224 );
225
226 let mut data = [0u8; 66];
227 data[0] = 0x19;
228 data[1] = 0x01;
229 data[2..34].copy_from_slice(domain_separator.as_ref());
230 data[34..66].copy_from_slice(struct_hash.as_ref());
231 Ok(keccak256(data).0)
232 }
233}
234
235mod permit2_sol {
240 use alloy::sol;
241
242 sol! {
243 struct PermitDetails {
244 address token;
245 uint160 amount;
246 uint48 expiration;
247 uint48 nonce;
248 }
249 struct PermitSingle {
250 PermitDetails details;
251 address spender;
252 uint256 sigDeadline;
253 }
254 }
255}
256
257fn p2_bytes_to_address(
258 b: &bytes::Bytes,
259 field: &str,
260) -> Result<alloy::primitives::Address, crate::error::FyndError> {
261 let arr: [u8; 20] = b.as_ref().try_into().map_err(|_| {
262 crate::error::FyndError::Protocol(format!(
263 "expected 20-byte address for {field}, got {} bytes",
264 b.len()
265 ))
266 })?;
267 Ok(alloy::primitives::Address::from(arr))
268}
269
270fn p2_biguint_to_uint160(
271 n: &num_bigint::BigUint,
272) -> Result<alloy::primitives::Uint<160, 3>, crate::error::FyndError> {
273 let bytes = n.to_bytes_be();
274 if bytes.len() > 20 {
275 return Err(crate::error::FyndError::Protocol(format!(
276 "permit amount exceeds uint160 ({} bytes)",
277 bytes.len()
278 )));
279 }
280 let mut arr = [0u8; 20];
281 arr[20 - bytes.len()..].copy_from_slice(&bytes);
282 Ok(alloy::primitives::Uint::<160, 3>::from_be_bytes(arr))
283}
284
285fn p2_biguint_to_uint48(
286 n: &num_bigint::BigUint,
287) -> Result<alloy::primitives::Uint<48, 1>, crate::error::FyndError> {
288 let bytes = n.to_bytes_be();
289 if bytes.len() > 6 {
290 return Err(crate::error::FyndError::Protocol(format!(
291 "permit value exceeds uint48 ({} bytes)",
292 bytes.len()
293 )));
294 }
295 let mut arr = [0u8; 6];
296 arr[6 - bytes.len()..].copy_from_slice(&bytes);
297 Ok(alloy::primitives::Uint::<48, 1>::from_be_bytes(arr))
298}
299
300#[derive(Debug, Clone)]
305pub struct EncodingOptions {
306 pub(crate) slippage: f64,
307 pub(crate) transfer_type: UserTransferType,
308 pub(crate) permit: Option<PermitSingle>,
309 pub(crate) permit2_signature: Option<Bytes>,
310 pub(crate) client_fee_params: Option<ClientFeeParams>,
311 pub(crate) price_guard: Option<PriceGuardConfig>,
312}
313
314impl EncodingOptions {
315 pub fn new(slippage: f64) -> Self {
320 Self {
321 slippage,
322 transfer_type: UserTransferType::TransferFrom,
323 permit: None,
324 permit2_signature: None,
325 client_fee_params: None,
326 price_guard: None,
327 }
328 }
329
330 pub fn with_permit2(
339 mut self,
340 permit: PermitSingle,
341 signature: bytes::Bytes,
342 ) -> Result<Self, crate::error::FyndError> {
343 if signature.len() != 65 {
344 return Err(crate::error::FyndError::Protocol(format!(
345 "Permit2 signature must be exactly 65 bytes, got {}",
346 signature.len()
347 )));
348 }
349 self.transfer_type = UserTransferType::TransferFromPermit2;
350 self.permit = Some(permit);
351 self.permit2_signature = Some(signature);
352 Ok(self)
353 }
354
355 pub fn with_vault_funds(mut self) -> Self {
357 self.transfer_type = UserTransferType::UseVaultsFunds;
358 self
359 }
360
361 pub fn with_client_fee(mut self, params: ClientFeeParams) -> Self {
363 self.client_fee_params = Some(params);
364 self
365 }
366
367 pub fn with_price_guard(mut self, config: PriceGuardConfig) -> Self {
371 self.price_guard = Some(config);
372 self
373 }
374}
375
376#[derive(Debug, Clone)]
380pub struct Transaction {
381 to: Bytes,
382 value: BigUint,
383 pub(crate) data: Vec<u8>,
384 pub(crate) client_fee_signature_offset: Option<usize>,
385}
386
387impl Transaction {
388 pub fn new(to: Bytes, value: BigUint, data: Vec<u8>) -> Self {
394 Self { to, value, data, client_fee_signature_offset: None }
395 }
396
397 pub fn to(&self) -> &Bytes {
399 &self.to
400 }
401
402 pub fn value(&self) -> &BigUint {
404 &self.value
405 }
406
407 pub fn data(&self) -> &[u8] {
409 &self.data
410 }
411
412 pub fn client_fee_signature_offset(&self) -> Option<usize> {
414 self.client_fee_signature_offset
415 }
416}
417
418#[non_exhaustive]
426#[derive(Debug, Clone, Copy, PartialEq, Eq)]
427pub enum OrderSide {
428 Sell,
430}
431
432#[derive(Debug, Clone)]
441pub struct Order {
442 token_in: Bytes,
443 token_out: Bytes,
444 amount: BigUint,
445 side: OrderSide,
446 sender: Bytes,
447 receiver: Option<Bytes>,
448}
449
450impl Order {
451 pub fn new(
460 token_in: Bytes,
461 token_out: Bytes,
462 amount: BigUint,
463 side: OrderSide,
464 sender: Bytes,
465 receiver: Option<Bytes>,
466 ) -> Self {
467 Self { token_in, token_out, amount, side, sender, receiver }
468 }
469
470 pub fn token_in(&self) -> &Bytes {
472 &self.token_in
473 }
474
475 pub fn token_out(&self) -> &Bytes {
477 &self.token_out
478 }
479
480 pub fn amount(&self) -> &BigUint {
482 &self.amount
483 }
484
485 pub fn side(&self) -> OrderSide {
487 self.side
488 }
489
490 pub fn sender(&self) -> &Bytes {
492 &self.sender
493 }
494
495 pub fn receiver(&self) -> Option<&Bytes> {
498 self.receiver.as_ref()
499 }
500}
501
502pub use fynd_rpc_types::PriceGuardConfig;
507
508#[derive(Debug, Clone, Default)]
512pub struct QuoteOptions {
513 pub(crate) timeout_ms: Option<u64>,
514 pub(crate) min_responses: Option<usize>,
515 pub(crate) max_gas: Option<BigUint>,
516 pub(crate) encoding_options: Option<EncodingOptions>,
517}
518
519impl QuoteOptions {
520 pub fn with_timeout_ms(mut self, ms: u64) -> Self {
522 self.timeout_ms = Some(ms);
523 self
524 }
525
526 pub fn with_min_responses(mut self, n: usize) -> Self {
531 self.min_responses = Some(n);
532 self
533 }
534
535 pub fn with_max_gas(mut self, gas: BigUint) -> Self {
537 self.max_gas = Some(gas);
538 self
539 }
540
541 pub fn with_encoding_options(mut self, opts: EncodingOptions) -> Self {
544 self.encoding_options = Some(opts);
545 self
546 }
547
548 pub fn timeout_ms(&self) -> Option<u64> {
550 self.timeout_ms
551 }
552
553 pub fn min_responses(&self) -> Option<usize> {
555 self.min_responses
556 }
557
558 pub fn max_gas(&self) -> Option<&BigUint> {
560 self.max_gas.as_ref()
561 }
562}
563
564#[derive(Debug, Clone)]
566pub struct QuoteParams {
567 pub(crate) order: Order,
568 pub(crate) options: QuoteOptions,
569}
570
571impl QuoteParams {
572 pub fn new(order: Order, options: QuoteOptions) -> Self {
574 Self { order, options }
575 }
576}
577
578#[derive(Debug, Clone)]
583pub struct BatchQuoteParams {
584 pub(crate) orders: Vec<Order>,
585 pub(crate) options: QuoteOptions,
586}
587
588impl BatchQuoteParams {
589 pub fn new(orders: Vec<Order>, options: QuoteOptions) -> Self {
594 Self { orders, options }
595 }
596}
597
598#[derive(Debug, Clone, Copy, PartialEq, Eq)]
604pub enum BackendKind {
605 Fynd,
607 Turbine,
609}
610
611#[derive(Debug, Clone, Copy, PartialEq, Eq)]
613pub enum QuoteStatus {
614 Success,
616 NoRouteFound,
618 InsufficientLiquidity,
620 Timeout,
622 NotReady,
624 PriceCheckFailed,
626}
627
628#[derive(Debug, Clone)]
633pub struct BlockInfo {
634 number: u64,
635 hash: String,
636 timestamp: u64,
637}
638
639impl BlockInfo {
640 pub fn number(&self) -> u64 {
642 self.number
643 }
644
645 pub fn hash(&self) -> &str {
647 &self.hash
648 }
649
650 pub fn timestamp(&self) -> u64 {
652 self.timestamp
653 }
654
655 pub fn new(number: u64, hash: String, timestamp: u64) -> Self {
657 Self { number, hash, timestamp }
658 }
659}
660
661#[derive(Debug, Clone)]
663pub struct Swap {
664 component_id: String,
665 protocol: String,
666 token_in: Bytes,
667 token_out: Bytes,
668 amount_in: BigUint,
669 amount_out: BigUint,
670 gas_estimate: BigUint,
671 #[allow(dead_code)]
672 split: f64,
673}
674
675impl Swap {
676 pub fn component_id(&self) -> &str {
678 &self.component_id
679 }
680
681 pub fn protocol(&self) -> &str {
683 &self.protocol
684 }
685
686 pub fn token_in(&self) -> &Bytes {
688 &self.token_in
689 }
690
691 pub fn token_out(&self) -> &Bytes {
693 &self.token_out
694 }
695
696 pub fn amount_in(&self) -> &BigUint {
698 &self.amount_in
699 }
700
701 pub fn amount_out(&self) -> &BigUint {
703 &self.amount_out
704 }
705
706 pub fn gas_estimate(&self) -> &BigUint {
708 &self.gas_estimate
709 }
710
711 #[allow(clippy::too_many_arguments)]
713 pub fn new(
714 component_id: String,
715 protocol: String,
716 token_in: Bytes,
717 token_out: Bytes,
718 amount_in: BigUint,
719 amount_out: BigUint,
720 gas_estimate: BigUint,
721 split: f64,
722 ) -> Self {
723 Self {
724 component_id,
725 protocol,
726 token_in,
727 token_out,
728 amount_in,
729 amount_out,
730 gas_estimate,
731 split,
732 }
733 }
734}
735
736#[derive(Debug, Clone)]
740pub struct Route {
741 swaps: Vec<Swap>,
742}
743
744impl Route {
745 pub fn swaps(&self) -> &[Swap] {
747 &self.swaps
748 }
749
750 pub fn new(swaps: Vec<Swap>) -> Self {
752 Self { swaps }
753 }
754}
755
756#[derive(Debug, Clone)]
760pub struct FeeBreakdown {
761 router_fee: BigUint,
762 client_fee: BigUint,
763 max_slippage: BigUint,
764 min_amount_received: BigUint,
765 swaps_hash: Option<[u8; 32]>,
767}
768
769impl FeeBreakdown {
770 pub(crate) fn new(
771 router_fee: BigUint,
772 client_fee: BigUint,
773 max_slippage: BigUint,
774 min_amount_received: BigUint,
775 swaps_hash: Option<[u8; 32]>,
776 ) -> Self {
777 Self { router_fee, client_fee, max_slippage, min_amount_received, swaps_hash }
778 }
779
780 pub fn router_fee(&self) -> &BigUint {
782 &self.router_fee
783 }
784
785 pub fn client_fee(&self) -> &BigUint {
787 &self.client_fee
788 }
789
790 pub fn max_slippage(&self) -> &BigUint {
792 &self.max_slippage
793 }
794
795 pub fn min_amount_received(&self) -> &BigUint {
798 &self.min_amount_received
799 }
800
801 pub fn swaps_hash(&self) -> Option<&[u8; 32]> {
807 self.swaps_hash.as_ref()
808 }
809}
810
811#[derive(Debug, Clone)]
813pub struct Quote {
814 order_id: String,
815 status: QuoteStatus,
816 backend: BackendKind,
817 route: Option<Route>,
818 amount_in: BigUint,
819 amount_out: BigUint,
820 gas_estimate: BigUint,
821 amount_out_net_gas: BigUint,
822 price_impact_bps: Option<i32>,
823 block: BlockInfo,
824 token_out: Bytes,
827 receiver: Bytes,
831 transaction: Option<Transaction>,
834 fee_breakdown: Option<FeeBreakdown>,
836 pub(crate) solve_time_ms: u64,
839}
840
841impl Quote {
842 pub fn order_id(&self) -> &str {
844 &self.order_id
845 }
846
847 pub fn status(&self) -> QuoteStatus {
849 self.status
850 }
851
852 pub fn backend(&self) -> BackendKind {
854 self.backend
855 }
856
857 pub fn route(&self) -> Option<&Route> {
859 self.route.as_ref()
860 }
861
862 pub fn amount_in(&self) -> &BigUint {
864 &self.amount_in
865 }
866
867 pub fn amount_out(&self) -> &BigUint {
869 &self.amount_out
870 }
871
872 pub fn gas_estimate(&self) -> &BigUint {
874 &self.gas_estimate
875 }
876
877 pub fn amount_out_net_gas(&self) -> &BigUint {
882 &self.amount_out_net_gas
883 }
884
885 pub fn price_impact_bps(&self) -> Option<i32> {
887 self.price_impact_bps
888 }
889
890 pub fn block(&self) -> &BlockInfo {
892 &self.block
893 }
894
895 pub fn token_out(&self) -> &Bytes {
900 &self.token_out
901 }
902
903 pub fn receiver(&self) -> &Bytes {
910 &self.receiver
911 }
912
913 pub fn transaction(&self) -> Option<&Transaction> {
918 self.transaction.as_ref()
919 }
920
921 pub fn fee_breakdown(&self) -> Option<&FeeBreakdown> {
926 self.fee_breakdown.as_ref()
927 }
928
929 pub fn solve_time_ms(&self) -> u64 {
933 self.solve_time_ms
934 }
935
936 pub fn with_client_fee_signature(mut self, signature: &[u8]) -> Result<Self, FyndError> {
952 let tx = self
953 .transaction
954 .as_mut()
955 .ok_or_else(|| {
956 FyndError::Protocol("transaction required for signature patching".into())
957 })?;
958 let offset = tx
959 .client_fee_signature_offset()
960 .ok_or_else(|| {
961 FyndError::Protocol(
962 "client_fee_signature_offset required for signature patching".into(),
963 )
964 })?;
965 tx.data[offset..offset + signature.len()].copy_from_slice(signature);
966 Ok(self)
967 }
968
969 #[allow(clippy::too_many_arguments)]
971 pub fn new(
972 order_id: String,
973 status: QuoteStatus,
974 backend: BackendKind,
975 route: Option<Route>,
976 amount_in: BigUint,
977 amount_out: BigUint,
978 gas_estimate: BigUint,
979 amount_out_net_gas: BigUint,
980 price_impact_bps: Option<i32>,
981 block: BlockInfo,
982 token_out: Bytes,
983 receiver: Bytes,
984 transaction: Option<Transaction>,
985 fee_breakdown: Option<FeeBreakdown>,
986 ) -> Self {
987 Self {
988 order_id,
989 status,
990 backend,
991 route,
992 amount_in,
993 amount_out,
994 gas_estimate,
995 amount_out_net_gas,
996 price_impact_bps,
997 block,
998 token_out,
999 receiver,
1000 transaction,
1001 fee_breakdown,
1002 solve_time_ms: 0,
1003 }
1004 }
1005}
1006
1007#[derive(Debug, Clone)]
1009pub struct InstanceInfo {
1010 router_address: Option<bytes::Bytes>,
1012 permit2_address: bytes::Bytes,
1014 chain_id: u64,
1016 version: String,
1018}
1019
1020impl InstanceInfo {
1021 pub(crate) fn new(
1022 router_address: Option<bytes::Bytes>,
1023 permit2_address: bytes::Bytes,
1024 chain_id: u64,
1025 version: String,
1026 ) -> Self {
1027 Self { router_address, permit2_address, chain_id, version }
1028 }
1029
1030 pub fn router_address(&self) -> Option<&bytes::Bytes> {
1032 self.router_address.as_ref()
1033 }
1034
1035 pub fn permit2_address(&self) -> &bytes::Bytes {
1037 &self.permit2_address
1038 }
1039
1040 pub fn chain_id(&self) -> u64 {
1042 self.chain_id
1043 }
1044
1045 pub fn version(&self) -> &str {
1047 &self.version
1048 }
1049}
1050
1051#[derive(Debug, Clone)]
1053pub struct HealthStatus {
1054 healthy: bool,
1055 last_update_ms: u64,
1056 num_solver_pools: usize,
1057 derived_data_ready: bool,
1058 gas_price_age_ms: Option<u64>,
1059}
1060
1061impl HealthStatus {
1062 pub fn healthy(&self) -> bool {
1064 self.healthy
1065 }
1066
1067 pub fn last_update_ms(&self) -> u64 {
1069 self.last_update_ms
1070 }
1071
1072 pub fn num_solver_pools(&self) -> usize {
1074 self.num_solver_pools
1075 }
1076
1077 pub fn derived_data_ready(&self) -> bool {
1083 self.derived_data_ready
1084 }
1085
1086 pub fn gas_price_age_ms(&self) -> Option<u64> {
1088 self.gas_price_age_ms
1089 }
1090
1091 pub(crate) fn new(
1092 healthy: bool,
1093 last_update_ms: u64,
1094 num_solver_pools: usize,
1095 derived_data_ready: bool,
1096 gas_price_age_ms: Option<u64>,
1097 ) -> Self {
1098 Self { healthy, last_update_ms, num_solver_pools, derived_data_ready, gas_price_age_ms }
1099 }
1100}
1101
1102#[cfg(test)]
1103mod tests {
1104 use num_bigint::BigUint;
1105
1106 use super::*;
1107
1108 fn addr(bytes: &[u8; 20]) -> Bytes {
1109 Bytes::copy_from_slice(bytes)
1110 }
1111
1112 #[test]
1113 fn order_new_and_getters() {
1114 let token_in = addr(&[0xaa; 20]);
1115 let token_out = addr(&[0xbb; 20]);
1116 let amount = BigUint::from(1_000_000u64);
1117 let sender = addr(&[0xcc; 20]);
1118
1119 let order = Order::new(
1120 token_in.clone(),
1121 token_out.clone(),
1122 amount.clone(),
1123 OrderSide::Sell,
1124 sender.clone(),
1125 None,
1126 );
1127
1128 assert_eq!(order.token_in(), &token_in);
1129 assert_eq!(order.token_out(), &token_out);
1130 assert_eq!(order.amount(), &amount);
1131 assert_eq!(order.sender(), &sender);
1132 assert!(order.receiver().is_none());
1133 assert_eq!(order.side(), OrderSide::Sell);
1134 }
1135
1136 #[test]
1137 fn order_with_explicit_receiver() {
1138 let receiver = Bytes::copy_from_slice(&[0xdd; 20]);
1139 let order = Order::new(
1140 Bytes::copy_from_slice(&[0xaa; 20]),
1141 Bytes::copy_from_slice(&[0xbb; 20]),
1142 BigUint::from(1u32),
1143 OrderSide::Sell,
1144 Bytes::copy_from_slice(&[0xcc; 20]),
1145 Some(receiver.clone()),
1146 );
1147 assert_eq!(order.receiver(), Some(&receiver));
1148 }
1149
1150 #[test]
1151 fn quote_options_builder() {
1152 let opts = QuoteOptions::default()
1153 .with_timeout_ms(500)
1154 .with_min_responses(2)
1155 .with_max_gas(BigUint::from(1_000_000u64));
1156
1157 assert_eq!(opts.timeout_ms(), Some(500));
1158 assert_eq!(opts.min_responses(), Some(2));
1159 assert_eq!(opts.max_gas(), Some(&BigUint::from(1_000_000u64)));
1160 }
1161
1162 #[test]
1163 fn quote_options_default_all_none() {
1164 let opts = QuoteOptions::default();
1165 assert!(opts.timeout_ms().is_none());
1166 assert!(opts.min_responses().is_none());
1167 assert!(opts.max_gas().is_none());
1168 }
1169
1170 #[test]
1171 fn encoding_options_with_permit2_sets_fields() {
1172 let token = Bytes::copy_from_slice(&[0xaa; 20]);
1173 let spender = Bytes::copy_from_slice(&[0xbb; 20]);
1174 let sig = Bytes::copy_from_slice(&[0xcc; 65]);
1175 let details = PermitDetails::new(
1176 token,
1177 BigUint::from(1_000u32),
1178 BigUint::from(9_999_999u32),
1179 BigUint::from(0u32),
1180 );
1181 let permit = PermitSingle::new(details, spender, BigUint::from(9_999_999u32));
1182
1183 let opts = EncodingOptions::new(0.005)
1184 .with_permit2(permit, sig.clone())
1185 .unwrap();
1186
1187 assert_eq!(opts.transfer_type, UserTransferType::TransferFromPermit2);
1188 assert!(opts.permit.is_some());
1189 assert_eq!(opts.permit2_signature.as_ref().unwrap(), &sig);
1190 }
1191
1192 #[test]
1193 fn encoding_options_with_permit2_rejects_wrong_signature_length() {
1194 let details = PermitDetails::new(
1195 Bytes::copy_from_slice(&[0xaa; 20]),
1196 BigUint::from(1_000u32),
1197 BigUint::from(9_999_999u32),
1198 BigUint::from(0u32),
1199 );
1200 let permit = PermitSingle::new(
1201 details,
1202 Bytes::copy_from_slice(&[0xbb; 20]),
1203 BigUint::from(9_999_999u32),
1204 );
1205 let bad_sig = Bytes::copy_from_slice(&[0xcc; 64]); assert!(matches!(
1207 EncodingOptions::new(0.005).with_permit2(permit, bad_sig),
1208 Err(crate::error::FyndError::Protocol(_))
1209 ));
1210 }
1211
1212 #[test]
1213 fn encoding_options_with_vault_funds_sets_variant() {
1214 let opts = EncodingOptions::new(0.005).with_vault_funds();
1215 assert_eq!(opts.transfer_type, UserTransferType::UseVaultsFunds);
1216 assert!(opts.permit.is_none());
1217 assert!(opts.permit2_signature.is_none());
1218 }
1219
1220 fn sample_permit_single() -> PermitSingle {
1221 let details = PermitDetails::new(
1222 Bytes::copy_from_slice(&[0xaa; 20]),
1223 BigUint::from(1_000u32),
1224 BigUint::from(9_999_999u32),
1225 BigUint::from(0u32),
1226 );
1227 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(9_999_999u32))
1228 }
1229
1230 #[test]
1231 fn eip712_signing_hash_returns_32_bytes() {
1232 let permit = sample_permit_single();
1233 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1234 let hash = permit
1235 .eip712_signing_hash(1, &permit2_addr)
1236 .unwrap();
1237 assert_eq!(hash.len(), 32);
1238 assert_ne!(hash, [0u8; 32]);
1240 }
1241
1242 #[test]
1243 fn eip712_signing_hash_is_deterministic() {
1244 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1245 let h1 = sample_permit_single()
1246 .eip712_signing_hash(1, &permit2_addr)
1247 .unwrap();
1248 let h2 = sample_permit_single()
1249 .eip712_signing_hash(1, &permit2_addr)
1250 .unwrap();
1251 assert_eq!(h1, h2);
1252 }
1253
1254 #[test]
1255 fn eip712_signing_hash_differs_by_chain_id() {
1256 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1257 let h1 = sample_permit_single()
1258 .eip712_signing_hash(1, &permit2_addr)
1259 .unwrap();
1260 let h137 = sample_permit_single()
1261 .eip712_signing_hash(137, &permit2_addr)
1262 .unwrap();
1263 assert_ne!(h1, h137);
1264 }
1265
1266 #[test]
1267 fn eip712_signing_hash_invalid_permit2_address() {
1268 let permit = sample_permit_single();
1269 let bad_addr = Bytes::copy_from_slice(&[0xcc; 4]);
1270 assert!(matches!(
1271 permit.eip712_signing_hash(1, &bad_addr),
1272 Err(crate::error::FyndError::Protocol(_))
1273 ));
1274 }
1275
1276 #[test]
1277 fn eip712_signing_hash_invalid_token_address() {
1278 let details = PermitDetails::new(
1279 Bytes::copy_from_slice(&[0xaa; 4]), BigUint::from(1u32),
1281 BigUint::from(1u32),
1282 BigUint::from(0u32),
1283 );
1284 let permit =
1285 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(1u32));
1286 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1287 assert!(matches!(
1288 permit.eip712_signing_hash(1, &permit2_addr),
1289 Err(crate::error::FyndError::Protocol(_))
1290 ));
1291 }
1292
1293 #[test]
1294 fn eip712_signing_hash_amount_exceeds_uint160() {
1295 let oversized_amount = BigUint::from_bytes_be(&[0x01; 21]);
1297 let details = PermitDetails::new(
1298 Bytes::copy_from_slice(&[0xaa; 20]),
1299 oversized_amount,
1300 BigUint::from(1u32),
1301 BigUint::from(0u32),
1302 );
1303 let permit =
1304 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(1u32));
1305 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1306 assert!(matches!(
1307 permit.eip712_signing_hash(1, &permit2_addr),
1308 Err(crate::error::FyndError::Protocol(_))
1309 ));
1310 }
1311
1312 fn sample_fee_receiver() -> Bytes {
1317 Bytes::copy_from_slice(&[0x44; 20])
1318 }
1319
1320 fn sample_router_address() -> Bytes {
1321 Bytes::copy_from_slice(&[0x33; 20])
1322 }
1323
1324 fn sample_fee_params(bps: u16, receiver: Bytes) -> ClientFeeParams {
1325 ClientFeeParams::new(bps, receiver, BigUint::ZERO, 1_893_456_000)
1326 }
1327
1328 fn sample_token_in() -> Bytes {
1329 Bytes::copy_from_slice(&[0x11; 20])
1330 }
1331
1332 fn sample_token_out() -> Bytes {
1333 Bytes::copy_from_slice(&[0x22; 20])
1334 }
1335
1336 fn sample_swap_receiver() -> Bytes {
1337 Bytes::copy_from_slice(&[0xAA; 20])
1338 }
1339
1340 fn sample_min_amount_out() -> BigUint {
1341 BigUint::from(1_000_000u64)
1342 }
1343
1344 fn sample_amount_in() -> BigUint {
1345 BigUint::from(1_000_000_000_000_000_000u64)
1346 }
1347
1348 fn sample_swaps_hash() -> [u8; 32] {
1349 [0xAB; 32]
1350 }
1351
1352 #[test]
1353 fn client_fee_with_client_fee_sets_fields() {
1354 let fee = ClientFeeParams::new(
1355 100,
1356 sample_fee_receiver(),
1357 BigUint::from(500_000u64),
1358 1_893_456_000,
1359 );
1360 let opts = EncodingOptions::new(0.01).with_client_fee(fee);
1361 assert!(opts.client_fee_params.is_some());
1362 let stored = opts.client_fee_params.as_ref().unwrap();
1363 assert_eq!(stored.bps, 100);
1364 assert_eq!(stored.max_contribution, BigUint::from(500_000u64));
1365 }
1366
1367 #[test]
1368 fn client_fee_signing_hash_returns_32_bytes() {
1369 let fee = sample_fee_params(100, sample_fee_receiver());
1370 let hash = fee
1371 .eip712_signing_hash(
1372 1,
1373 &sample_router_address(),
1374 &sample_amount_in(),
1375 &sample_token_in(),
1376 &sample_token_out(),
1377 &sample_min_amount_out(),
1378 &sample_swap_receiver(),
1379 &sample_swaps_hash(),
1380 )
1381 .unwrap();
1382 assert_eq!(hash.len(), 32);
1383 assert_ne!(hash, [0u8; 32]);
1384 }
1385
1386 #[test]
1387 fn client_fee_signing_hash_is_deterministic() {
1388 let fee = sample_fee_params(100, sample_fee_receiver());
1389 let h1 = fee
1390 .eip712_signing_hash(
1391 1,
1392 &sample_router_address(),
1393 &sample_amount_in(),
1394 &sample_token_in(),
1395 &sample_token_out(),
1396 &sample_min_amount_out(),
1397 &sample_swap_receiver(),
1398 &sample_swaps_hash(),
1399 )
1400 .unwrap();
1401 let h2 = fee
1402 .eip712_signing_hash(
1403 1,
1404 &sample_router_address(),
1405 &sample_amount_in(),
1406 &sample_token_in(),
1407 &sample_token_out(),
1408 &sample_min_amount_out(),
1409 &sample_swap_receiver(),
1410 &sample_swaps_hash(),
1411 )
1412 .unwrap();
1413 assert_eq!(h1, h2);
1414 }
1415
1416 #[test]
1417 fn client_fee_signing_hash_differs_by_chain_id() {
1418 let fee = sample_fee_params(100, sample_fee_receiver());
1419 let h1 = fee
1420 .eip712_signing_hash(
1421 1,
1422 &sample_router_address(),
1423 &sample_amount_in(),
1424 &sample_token_in(),
1425 &sample_token_out(),
1426 &sample_min_amount_out(),
1427 &sample_swap_receiver(),
1428 &sample_swaps_hash(),
1429 )
1430 .unwrap();
1431 let h137 = fee
1432 .eip712_signing_hash(
1433 137,
1434 &sample_router_address(),
1435 &sample_amount_in(),
1436 &sample_token_in(),
1437 &sample_token_out(),
1438 &sample_min_amount_out(),
1439 &sample_swap_receiver(),
1440 &sample_swaps_hash(),
1441 )
1442 .unwrap();
1443 assert_ne!(h1, h137);
1444 }
1445
1446 #[test]
1447 fn client_fee_signing_hash_differs_by_bps() {
1448 let h100 = sample_fee_params(100, sample_fee_receiver())
1449 .eip712_signing_hash(
1450 1,
1451 &sample_router_address(),
1452 &sample_amount_in(),
1453 &sample_token_in(),
1454 &sample_token_out(),
1455 &sample_min_amount_out(),
1456 &sample_swap_receiver(),
1457 &sample_swaps_hash(),
1458 )
1459 .unwrap();
1460 let h200 = sample_fee_params(200, sample_fee_receiver())
1461 .eip712_signing_hash(
1462 1,
1463 &sample_router_address(),
1464 &sample_amount_in(),
1465 &sample_token_in(),
1466 &sample_token_out(),
1467 &sample_min_amount_out(),
1468 &sample_swap_receiver(),
1469 &sample_swaps_hash(),
1470 )
1471 .unwrap();
1472 assert_ne!(h100, h200);
1473 }
1474
1475 #[test]
1476 fn client_fee_signing_hash_differs_by_receiver() {
1477 let other_receiver = Bytes::copy_from_slice(&[0x55; 20]);
1478 let h1 = sample_fee_params(100, sample_fee_receiver())
1479 .eip712_signing_hash(
1480 1,
1481 &sample_router_address(),
1482 &sample_amount_in(),
1483 &sample_token_in(),
1484 &sample_token_out(),
1485 &sample_min_amount_out(),
1486 &sample_swap_receiver(),
1487 &sample_swaps_hash(),
1488 )
1489 .unwrap();
1490 let h2 = sample_fee_params(100, other_receiver)
1491 .eip712_signing_hash(
1492 1,
1493 &sample_router_address(),
1494 &sample_amount_in(),
1495 &sample_token_in(),
1496 &sample_token_out(),
1497 &sample_min_amount_out(),
1498 &sample_swap_receiver(),
1499 &sample_swaps_hash(),
1500 )
1501 .unwrap();
1502 assert_ne!(h1, h2);
1503 }
1504
1505 #[test]
1506 fn client_fee_signing_hash_rejects_bad_receiver_address() {
1507 let bad_addr = Bytes::copy_from_slice(&[0x44; 4]);
1508 let fee = sample_fee_params(100, bad_addr);
1509 assert!(matches!(
1510 fee.eip712_signing_hash(
1511 1,
1512 &sample_router_address(),
1513 &sample_amount_in(),
1514 &sample_token_in(),
1515 &sample_token_out(),
1516 &sample_min_amount_out(),
1517 &sample_swap_receiver(),
1518 &sample_swaps_hash(),
1519 ),
1520 Err(crate::error::FyndError::Protocol(_))
1521 ));
1522 }
1523
1524 #[test]
1525 fn client_fee_signing_hash_rejects_bad_router_address() {
1526 let bad_addr = Bytes::copy_from_slice(&[0x33; 4]);
1527 let fee = sample_fee_params(100, sample_fee_receiver());
1528 assert!(matches!(
1529 fee.eip712_signing_hash(
1530 1,
1531 &bad_addr,
1532 &sample_amount_in(),
1533 &sample_token_in(),
1534 &sample_token_out(),
1535 &sample_min_amount_out(),
1536 &sample_swap_receiver(),
1537 &sample_swaps_hash(),
1538 ),
1539 Err(crate::error::FyndError::Protocol(_))
1540 ));
1541 }
1542}