1use alloy::{
2 primitives::{keccak256, U256},
3 sol_types::SolValue,
4};
5use bytes::Bytes;
6use num_bigint::BigUint;
7
8use crate::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(
37 token: bytes::Bytes,
38 amount: num_bigint::BigUint,
39 expiration: num_bigint::BigUint,
40 nonce: num_bigint::BigUint,
41 ) -> Self {
42 Self { token, amount, expiration, nonce }
43 }
44}
45
46#[derive(Debug, Clone)]
48pub struct PermitSingle {
49 pub(crate) details: PermitDetails,
50 pub(crate) spender: bytes::Bytes,
51 pub(crate) sig_deadline: num_bigint::BigUint,
52}
53
54impl PermitSingle {
55 pub fn new(
56 details: PermitDetails,
57 spender: bytes::Bytes,
58 sig_deadline: num_bigint::BigUint,
59 ) -> Self {
60 Self { details, spender, sig_deadline }
61 }
62
63 pub fn eip712_signing_hash(
76 &self,
77 chain_id: u64,
78 permit2_address: &bytes::Bytes,
79 ) -> Result<[u8; 32], crate::error::FyndError> {
80 use alloy::sol_types::{eip712_domain, SolStruct};
81
82 let permit2_addr = p2_bytes_to_address(permit2_address, "permit2_address")?;
83 let token = p2_bytes_to_address(&self.details.token, "token")?;
84 let spender = p2_bytes_to_address(&self.spender, "spender")?;
85
86 let amount = p2_biguint_to_uint160(&self.details.amount)?;
87 let expiration = p2_biguint_to_uint48(&self.details.expiration)?;
88 let nonce = p2_biguint_to_uint48(&self.details.nonce)?;
89 let sig_deadline = crate::mapping::biguint_to_u256(&self.sig_deadline);
90
91 let domain = eip712_domain! {
92 name: "Permit2",
93 chain_id: chain_id,
94 verifying_contract: permit2_addr,
95 };
96 #[allow(non_snake_case)]
97 let permit = permit2_sol::PermitSingle {
98 details: permit2_sol::PermitDetails { token, amount, expiration, nonce },
99 spender,
100 sigDeadline: sig_deadline,
101 };
102 Ok(permit.eip712_signing_hash(&domain).0)
103 }
104}
105
106#[derive(Debug, Clone)]
113pub struct ClientFeeParams {
114 pub(crate) bps: u16,
115 pub(crate) receiver: Bytes,
116 pub(crate) max_contribution: BigUint,
117 pub(crate) deadline: u64,
118 pub(crate) signature: Option<Bytes>,
119}
120
121impl ClientFeeParams {
122 pub fn new(bps: u16, receiver: Bytes, max_contribution: BigUint, deadline: u64) -> Self {
126 Self { bps, receiver, max_contribution, deadline, signature: None }
127 }
128
129 pub fn with_signature(mut self, signature: Bytes) -> Self {
131 self.signature = Some(signature);
132 self
133 }
134
135 pub fn eip712_signing_hash(
142 &self,
143 chain_id: u64,
144 router_address: &Bytes,
145 ) -> Result<[u8; 32], crate::error::FyndError> {
146 let router_addr = p2_bytes_to_address(router_address, "router_address")?;
147 let fee_receiver = p2_bytes_to_address(&self.receiver, "receiver")?;
148 let max_contrib = biguint_to_u256(&self.max_contribution);
149 let dl = U256::from(self.deadline);
150
151 let type_hash = keccak256(
152 b"ClientFee(uint16 clientFeeBps,address clientFeeReceiver,\
153uint256 maxClientContribution,uint256 deadline)",
154 );
155
156 let domain_type_hash = keccak256(
157 b"EIP712Domain(string name,string version,\
158uint256 chainId,address verifyingContract)",
159 );
160 let domain_separator = keccak256(
161 (
162 domain_type_hash,
163 keccak256(b"TychoRouter"),
164 keccak256(b"1"),
165 U256::from(chain_id),
166 router_addr,
167 )
168 .abi_encode(),
169 );
170
171 let struct_hash = keccak256(
172 (type_hash, U256::from(self.bps), fee_receiver, max_contrib, dl).abi_encode(),
173 );
174
175 let mut data = [0u8; 66];
176 data[0] = 0x19;
177 data[1] = 0x01;
178 data[2..34].copy_from_slice(domain_separator.as_ref());
179 data[34..66].copy_from_slice(struct_hash.as_ref());
180 Ok(keccak256(data).0)
181 }
182}
183
184mod permit2_sol {
189 use alloy::sol;
190
191 sol! {
192 struct PermitDetails {
193 address token;
194 uint160 amount;
195 uint48 expiration;
196 uint48 nonce;
197 }
198 struct PermitSingle {
199 PermitDetails details;
200 address spender;
201 uint256 sigDeadline;
202 }
203 }
204}
205
206fn p2_bytes_to_address(
207 b: &bytes::Bytes,
208 field: &str,
209) -> Result<alloy::primitives::Address, crate::error::FyndError> {
210 let arr: [u8; 20] = b.as_ref().try_into().map_err(|_| {
211 crate::error::FyndError::Protocol(format!(
212 "expected 20-byte address for {field}, got {} bytes",
213 b.len()
214 ))
215 })?;
216 Ok(alloy::primitives::Address::from(arr))
217}
218
219fn p2_biguint_to_uint160(
220 n: &num_bigint::BigUint,
221) -> Result<alloy::primitives::Uint<160, 3>, crate::error::FyndError> {
222 let bytes = n.to_bytes_be();
223 if bytes.len() > 20 {
224 return Err(crate::error::FyndError::Protocol(format!(
225 "permit amount exceeds uint160 ({} bytes)",
226 bytes.len()
227 )));
228 }
229 let mut arr = [0u8; 20];
230 arr[20 - bytes.len()..].copy_from_slice(&bytes);
231 Ok(alloy::primitives::Uint::<160, 3>::from_be_bytes(arr))
232}
233
234fn p2_biguint_to_uint48(
235 n: &num_bigint::BigUint,
236) -> Result<alloy::primitives::Uint<48, 1>, crate::error::FyndError> {
237 let bytes = n.to_bytes_be();
238 if bytes.len() > 6 {
239 return Err(crate::error::FyndError::Protocol(format!(
240 "permit value exceeds uint48 ({} bytes)",
241 bytes.len()
242 )));
243 }
244 let mut arr = [0u8; 6];
245 arr[6 - bytes.len()..].copy_from_slice(&bytes);
246 Ok(alloy::primitives::Uint::<48, 1>::from_be_bytes(arr))
247}
248
249#[derive(Debug, Clone)]
254pub struct EncodingOptions {
255 pub(crate) slippage: f64,
256 pub(crate) transfer_type: UserTransferType,
257 pub(crate) permit: Option<PermitSingle>,
258 pub(crate) permit2_signature: Option<Bytes>,
259 pub(crate) client_fee_params: Option<ClientFeeParams>,
260}
261
262impl EncodingOptions {
263 pub fn new(slippage: f64) -> Self {
268 Self {
269 slippage,
270 transfer_type: UserTransferType::TransferFrom,
271 permit: None,
272 permit2_signature: None,
273 client_fee_params: None,
274 }
275 }
276
277 pub fn with_permit2(
286 mut self,
287 permit: PermitSingle,
288 signature: bytes::Bytes,
289 ) -> Result<Self, crate::error::FyndError> {
290 if signature.len() != 65 {
291 return Err(crate::error::FyndError::Protocol(format!(
292 "Permit2 signature must be exactly 65 bytes, got {}",
293 signature.len()
294 )));
295 }
296 self.transfer_type = UserTransferType::TransferFromPermit2;
297 self.permit = Some(permit);
298 self.permit2_signature = Some(signature);
299 Ok(self)
300 }
301
302 pub fn with_vault_funds(mut self) -> Self {
304 self.transfer_type = UserTransferType::UseVaultsFunds;
305 self
306 }
307
308 pub fn with_client_fee(mut self, params: ClientFeeParams) -> Self {
310 self.client_fee_params = Some(params);
311 self
312 }
313}
314
315#[derive(Debug, Clone)]
319pub struct Transaction {
320 to: Bytes,
321 value: BigUint,
322 data: Vec<u8>,
323}
324
325impl Transaction {
326 pub(crate) fn new(to: Bytes, value: BigUint, data: Vec<u8>) -> Self {
327 Self { to, value, data }
328 }
329
330 pub fn to(&self) -> &Bytes {
332 &self.to
333 }
334
335 pub fn value(&self) -> &BigUint {
337 &self.value
338 }
339
340 pub fn data(&self) -> &[u8] {
342 &self.data
343 }
344}
345
346#[derive(Debug, Clone, Copy, PartialEq, Eq)]
354pub enum OrderSide {
355 Sell,
357}
358
359pub struct Order {
368 token_in: Bytes,
369 token_out: Bytes,
370 amount: BigUint,
371 side: OrderSide,
372 sender: Bytes,
373 receiver: Option<Bytes>,
374}
375
376impl Order {
377 pub fn new(
386 token_in: Bytes,
387 token_out: Bytes,
388 amount: BigUint,
389 side: OrderSide,
390 sender: Bytes,
391 receiver: Option<Bytes>,
392 ) -> Self {
393 Self { token_in, token_out, amount, side, sender, receiver }
394 }
395
396 pub fn token_in(&self) -> &Bytes {
398 &self.token_in
399 }
400
401 pub fn token_out(&self) -> &Bytes {
403 &self.token_out
404 }
405
406 pub fn amount(&self) -> &BigUint {
408 &self.amount
409 }
410
411 pub fn side(&self) -> OrderSide {
413 self.side
414 }
415
416 pub fn sender(&self) -> &Bytes {
418 &self.sender
419 }
420
421 pub fn receiver(&self) -> Option<&Bytes> {
424 self.receiver.as_ref()
425 }
426}
427
428#[derive(Default)]
432pub struct QuoteOptions {
433 pub(crate) timeout_ms: Option<u64>,
434 pub(crate) min_responses: Option<usize>,
435 pub(crate) max_gas: Option<BigUint>,
436 pub(crate) encoding_options: Option<EncodingOptions>,
437}
438
439impl QuoteOptions {
440 pub fn with_timeout_ms(mut self, ms: u64) -> Self {
442 self.timeout_ms = Some(ms);
443 self
444 }
445
446 pub fn with_min_responses(mut self, n: usize) -> Self {
451 self.min_responses = Some(n);
452 self
453 }
454
455 pub fn with_max_gas(mut self, gas: BigUint) -> Self {
457 self.max_gas = Some(gas);
458 self
459 }
460
461 pub fn with_encoding_options(mut self, opts: EncodingOptions) -> Self {
464 self.encoding_options = Some(opts);
465 self
466 }
467
468 pub fn timeout_ms(&self) -> Option<u64> {
470 self.timeout_ms
471 }
472
473 pub fn min_responses(&self) -> Option<usize> {
475 self.min_responses
476 }
477
478 pub fn max_gas(&self) -> Option<&BigUint> {
480 self.max_gas.as_ref()
481 }
482}
483
484pub struct QuoteParams {
486 pub(crate) order: Order,
487 pub(crate) options: QuoteOptions,
488}
489
490impl QuoteParams {
491 pub fn new(order: Order, options: QuoteOptions) -> Self {
493 Self { order, options }
494 }
495}
496
497#[derive(Debug, Clone, Copy, PartialEq, Eq)]
503pub enum BackendKind {
504 Fynd,
506 Turbine,
508}
509
510#[derive(Debug, Clone, Copy, PartialEq, Eq)]
512pub enum QuoteStatus {
513 Success,
515 NoRouteFound,
517 InsufficientLiquidity,
519 Timeout,
521 NotReady,
523}
524
525#[derive(Debug, Clone)]
530pub struct BlockInfo {
531 number: u64,
532 hash: String,
533 timestamp: u64,
534}
535
536impl BlockInfo {
537 pub fn number(&self) -> u64 {
539 self.number
540 }
541
542 pub fn hash(&self) -> &str {
544 &self.hash
545 }
546
547 pub fn timestamp(&self) -> u64 {
549 self.timestamp
550 }
551
552 pub(crate) fn new(number: u64, hash: String, timestamp: u64) -> Self {
553 Self { number, hash, timestamp }
554 }
555}
556
557#[derive(Debug, Clone)]
559pub struct Swap {
560 component_id: String,
561 protocol: String,
562 token_in: Bytes,
563 token_out: Bytes,
564 amount_in: BigUint,
565 amount_out: BigUint,
566 gas_estimate: BigUint,
567 #[allow(dead_code)]
568 split: f64,
569}
570
571impl Swap {
572 pub fn component_id(&self) -> &str {
574 &self.component_id
575 }
576
577 pub fn protocol(&self) -> &str {
579 &self.protocol
580 }
581
582 pub fn token_in(&self) -> &Bytes {
584 &self.token_in
585 }
586
587 pub fn token_out(&self) -> &Bytes {
589 &self.token_out
590 }
591
592 pub fn amount_in(&self) -> &BigUint {
594 &self.amount_in
595 }
596
597 pub fn amount_out(&self) -> &BigUint {
599 &self.amount_out
600 }
601
602 pub fn gas_estimate(&self) -> &BigUint {
604 &self.gas_estimate
605 }
606
607 #[allow(clippy::too_many_arguments)]
608 pub(crate) fn new(
609 component_id: String,
610 protocol: String,
611 token_in: Bytes,
612 token_out: Bytes,
613 amount_in: BigUint,
614 amount_out: BigUint,
615 gas_estimate: BigUint,
616 split: f64,
617 ) -> Self {
618 Self {
619 component_id,
620 protocol,
621 token_in,
622 token_out,
623 amount_in,
624 amount_out,
625 gas_estimate,
626 split,
627 }
628 }
629}
630
631#[derive(Debug, Clone)]
635pub struct Route {
636 swaps: Vec<Swap>,
637}
638
639impl Route {
640 pub fn swaps(&self) -> &[Swap] {
642 &self.swaps
643 }
644
645 pub(crate) fn new(swaps: Vec<Swap>) -> Self {
646 Self { swaps }
647 }
648}
649
650#[derive(Debug, Clone)]
654pub struct FeeBreakdown {
655 router_fee: BigUint,
656 client_fee: BigUint,
657 max_slippage: BigUint,
658 min_amount_received: BigUint,
659}
660
661impl FeeBreakdown {
662 pub(crate) fn new(
663 router_fee: BigUint,
664 client_fee: BigUint,
665 max_slippage: BigUint,
666 min_amount_received: BigUint,
667 ) -> Self {
668 Self { router_fee, client_fee, max_slippage, min_amount_received }
669 }
670
671 pub fn router_fee(&self) -> &BigUint {
673 &self.router_fee
674 }
675
676 pub fn client_fee(&self) -> &BigUint {
678 &self.client_fee
679 }
680
681 pub fn max_slippage(&self) -> &BigUint {
683 &self.max_slippage
684 }
685
686 pub fn min_amount_received(&self) -> &BigUint {
689 &self.min_amount_received
690 }
691}
692
693#[derive(Debug, Clone)]
695pub struct Quote {
696 order_id: String,
697 status: QuoteStatus,
698 backend: BackendKind,
699 route: Option<Route>,
700 amount_in: BigUint,
701 amount_out: BigUint,
702 gas_estimate: BigUint,
703 amount_out_net_gas: BigUint,
704 price_impact_bps: Option<i32>,
705 block: BlockInfo,
706 token_out: Bytes,
709 receiver: Bytes,
713 transaction: Option<Transaction>,
716 fee_breakdown: Option<FeeBreakdown>,
718 pub(crate) solve_time_ms: u64,
721}
722
723impl Quote {
724 pub fn order_id(&self) -> &str {
726 &self.order_id
727 }
728
729 pub fn status(&self) -> QuoteStatus {
731 self.status
732 }
733
734 pub fn backend(&self) -> BackendKind {
736 self.backend
737 }
738
739 pub fn route(&self) -> Option<&Route> {
741 self.route.as_ref()
742 }
743
744 pub fn amount_in(&self) -> &BigUint {
746 &self.amount_in
747 }
748
749 pub fn amount_out(&self) -> &BigUint {
751 &self.amount_out
752 }
753
754 pub fn gas_estimate(&self) -> &BigUint {
756 &self.gas_estimate
757 }
758
759 pub fn amount_out_net_gas(&self) -> &BigUint {
764 &self.amount_out_net_gas
765 }
766
767 pub fn price_impact_bps(&self) -> Option<i32> {
769 self.price_impact_bps
770 }
771
772 pub fn block(&self) -> &BlockInfo {
774 &self.block
775 }
776
777 pub fn token_out(&self) -> &Bytes {
782 &self.token_out
783 }
784
785 pub fn receiver(&self) -> &Bytes {
792 &self.receiver
793 }
794
795 pub fn transaction(&self) -> Option<&Transaction> {
800 self.transaction.as_ref()
801 }
802
803 pub fn fee_breakdown(&self) -> Option<&FeeBreakdown> {
808 self.fee_breakdown.as_ref()
809 }
810
811 pub fn solve_time_ms(&self) -> u64 {
815 self.solve_time_ms
816 }
817
818 #[allow(clippy::too_many_arguments)]
819 pub(crate) fn new(
820 order_id: String,
821 status: QuoteStatus,
822 backend: BackendKind,
823 route: Option<Route>,
824 amount_in: BigUint,
825 amount_out: BigUint,
826 gas_estimate: BigUint,
827 amount_out_net_gas: BigUint,
828 price_impact_bps: Option<i32>,
829 block: BlockInfo,
830 token_out: Bytes,
831 receiver: Bytes,
832 transaction: Option<Transaction>,
833 fee_breakdown: Option<FeeBreakdown>,
834 ) -> Self {
835 Self {
836 order_id,
837 status,
838 backend,
839 route,
840 amount_in,
841 amount_out,
842 gas_estimate,
843 amount_out_net_gas,
844 price_impact_bps,
845 block,
846 token_out,
847 receiver,
848 transaction,
849 fee_breakdown,
850 solve_time_ms: 0,
851 }
852 }
853}
854
855#[derive(Debug)]
857pub(crate) struct BatchQuote {
858 quotes: Vec<Quote>,
859}
860
861impl BatchQuote {
862 pub fn quotes(&self) -> &[Quote] {
864 &self.quotes
865 }
866
867 pub(crate) fn new(quotes: Vec<Quote>) -> Self {
868 Self { quotes }
869 }
870}
871
872pub struct InstanceInfo {
874 router_address: bytes::Bytes,
876 permit2_address: bytes::Bytes,
878 chain_id: u64,
880}
881
882impl InstanceInfo {
883 pub(crate) fn new(
884 router_address: bytes::Bytes,
885 permit2_address: bytes::Bytes,
886 chain_id: u64,
887 ) -> Self {
888 Self { router_address, permit2_address, chain_id }
889 }
890
891 pub fn router_address(&self) -> &bytes::Bytes {
893 &self.router_address
894 }
895
896 pub fn permit2_address(&self) -> &bytes::Bytes {
898 &self.permit2_address
899 }
900
901 pub fn chain_id(&self) -> u64 {
903 self.chain_id
904 }
905}
906
907#[derive(Debug)]
909pub struct HealthStatus {
910 healthy: bool,
911 last_update_ms: u64,
912 num_solver_pools: usize,
913 derived_data_ready: bool,
914 gas_price_age_ms: Option<u64>,
915}
916
917impl HealthStatus {
918 pub fn healthy(&self) -> bool {
920 self.healthy
921 }
922
923 pub fn last_update_ms(&self) -> u64 {
925 self.last_update_ms
926 }
927
928 pub fn num_solver_pools(&self) -> usize {
930 self.num_solver_pools
931 }
932
933 pub fn derived_data_ready(&self) -> bool {
939 self.derived_data_ready
940 }
941
942 pub fn gas_price_age_ms(&self) -> Option<u64> {
944 self.gas_price_age_ms
945 }
946
947 pub(crate) fn new(
948 healthy: bool,
949 last_update_ms: u64,
950 num_solver_pools: usize,
951 derived_data_ready: bool,
952 gas_price_age_ms: Option<u64>,
953 ) -> Self {
954 Self { healthy, last_update_ms, num_solver_pools, derived_data_ready, gas_price_age_ms }
955 }
956}
957
958#[cfg(test)]
959mod tests {
960 use num_bigint::BigUint;
961
962 use super::*;
963
964 fn addr(bytes: &[u8; 20]) -> Bytes {
965 Bytes::copy_from_slice(bytes)
966 }
967
968 #[test]
969 fn order_new_and_getters() {
970 let token_in = addr(&[0xaa; 20]);
971 let token_out = addr(&[0xbb; 20]);
972 let amount = BigUint::from(1_000_000u64);
973 let sender = addr(&[0xcc; 20]);
974
975 let order = Order::new(
976 token_in.clone(),
977 token_out.clone(),
978 amount.clone(),
979 OrderSide::Sell,
980 sender.clone(),
981 None,
982 );
983
984 assert_eq!(order.token_in(), &token_in);
985 assert_eq!(order.token_out(), &token_out);
986 assert_eq!(order.amount(), &amount);
987 assert_eq!(order.sender(), &sender);
988 assert!(order.receiver().is_none());
989 assert_eq!(order.side(), OrderSide::Sell);
990 }
991
992 #[test]
993 fn order_with_explicit_receiver() {
994 let receiver = Bytes::copy_from_slice(&[0xdd; 20]);
995 let order = Order::new(
996 Bytes::copy_from_slice(&[0xaa; 20]),
997 Bytes::copy_from_slice(&[0xbb; 20]),
998 BigUint::from(1u32),
999 OrderSide::Sell,
1000 Bytes::copy_from_slice(&[0xcc; 20]),
1001 Some(receiver.clone()),
1002 );
1003 assert_eq!(order.receiver(), Some(&receiver));
1004 }
1005
1006 #[test]
1007 fn quote_options_builder() {
1008 let opts = QuoteOptions::default()
1009 .with_timeout_ms(500)
1010 .with_min_responses(2)
1011 .with_max_gas(BigUint::from(1_000_000u64));
1012
1013 assert_eq!(opts.timeout_ms(), Some(500));
1014 assert_eq!(opts.min_responses(), Some(2));
1015 assert_eq!(opts.max_gas(), Some(&BigUint::from(1_000_000u64)));
1016 }
1017
1018 #[test]
1019 fn quote_options_default_all_none() {
1020 let opts = QuoteOptions::default();
1021 assert!(opts.timeout_ms().is_none());
1022 assert!(opts.min_responses().is_none());
1023 assert!(opts.max_gas().is_none());
1024 }
1025
1026 #[test]
1027 fn encoding_options_with_permit2_sets_fields() {
1028 let token = Bytes::copy_from_slice(&[0xaa; 20]);
1029 let spender = Bytes::copy_from_slice(&[0xbb; 20]);
1030 let sig = Bytes::copy_from_slice(&[0xcc; 65]);
1031 let details = PermitDetails::new(
1032 token,
1033 BigUint::from(1_000u32),
1034 BigUint::from(9_999_999u32),
1035 BigUint::from(0u32),
1036 );
1037 let permit = PermitSingle::new(details, spender, BigUint::from(9_999_999u32));
1038
1039 let opts = EncodingOptions::new(0.005)
1040 .with_permit2(permit, sig.clone())
1041 .unwrap();
1042
1043 assert_eq!(opts.transfer_type, UserTransferType::TransferFromPermit2);
1044 assert!(opts.permit.is_some());
1045 assert_eq!(opts.permit2_signature.as_ref().unwrap(), &sig);
1046 }
1047
1048 #[test]
1049 fn encoding_options_with_permit2_rejects_wrong_signature_length() {
1050 let details = PermitDetails::new(
1051 Bytes::copy_from_slice(&[0xaa; 20]),
1052 BigUint::from(1_000u32),
1053 BigUint::from(9_999_999u32),
1054 BigUint::from(0u32),
1055 );
1056 let permit = PermitSingle::new(
1057 details,
1058 Bytes::copy_from_slice(&[0xbb; 20]),
1059 BigUint::from(9_999_999u32),
1060 );
1061 let bad_sig = Bytes::copy_from_slice(&[0xcc; 64]); assert!(matches!(
1063 EncodingOptions::new(0.005).with_permit2(permit, bad_sig),
1064 Err(crate::error::FyndError::Protocol(_))
1065 ));
1066 }
1067
1068 #[test]
1069 fn encoding_options_with_vault_funds_sets_variant() {
1070 let opts = EncodingOptions::new(0.005).with_vault_funds();
1071 assert_eq!(opts.transfer_type, UserTransferType::UseVaultsFunds);
1072 assert!(opts.permit.is_none());
1073 assert!(opts.permit2_signature.is_none());
1074 }
1075
1076 fn sample_permit_single() -> PermitSingle {
1077 let details = PermitDetails::new(
1078 Bytes::copy_from_slice(&[0xaa; 20]),
1079 BigUint::from(1_000u32),
1080 BigUint::from(9_999_999u32),
1081 BigUint::from(0u32),
1082 );
1083 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(9_999_999u32))
1084 }
1085
1086 #[test]
1087 fn eip712_signing_hash_returns_32_bytes() {
1088 let permit = sample_permit_single();
1089 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1090 let hash = permit
1091 .eip712_signing_hash(1, &permit2_addr)
1092 .unwrap();
1093 assert_eq!(hash.len(), 32);
1094 assert_ne!(hash, [0u8; 32]);
1096 }
1097
1098 #[test]
1099 fn eip712_signing_hash_is_deterministic() {
1100 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1101 let h1 = sample_permit_single()
1102 .eip712_signing_hash(1, &permit2_addr)
1103 .unwrap();
1104 let h2 = sample_permit_single()
1105 .eip712_signing_hash(1, &permit2_addr)
1106 .unwrap();
1107 assert_eq!(h1, h2);
1108 }
1109
1110 #[test]
1111 fn eip712_signing_hash_differs_by_chain_id() {
1112 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1113 let h1 = sample_permit_single()
1114 .eip712_signing_hash(1, &permit2_addr)
1115 .unwrap();
1116 let h137 = sample_permit_single()
1117 .eip712_signing_hash(137, &permit2_addr)
1118 .unwrap();
1119 assert_ne!(h1, h137);
1120 }
1121
1122 #[test]
1123 fn eip712_signing_hash_invalid_permit2_address() {
1124 let permit = sample_permit_single();
1125 let bad_addr = Bytes::copy_from_slice(&[0xcc; 4]);
1126 assert!(matches!(
1127 permit.eip712_signing_hash(1, &bad_addr),
1128 Err(crate::error::FyndError::Protocol(_))
1129 ));
1130 }
1131
1132 #[test]
1133 fn eip712_signing_hash_invalid_token_address() {
1134 let details = PermitDetails::new(
1135 Bytes::copy_from_slice(&[0xaa; 4]), BigUint::from(1u32),
1137 BigUint::from(1u32),
1138 BigUint::from(0u32),
1139 );
1140 let permit =
1141 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(1u32));
1142 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1143 assert!(matches!(
1144 permit.eip712_signing_hash(1, &permit2_addr),
1145 Err(crate::error::FyndError::Protocol(_))
1146 ));
1147 }
1148
1149 #[test]
1150 fn eip712_signing_hash_amount_exceeds_uint160() {
1151 let oversized_amount = BigUint::from_bytes_be(&[0x01; 21]);
1153 let details = PermitDetails::new(
1154 Bytes::copy_from_slice(&[0xaa; 20]),
1155 oversized_amount,
1156 BigUint::from(1u32),
1157 BigUint::from(0u32),
1158 );
1159 let permit =
1160 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(1u32));
1161 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1162 assert!(matches!(
1163 permit.eip712_signing_hash(1, &permit2_addr),
1164 Err(crate::error::FyndError::Protocol(_))
1165 ));
1166 }
1167
1168 fn sample_fee_receiver() -> Bytes {
1173 Bytes::copy_from_slice(&[0x44; 20])
1174 }
1175
1176 fn sample_router_address() -> Bytes {
1177 Bytes::copy_from_slice(&[0x33; 20])
1178 }
1179
1180 fn sample_fee_params(bps: u16, receiver: Bytes) -> ClientFeeParams {
1181 ClientFeeParams::new(bps, receiver, BigUint::ZERO, 1_893_456_000)
1182 }
1183
1184 #[test]
1185 fn client_fee_with_client_fee_sets_fields() {
1186 let fee = ClientFeeParams::new(
1187 100,
1188 sample_fee_receiver(),
1189 BigUint::from(500_000u64),
1190 1_893_456_000,
1191 );
1192 let opts = EncodingOptions::new(0.01).with_client_fee(fee);
1193 assert!(opts.client_fee_params.is_some());
1194 let stored = opts.client_fee_params.as_ref().unwrap();
1195 assert_eq!(stored.bps, 100);
1196 assert_eq!(stored.max_contribution, BigUint::from(500_000u64));
1197 }
1198
1199 #[test]
1200 fn client_fee_signing_hash_returns_32_bytes() {
1201 let fee = sample_fee_params(100, sample_fee_receiver());
1202 let hash = fee
1203 .eip712_signing_hash(1, &sample_router_address())
1204 .unwrap();
1205 assert_eq!(hash.len(), 32);
1206 assert_ne!(hash, [0u8; 32]);
1207 }
1208
1209 #[test]
1210 fn client_fee_signing_hash_is_deterministic() {
1211 let fee = sample_fee_params(100, sample_fee_receiver());
1212 let h1 = fee
1213 .eip712_signing_hash(1, &sample_router_address())
1214 .unwrap();
1215 let h2 = fee
1216 .eip712_signing_hash(1, &sample_router_address())
1217 .unwrap();
1218 assert_eq!(h1, h2);
1219 }
1220
1221 #[test]
1222 fn client_fee_signing_hash_differs_by_chain_id() {
1223 let fee = sample_fee_params(100, sample_fee_receiver());
1224 let h1 = fee
1225 .eip712_signing_hash(1, &sample_router_address())
1226 .unwrap();
1227 let h137 = fee
1228 .eip712_signing_hash(137, &sample_router_address())
1229 .unwrap();
1230 assert_ne!(h1, h137);
1231 }
1232
1233 #[test]
1234 fn client_fee_signing_hash_differs_by_bps() {
1235 let h100 = sample_fee_params(100, sample_fee_receiver())
1236 .eip712_signing_hash(1, &sample_router_address())
1237 .unwrap();
1238 let h200 = sample_fee_params(200, sample_fee_receiver())
1239 .eip712_signing_hash(1, &sample_router_address())
1240 .unwrap();
1241 assert_ne!(h100, h200);
1242 }
1243
1244 #[test]
1245 fn client_fee_signing_hash_differs_by_receiver() {
1246 let other_receiver = Bytes::copy_from_slice(&[0x55; 20]);
1247 let h1 = sample_fee_params(100, sample_fee_receiver())
1248 .eip712_signing_hash(1, &sample_router_address())
1249 .unwrap();
1250 let h2 = sample_fee_params(100, other_receiver)
1251 .eip712_signing_hash(1, &sample_router_address())
1252 .unwrap();
1253 assert_ne!(h1, h2);
1254 }
1255
1256 #[test]
1257 fn client_fee_signing_hash_rejects_bad_receiver_address() {
1258 let bad_addr = Bytes::copy_from_slice(&[0x44; 4]);
1259 let fee = sample_fee_params(100, bad_addr);
1260 assert!(matches!(
1261 fee.eip712_signing_hash(1, &sample_router_address()),
1262 Err(crate::error::FyndError::Protocol(_))
1263 ));
1264 }
1265
1266 #[test]
1267 fn client_fee_signing_hash_rejects_bad_router_address() {
1268 let bad_addr = Bytes::copy_from_slice(&[0x33; 4]);
1269 let fee = sample_fee_params(100, sample_fee_receiver());
1270 assert!(matches!(
1271 fee.eip712_signing_hash(1, &bad_addr),
1272 Err(crate::error::FyndError::Protocol(_))
1273 ));
1274 }
1275}