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: bytes::Bytes,
1012 permit2_address: bytes::Bytes,
1014 chain_id: u64,
1016}
1017
1018impl InstanceInfo {
1019 pub(crate) fn new(
1020 router_address: bytes::Bytes,
1021 permit2_address: bytes::Bytes,
1022 chain_id: u64,
1023 ) -> Self {
1024 Self { router_address, permit2_address, chain_id }
1025 }
1026
1027 pub fn router_address(&self) -> &bytes::Bytes {
1029 &self.router_address
1030 }
1031
1032 pub fn permit2_address(&self) -> &bytes::Bytes {
1034 &self.permit2_address
1035 }
1036
1037 pub fn chain_id(&self) -> u64 {
1039 self.chain_id
1040 }
1041}
1042
1043#[derive(Debug, Clone)]
1045pub struct HealthStatus {
1046 healthy: bool,
1047 last_update_ms: u64,
1048 num_solver_pools: usize,
1049 derived_data_ready: bool,
1050 gas_price_age_ms: Option<u64>,
1051}
1052
1053impl HealthStatus {
1054 pub fn healthy(&self) -> bool {
1056 self.healthy
1057 }
1058
1059 pub fn last_update_ms(&self) -> u64 {
1061 self.last_update_ms
1062 }
1063
1064 pub fn num_solver_pools(&self) -> usize {
1066 self.num_solver_pools
1067 }
1068
1069 pub fn derived_data_ready(&self) -> bool {
1075 self.derived_data_ready
1076 }
1077
1078 pub fn gas_price_age_ms(&self) -> Option<u64> {
1080 self.gas_price_age_ms
1081 }
1082
1083 pub(crate) fn new(
1084 healthy: bool,
1085 last_update_ms: u64,
1086 num_solver_pools: usize,
1087 derived_data_ready: bool,
1088 gas_price_age_ms: Option<u64>,
1089 ) -> Self {
1090 Self { healthy, last_update_ms, num_solver_pools, derived_data_ready, gas_price_age_ms }
1091 }
1092}
1093
1094#[cfg(test)]
1095mod tests {
1096 use num_bigint::BigUint;
1097
1098 use super::*;
1099
1100 fn addr(bytes: &[u8; 20]) -> Bytes {
1101 Bytes::copy_from_slice(bytes)
1102 }
1103
1104 #[test]
1105 fn order_new_and_getters() {
1106 let token_in = addr(&[0xaa; 20]);
1107 let token_out = addr(&[0xbb; 20]);
1108 let amount = BigUint::from(1_000_000u64);
1109 let sender = addr(&[0xcc; 20]);
1110
1111 let order = Order::new(
1112 token_in.clone(),
1113 token_out.clone(),
1114 amount.clone(),
1115 OrderSide::Sell,
1116 sender.clone(),
1117 None,
1118 );
1119
1120 assert_eq!(order.token_in(), &token_in);
1121 assert_eq!(order.token_out(), &token_out);
1122 assert_eq!(order.amount(), &amount);
1123 assert_eq!(order.sender(), &sender);
1124 assert!(order.receiver().is_none());
1125 assert_eq!(order.side(), OrderSide::Sell);
1126 }
1127
1128 #[test]
1129 fn order_with_explicit_receiver() {
1130 let receiver = Bytes::copy_from_slice(&[0xdd; 20]);
1131 let order = Order::new(
1132 Bytes::copy_from_slice(&[0xaa; 20]),
1133 Bytes::copy_from_slice(&[0xbb; 20]),
1134 BigUint::from(1u32),
1135 OrderSide::Sell,
1136 Bytes::copy_from_slice(&[0xcc; 20]),
1137 Some(receiver.clone()),
1138 );
1139 assert_eq!(order.receiver(), Some(&receiver));
1140 }
1141
1142 #[test]
1143 fn quote_options_builder() {
1144 let opts = QuoteOptions::default()
1145 .with_timeout_ms(500)
1146 .with_min_responses(2)
1147 .with_max_gas(BigUint::from(1_000_000u64));
1148
1149 assert_eq!(opts.timeout_ms(), Some(500));
1150 assert_eq!(opts.min_responses(), Some(2));
1151 assert_eq!(opts.max_gas(), Some(&BigUint::from(1_000_000u64)));
1152 }
1153
1154 #[test]
1155 fn quote_options_default_all_none() {
1156 let opts = QuoteOptions::default();
1157 assert!(opts.timeout_ms().is_none());
1158 assert!(opts.min_responses().is_none());
1159 assert!(opts.max_gas().is_none());
1160 }
1161
1162 #[test]
1163 fn encoding_options_with_permit2_sets_fields() {
1164 let token = Bytes::copy_from_slice(&[0xaa; 20]);
1165 let spender = Bytes::copy_from_slice(&[0xbb; 20]);
1166 let sig = Bytes::copy_from_slice(&[0xcc; 65]);
1167 let details = PermitDetails::new(
1168 token,
1169 BigUint::from(1_000u32),
1170 BigUint::from(9_999_999u32),
1171 BigUint::from(0u32),
1172 );
1173 let permit = PermitSingle::new(details, spender, BigUint::from(9_999_999u32));
1174
1175 let opts = EncodingOptions::new(0.005)
1176 .with_permit2(permit, sig.clone())
1177 .unwrap();
1178
1179 assert_eq!(opts.transfer_type, UserTransferType::TransferFromPermit2);
1180 assert!(opts.permit.is_some());
1181 assert_eq!(opts.permit2_signature.as_ref().unwrap(), &sig);
1182 }
1183
1184 #[test]
1185 fn encoding_options_with_permit2_rejects_wrong_signature_length() {
1186 let details = PermitDetails::new(
1187 Bytes::copy_from_slice(&[0xaa; 20]),
1188 BigUint::from(1_000u32),
1189 BigUint::from(9_999_999u32),
1190 BigUint::from(0u32),
1191 );
1192 let permit = PermitSingle::new(
1193 details,
1194 Bytes::copy_from_slice(&[0xbb; 20]),
1195 BigUint::from(9_999_999u32),
1196 );
1197 let bad_sig = Bytes::copy_from_slice(&[0xcc; 64]); assert!(matches!(
1199 EncodingOptions::new(0.005).with_permit2(permit, bad_sig),
1200 Err(crate::error::FyndError::Protocol(_))
1201 ));
1202 }
1203
1204 #[test]
1205 fn encoding_options_with_vault_funds_sets_variant() {
1206 let opts = EncodingOptions::new(0.005).with_vault_funds();
1207 assert_eq!(opts.transfer_type, UserTransferType::UseVaultsFunds);
1208 assert!(opts.permit.is_none());
1209 assert!(opts.permit2_signature.is_none());
1210 }
1211
1212 fn sample_permit_single() -> PermitSingle {
1213 let details = PermitDetails::new(
1214 Bytes::copy_from_slice(&[0xaa; 20]),
1215 BigUint::from(1_000u32),
1216 BigUint::from(9_999_999u32),
1217 BigUint::from(0u32),
1218 );
1219 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(9_999_999u32))
1220 }
1221
1222 #[test]
1223 fn eip712_signing_hash_returns_32_bytes() {
1224 let permit = sample_permit_single();
1225 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1226 let hash = permit
1227 .eip712_signing_hash(1, &permit2_addr)
1228 .unwrap();
1229 assert_eq!(hash.len(), 32);
1230 assert_ne!(hash, [0u8; 32]);
1232 }
1233
1234 #[test]
1235 fn eip712_signing_hash_is_deterministic() {
1236 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1237 let h1 = sample_permit_single()
1238 .eip712_signing_hash(1, &permit2_addr)
1239 .unwrap();
1240 let h2 = sample_permit_single()
1241 .eip712_signing_hash(1, &permit2_addr)
1242 .unwrap();
1243 assert_eq!(h1, h2);
1244 }
1245
1246 #[test]
1247 fn eip712_signing_hash_differs_by_chain_id() {
1248 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1249 let h1 = sample_permit_single()
1250 .eip712_signing_hash(1, &permit2_addr)
1251 .unwrap();
1252 let h137 = sample_permit_single()
1253 .eip712_signing_hash(137, &permit2_addr)
1254 .unwrap();
1255 assert_ne!(h1, h137);
1256 }
1257
1258 #[test]
1259 fn eip712_signing_hash_invalid_permit2_address() {
1260 let permit = sample_permit_single();
1261 let bad_addr = Bytes::copy_from_slice(&[0xcc; 4]);
1262 assert!(matches!(
1263 permit.eip712_signing_hash(1, &bad_addr),
1264 Err(crate::error::FyndError::Protocol(_))
1265 ));
1266 }
1267
1268 #[test]
1269 fn eip712_signing_hash_invalid_token_address() {
1270 let details = PermitDetails::new(
1271 Bytes::copy_from_slice(&[0xaa; 4]), BigUint::from(1u32),
1273 BigUint::from(1u32),
1274 BigUint::from(0u32),
1275 );
1276 let permit =
1277 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(1u32));
1278 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1279 assert!(matches!(
1280 permit.eip712_signing_hash(1, &permit2_addr),
1281 Err(crate::error::FyndError::Protocol(_))
1282 ));
1283 }
1284
1285 #[test]
1286 fn eip712_signing_hash_amount_exceeds_uint160() {
1287 let oversized_amount = BigUint::from_bytes_be(&[0x01; 21]);
1289 let details = PermitDetails::new(
1290 Bytes::copy_from_slice(&[0xaa; 20]),
1291 oversized_amount,
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 fn sample_fee_receiver() -> Bytes {
1309 Bytes::copy_from_slice(&[0x44; 20])
1310 }
1311
1312 fn sample_router_address() -> Bytes {
1313 Bytes::copy_from_slice(&[0x33; 20])
1314 }
1315
1316 fn sample_fee_params(bps: u16, receiver: Bytes) -> ClientFeeParams {
1317 ClientFeeParams::new(bps, receiver, BigUint::ZERO, 1_893_456_000)
1318 }
1319
1320 fn sample_token_in() -> Bytes {
1321 Bytes::copy_from_slice(&[0x11; 20])
1322 }
1323
1324 fn sample_token_out() -> Bytes {
1325 Bytes::copy_from_slice(&[0x22; 20])
1326 }
1327
1328 fn sample_swap_receiver() -> Bytes {
1329 Bytes::copy_from_slice(&[0xAA; 20])
1330 }
1331
1332 fn sample_min_amount_out() -> BigUint {
1333 BigUint::from(1_000_000u64)
1334 }
1335
1336 fn sample_amount_in() -> BigUint {
1337 BigUint::from(1_000_000_000_000_000_000u64)
1338 }
1339
1340 fn sample_swaps_hash() -> [u8; 32] {
1341 [0xAB; 32]
1342 }
1343
1344 #[test]
1345 fn client_fee_with_client_fee_sets_fields() {
1346 let fee = ClientFeeParams::new(
1347 100,
1348 sample_fee_receiver(),
1349 BigUint::from(500_000u64),
1350 1_893_456_000,
1351 );
1352 let opts = EncodingOptions::new(0.01).with_client_fee(fee);
1353 assert!(opts.client_fee_params.is_some());
1354 let stored = opts.client_fee_params.as_ref().unwrap();
1355 assert_eq!(stored.bps, 100);
1356 assert_eq!(stored.max_contribution, BigUint::from(500_000u64));
1357 }
1358
1359 #[test]
1360 fn client_fee_signing_hash_returns_32_bytes() {
1361 let fee = sample_fee_params(100, sample_fee_receiver());
1362 let hash = fee
1363 .eip712_signing_hash(
1364 1,
1365 &sample_router_address(),
1366 &sample_amount_in(),
1367 &sample_token_in(),
1368 &sample_token_out(),
1369 &sample_min_amount_out(),
1370 &sample_swap_receiver(),
1371 &sample_swaps_hash(),
1372 )
1373 .unwrap();
1374 assert_eq!(hash.len(), 32);
1375 assert_ne!(hash, [0u8; 32]);
1376 }
1377
1378 #[test]
1379 fn client_fee_signing_hash_is_deterministic() {
1380 let fee = sample_fee_params(100, sample_fee_receiver());
1381 let h1 = fee
1382 .eip712_signing_hash(
1383 1,
1384 &sample_router_address(),
1385 &sample_amount_in(),
1386 &sample_token_in(),
1387 &sample_token_out(),
1388 &sample_min_amount_out(),
1389 &sample_swap_receiver(),
1390 &sample_swaps_hash(),
1391 )
1392 .unwrap();
1393 let h2 = fee
1394 .eip712_signing_hash(
1395 1,
1396 &sample_router_address(),
1397 &sample_amount_in(),
1398 &sample_token_in(),
1399 &sample_token_out(),
1400 &sample_min_amount_out(),
1401 &sample_swap_receiver(),
1402 &sample_swaps_hash(),
1403 )
1404 .unwrap();
1405 assert_eq!(h1, h2);
1406 }
1407
1408 #[test]
1409 fn client_fee_signing_hash_differs_by_chain_id() {
1410 let fee = sample_fee_params(100, sample_fee_receiver());
1411 let h1 = fee
1412 .eip712_signing_hash(
1413 1,
1414 &sample_router_address(),
1415 &sample_amount_in(),
1416 &sample_token_in(),
1417 &sample_token_out(),
1418 &sample_min_amount_out(),
1419 &sample_swap_receiver(),
1420 &sample_swaps_hash(),
1421 )
1422 .unwrap();
1423 let h137 = fee
1424 .eip712_signing_hash(
1425 137,
1426 &sample_router_address(),
1427 &sample_amount_in(),
1428 &sample_token_in(),
1429 &sample_token_out(),
1430 &sample_min_amount_out(),
1431 &sample_swap_receiver(),
1432 &sample_swaps_hash(),
1433 )
1434 .unwrap();
1435 assert_ne!(h1, h137);
1436 }
1437
1438 #[test]
1439 fn client_fee_signing_hash_differs_by_bps() {
1440 let h100 = sample_fee_params(100, sample_fee_receiver())
1441 .eip712_signing_hash(
1442 1,
1443 &sample_router_address(),
1444 &sample_amount_in(),
1445 &sample_token_in(),
1446 &sample_token_out(),
1447 &sample_min_amount_out(),
1448 &sample_swap_receiver(),
1449 &sample_swaps_hash(),
1450 )
1451 .unwrap();
1452 let h200 = sample_fee_params(200, sample_fee_receiver())
1453 .eip712_signing_hash(
1454 1,
1455 &sample_router_address(),
1456 &sample_amount_in(),
1457 &sample_token_in(),
1458 &sample_token_out(),
1459 &sample_min_amount_out(),
1460 &sample_swap_receiver(),
1461 &sample_swaps_hash(),
1462 )
1463 .unwrap();
1464 assert_ne!(h100, h200);
1465 }
1466
1467 #[test]
1468 fn client_fee_signing_hash_differs_by_receiver() {
1469 let other_receiver = Bytes::copy_from_slice(&[0x55; 20]);
1470 let h1 = sample_fee_params(100, sample_fee_receiver())
1471 .eip712_signing_hash(
1472 1,
1473 &sample_router_address(),
1474 &sample_amount_in(),
1475 &sample_token_in(),
1476 &sample_token_out(),
1477 &sample_min_amount_out(),
1478 &sample_swap_receiver(),
1479 &sample_swaps_hash(),
1480 )
1481 .unwrap();
1482 let h2 = sample_fee_params(100, other_receiver)
1483 .eip712_signing_hash(
1484 1,
1485 &sample_router_address(),
1486 &sample_amount_in(),
1487 &sample_token_in(),
1488 &sample_token_out(),
1489 &sample_min_amount_out(),
1490 &sample_swap_receiver(),
1491 &sample_swaps_hash(),
1492 )
1493 .unwrap();
1494 assert_ne!(h1, h2);
1495 }
1496
1497 #[test]
1498 fn client_fee_signing_hash_rejects_bad_receiver_address() {
1499 let bad_addr = Bytes::copy_from_slice(&[0x44; 4]);
1500 let fee = sample_fee_params(100, bad_addr);
1501 assert!(matches!(
1502 fee.eip712_signing_hash(
1503 1,
1504 &sample_router_address(),
1505 &sample_amount_in(),
1506 &sample_token_in(),
1507 &sample_token_out(),
1508 &sample_min_amount_out(),
1509 &sample_swap_receiver(),
1510 &sample_swaps_hash(),
1511 ),
1512 Err(crate::error::FyndError::Protocol(_))
1513 ));
1514 }
1515
1516 #[test]
1517 fn client_fee_signing_hash_rejects_bad_router_address() {
1518 let bad_addr = Bytes::copy_from_slice(&[0x33; 4]);
1519 let fee = sample_fee_params(100, sample_fee_receiver());
1520 assert!(matches!(
1521 fee.eip712_signing_hash(
1522 1,
1523 &bad_addr,
1524 &sample_amount_in(),
1525 &sample_token_in(),
1526 &sample_token_out(),
1527 &sample_min_amount_out(),
1528 &sample_swap_receiver(),
1529 &sample_swaps_hash(),
1530 ),
1531 Err(crate::error::FyndError::Protocol(_))
1532 ));
1533 }
1534}