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(
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 pub fn eip712_signing_hash(
154 &self,
155 chain_id: u64,
156 router_address: &Bytes,
157 ) -> Result<[u8; 32], crate::error::FyndError> {
158 let router_addr = p2_bytes_to_address(router_address, "router_address")?;
159 let fee_receiver = p2_bytes_to_address(&self.receiver, "receiver")?;
160 let max_contrib = biguint_to_u256(&self.max_contribution);
161 let dl = U256::from(self.deadline);
162
163 let type_hash = keccak256(
164 b"ClientFee(uint16 clientFeeBps,address clientFeeReceiver,\
165uint256 maxClientContribution,uint256 deadline)",
166 );
167
168 let domain_type_hash = keccak256(
169 b"EIP712Domain(string name,string version,\
170uint256 chainId,address verifyingContract)",
171 );
172 let domain_separator = keccak256(
173 (
174 domain_type_hash,
175 keccak256(b"TychoRouter"),
176 keccak256(b"1"),
177 U256::from(chain_id),
178 router_addr,
179 )
180 .abi_encode(),
181 );
182
183 let struct_hash = keccak256(
184 (type_hash, U256::from(self.bps), fee_receiver, max_contrib, dl).abi_encode(),
185 );
186
187 let mut data = [0u8; 66];
188 data[0] = 0x19;
189 data[1] = 0x01;
190 data[2..34].copy_from_slice(domain_separator.as_ref());
191 data[34..66].copy_from_slice(struct_hash.as_ref());
192 Ok(keccak256(data).0)
193 }
194}
195
196mod permit2_sol {
201 use alloy::sol;
202
203 sol! {
204 struct PermitDetails {
205 address token;
206 uint160 amount;
207 uint48 expiration;
208 uint48 nonce;
209 }
210 struct PermitSingle {
211 PermitDetails details;
212 address spender;
213 uint256 sigDeadline;
214 }
215 }
216}
217
218fn p2_bytes_to_address(
219 b: &bytes::Bytes,
220 field: &str,
221) -> Result<alloy::primitives::Address, crate::error::FyndError> {
222 let arr: [u8; 20] = b.as_ref().try_into().map_err(|_| {
223 crate::error::FyndError::Protocol(format!(
224 "expected 20-byte address for {field}, got {} bytes",
225 b.len()
226 ))
227 })?;
228 Ok(alloy::primitives::Address::from(arr))
229}
230
231fn p2_biguint_to_uint160(
232 n: &num_bigint::BigUint,
233) -> Result<alloy::primitives::Uint<160, 3>, crate::error::FyndError> {
234 let bytes = n.to_bytes_be();
235 if bytes.len() > 20 {
236 return Err(crate::error::FyndError::Protocol(format!(
237 "permit amount exceeds uint160 ({} bytes)",
238 bytes.len()
239 )));
240 }
241 let mut arr = [0u8; 20];
242 arr[20 - bytes.len()..].copy_from_slice(&bytes);
243 Ok(alloy::primitives::Uint::<160, 3>::from_be_bytes(arr))
244}
245
246fn p2_biguint_to_uint48(
247 n: &num_bigint::BigUint,
248) -> Result<alloy::primitives::Uint<48, 1>, crate::error::FyndError> {
249 let bytes = n.to_bytes_be();
250 if bytes.len() > 6 {
251 return Err(crate::error::FyndError::Protocol(format!(
252 "permit value exceeds uint48 ({} bytes)",
253 bytes.len()
254 )));
255 }
256 let mut arr = [0u8; 6];
257 arr[6 - bytes.len()..].copy_from_slice(&bytes);
258 Ok(alloy::primitives::Uint::<48, 1>::from_be_bytes(arr))
259}
260
261#[derive(Debug, Clone)]
266pub struct EncodingOptions {
267 pub(crate) slippage: f64,
268 pub(crate) transfer_type: UserTransferType,
269 pub(crate) permit: Option<PermitSingle>,
270 pub(crate) permit2_signature: Option<Bytes>,
271 pub(crate) client_fee_params: Option<ClientFeeParams>,
272}
273
274impl EncodingOptions {
275 pub fn new(slippage: f64) -> Self {
280 Self {
281 slippage,
282 transfer_type: UserTransferType::TransferFrom,
283 permit: None,
284 permit2_signature: None,
285 client_fee_params: None,
286 }
287 }
288
289 pub fn with_permit2(
298 mut self,
299 permit: PermitSingle,
300 signature: bytes::Bytes,
301 ) -> Result<Self, crate::error::FyndError> {
302 if signature.len() != 65 {
303 return Err(crate::error::FyndError::Protocol(format!(
304 "Permit2 signature must be exactly 65 bytes, got {}",
305 signature.len()
306 )));
307 }
308 self.transfer_type = UserTransferType::TransferFromPermit2;
309 self.permit = Some(permit);
310 self.permit2_signature = Some(signature);
311 Ok(self)
312 }
313
314 pub fn with_vault_funds(mut self) -> Self {
316 self.transfer_type = UserTransferType::UseVaultsFunds;
317 self
318 }
319
320 pub fn with_client_fee(mut self, params: ClientFeeParams) -> Self {
322 self.client_fee_params = Some(params);
323 self
324 }
325}
326
327#[derive(Debug, Clone)]
331pub struct Transaction {
332 to: Bytes,
333 value: BigUint,
334 data: Vec<u8>,
335}
336
337impl Transaction {
338 pub(crate) fn new(to: Bytes, value: BigUint, data: Vec<u8>) -> Self {
339 Self { to, value, data }
340 }
341
342 pub fn to(&self) -> &Bytes {
344 &self.to
345 }
346
347 pub fn value(&self) -> &BigUint {
349 &self.value
350 }
351
352 pub fn data(&self) -> &[u8] {
354 &self.data
355 }
356}
357
358#[non_exhaustive]
366#[derive(Debug, Clone, Copy, PartialEq, Eq)]
367pub enum OrderSide {
368 Sell,
370}
371
372pub struct Order {
381 token_in: Bytes,
382 token_out: Bytes,
383 amount: BigUint,
384 side: OrderSide,
385 sender: Bytes,
386 receiver: Option<Bytes>,
387}
388
389impl Order {
390 pub fn new(
399 token_in: Bytes,
400 token_out: Bytes,
401 amount: BigUint,
402 side: OrderSide,
403 sender: Bytes,
404 receiver: Option<Bytes>,
405 ) -> Self {
406 Self { token_in, token_out, amount, side, sender, receiver }
407 }
408
409 pub fn token_in(&self) -> &Bytes {
411 &self.token_in
412 }
413
414 pub fn token_out(&self) -> &Bytes {
416 &self.token_out
417 }
418
419 pub fn amount(&self) -> &BigUint {
421 &self.amount
422 }
423
424 pub fn side(&self) -> OrderSide {
426 self.side
427 }
428
429 pub fn sender(&self) -> &Bytes {
431 &self.sender
432 }
433
434 pub fn receiver(&self) -> Option<&Bytes> {
437 self.receiver.as_ref()
438 }
439}
440
441#[derive(Default)]
445pub struct QuoteOptions {
446 pub(crate) timeout_ms: Option<u64>,
447 pub(crate) min_responses: Option<usize>,
448 pub(crate) max_gas: Option<BigUint>,
449 pub(crate) encoding_options: Option<EncodingOptions>,
450}
451
452impl QuoteOptions {
453 pub fn with_timeout_ms(mut self, ms: u64) -> Self {
455 self.timeout_ms = Some(ms);
456 self
457 }
458
459 pub fn with_min_responses(mut self, n: usize) -> Self {
464 self.min_responses = Some(n);
465 self
466 }
467
468 pub fn with_max_gas(mut self, gas: BigUint) -> Self {
470 self.max_gas = Some(gas);
471 self
472 }
473
474 pub fn with_encoding_options(mut self, opts: EncodingOptions) -> Self {
477 self.encoding_options = Some(opts);
478 self
479 }
480
481 pub fn timeout_ms(&self) -> Option<u64> {
483 self.timeout_ms
484 }
485
486 pub fn min_responses(&self) -> Option<usize> {
488 self.min_responses
489 }
490
491 pub fn max_gas(&self) -> Option<&BigUint> {
493 self.max_gas.as_ref()
494 }
495}
496
497pub struct QuoteParams {
499 pub(crate) order: Order,
500 pub(crate) options: QuoteOptions,
501}
502
503impl QuoteParams {
504 pub fn new(order: Order, options: QuoteOptions) -> Self {
506 Self { order, options }
507 }
508}
509
510#[derive(Debug, Clone, Copy, PartialEq, Eq)]
516pub enum BackendKind {
517 Fynd,
519 Turbine,
521}
522
523#[derive(Debug, Clone, Copy, PartialEq, Eq)]
525pub enum QuoteStatus {
526 Success,
528 NoRouteFound,
530 InsufficientLiquidity,
532 Timeout,
534 NotReady,
536}
537
538#[derive(Debug, Clone)]
543pub struct BlockInfo {
544 number: u64,
545 hash: String,
546 timestamp: u64,
547}
548
549impl BlockInfo {
550 pub fn number(&self) -> u64 {
552 self.number
553 }
554
555 pub fn hash(&self) -> &str {
557 &self.hash
558 }
559
560 pub fn timestamp(&self) -> u64 {
562 self.timestamp
563 }
564
565 pub(crate) fn new(number: u64, hash: String, timestamp: u64) -> Self {
566 Self { number, hash, timestamp }
567 }
568}
569
570#[derive(Debug, Clone)]
572pub struct Swap {
573 component_id: String,
574 protocol: String,
575 token_in: Bytes,
576 token_out: Bytes,
577 amount_in: BigUint,
578 amount_out: BigUint,
579 gas_estimate: BigUint,
580 #[allow(dead_code)]
581 split: f64,
582}
583
584impl Swap {
585 pub fn component_id(&self) -> &str {
587 &self.component_id
588 }
589
590 pub fn protocol(&self) -> &str {
592 &self.protocol
593 }
594
595 pub fn token_in(&self) -> &Bytes {
597 &self.token_in
598 }
599
600 pub fn token_out(&self) -> &Bytes {
602 &self.token_out
603 }
604
605 pub fn amount_in(&self) -> &BigUint {
607 &self.amount_in
608 }
609
610 pub fn amount_out(&self) -> &BigUint {
612 &self.amount_out
613 }
614
615 pub fn gas_estimate(&self) -> &BigUint {
617 &self.gas_estimate
618 }
619
620 #[allow(clippy::too_many_arguments)]
621 pub(crate) fn new(
622 component_id: String,
623 protocol: String,
624 token_in: Bytes,
625 token_out: Bytes,
626 amount_in: BigUint,
627 amount_out: BigUint,
628 gas_estimate: BigUint,
629 split: f64,
630 ) -> Self {
631 Self {
632 component_id,
633 protocol,
634 token_in,
635 token_out,
636 amount_in,
637 amount_out,
638 gas_estimate,
639 split,
640 }
641 }
642}
643
644#[derive(Debug, Clone)]
648pub struct Route {
649 swaps: Vec<Swap>,
650}
651
652impl Route {
653 pub fn swaps(&self) -> &[Swap] {
655 &self.swaps
656 }
657
658 pub(crate) fn new(swaps: Vec<Swap>) -> Self {
659 Self { swaps }
660 }
661}
662
663#[derive(Debug, Clone)]
667pub struct FeeBreakdown {
668 router_fee: BigUint,
669 client_fee: BigUint,
670 max_slippage: BigUint,
671 min_amount_received: BigUint,
672}
673
674impl FeeBreakdown {
675 pub(crate) fn new(
676 router_fee: BigUint,
677 client_fee: BigUint,
678 max_slippage: BigUint,
679 min_amount_received: BigUint,
680 ) -> Self {
681 Self { router_fee, client_fee, max_slippage, min_amount_received }
682 }
683
684 pub fn router_fee(&self) -> &BigUint {
686 &self.router_fee
687 }
688
689 pub fn client_fee(&self) -> &BigUint {
691 &self.client_fee
692 }
693
694 pub fn max_slippage(&self) -> &BigUint {
696 &self.max_slippage
697 }
698
699 pub fn min_amount_received(&self) -> &BigUint {
702 &self.min_amount_received
703 }
704}
705
706#[derive(Debug, Clone)]
708pub struct Quote {
709 order_id: String,
710 status: QuoteStatus,
711 backend: BackendKind,
712 route: Option<Route>,
713 amount_in: BigUint,
714 amount_out: BigUint,
715 gas_estimate: BigUint,
716 amount_out_net_gas: BigUint,
717 price_impact_bps: Option<i32>,
718 block: BlockInfo,
719 token_out: Bytes,
722 receiver: Bytes,
726 transaction: Option<Transaction>,
729 fee_breakdown: Option<FeeBreakdown>,
731 pub(crate) solve_time_ms: u64,
734}
735
736impl Quote {
737 pub fn order_id(&self) -> &str {
739 &self.order_id
740 }
741
742 pub fn status(&self) -> QuoteStatus {
744 self.status
745 }
746
747 pub fn backend(&self) -> BackendKind {
749 self.backend
750 }
751
752 pub fn route(&self) -> Option<&Route> {
754 self.route.as_ref()
755 }
756
757 pub fn amount_in(&self) -> &BigUint {
759 &self.amount_in
760 }
761
762 pub fn amount_out(&self) -> &BigUint {
764 &self.amount_out
765 }
766
767 pub fn gas_estimate(&self) -> &BigUint {
769 &self.gas_estimate
770 }
771
772 pub fn amount_out_net_gas(&self) -> &BigUint {
777 &self.amount_out_net_gas
778 }
779
780 pub fn price_impact_bps(&self) -> Option<i32> {
782 self.price_impact_bps
783 }
784
785 pub fn block(&self) -> &BlockInfo {
787 &self.block
788 }
789
790 pub fn token_out(&self) -> &Bytes {
795 &self.token_out
796 }
797
798 pub fn receiver(&self) -> &Bytes {
805 &self.receiver
806 }
807
808 pub fn transaction(&self) -> Option<&Transaction> {
813 self.transaction.as_ref()
814 }
815
816 pub fn fee_breakdown(&self) -> Option<&FeeBreakdown> {
821 self.fee_breakdown.as_ref()
822 }
823
824 pub fn solve_time_ms(&self) -> u64 {
828 self.solve_time_ms
829 }
830
831 #[allow(clippy::too_many_arguments)]
832 pub(crate) fn new(
833 order_id: String,
834 status: QuoteStatus,
835 backend: BackendKind,
836 route: Option<Route>,
837 amount_in: BigUint,
838 amount_out: BigUint,
839 gas_estimate: BigUint,
840 amount_out_net_gas: BigUint,
841 price_impact_bps: Option<i32>,
842 block: BlockInfo,
843 token_out: Bytes,
844 receiver: Bytes,
845 transaction: Option<Transaction>,
846 fee_breakdown: Option<FeeBreakdown>,
847 ) -> Self {
848 Self {
849 order_id,
850 status,
851 backend,
852 route,
853 amount_in,
854 amount_out,
855 gas_estimate,
856 amount_out_net_gas,
857 price_impact_bps,
858 block,
859 token_out,
860 receiver,
861 transaction,
862 fee_breakdown,
863 solve_time_ms: 0,
864 }
865 }
866}
867
868#[derive(Debug)]
870pub(crate) struct BatchQuote {
871 quotes: Vec<Quote>,
872}
873
874impl BatchQuote {
875 pub fn quotes(&self) -> &[Quote] {
877 &self.quotes
878 }
879
880 pub(crate) fn new(quotes: Vec<Quote>) -> Self {
881 Self { quotes }
882 }
883}
884
885pub struct InstanceInfo {
887 router_address: bytes::Bytes,
889 permit2_address: bytes::Bytes,
891 chain_id: u64,
893}
894
895impl InstanceInfo {
896 pub(crate) fn new(
897 router_address: bytes::Bytes,
898 permit2_address: bytes::Bytes,
899 chain_id: u64,
900 ) -> Self {
901 Self { router_address, permit2_address, chain_id }
902 }
903
904 pub fn router_address(&self) -> &bytes::Bytes {
906 &self.router_address
907 }
908
909 pub fn permit2_address(&self) -> &bytes::Bytes {
911 &self.permit2_address
912 }
913
914 pub fn chain_id(&self) -> u64 {
916 self.chain_id
917 }
918}
919
920#[derive(Debug)]
922pub struct HealthStatus {
923 healthy: bool,
924 last_update_ms: u64,
925 num_solver_pools: usize,
926 derived_data_ready: bool,
927 gas_price_age_ms: Option<u64>,
928}
929
930impl HealthStatus {
931 pub fn healthy(&self) -> bool {
933 self.healthy
934 }
935
936 pub fn last_update_ms(&self) -> u64 {
938 self.last_update_ms
939 }
940
941 pub fn num_solver_pools(&self) -> usize {
943 self.num_solver_pools
944 }
945
946 pub fn derived_data_ready(&self) -> bool {
952 self.derived_data_ready
953 }
954
955 pub fn gas_price_age_ms(&self) -> Option<u64> {
957 self.gas_price_age_ms
958 }
959
960 pub(crate) fn new(
961 healthy: bool,
962 last_update_ms: u64,
963 num_solver_pools: usize,
964 derived_data_ready: bool,
965 gas_price_age_ms: Option<u64>,
966 ) -> Self {
967 Self { healthy, last_update_ms, num_solver_pools, derived_data_ready, gas_price_age_ms }
968 }
969}
970
971#[cfg(test)]
972mod tests {
973 use num_bigint::BigUint;
974
975 use super::*;
976
977 fn addr(bytes: &[u8; 20]) -> Bytes {
978 Bytes::copy_from_slice(bytes)
979 }
980
981 #[test]
982 fn order_new_and_getters() {
983 let token_in = addr(&[0xaa; 20]);
984 let token_out = addr(&[0xbb; 20]);
985 let amount = BigUint::from(1_000_000u64);
986 let sender = addr(&[0xcc; 20]);
987
988 let order = Order::new(
989 token_in.clone(),
990 token_out.clone(),
991 amount.clone(),
992 OrderSide::Sell,
993 sender.clone(),
994 None,
995 );
996
997 assert_eq!(order.token_in(), &token_in);
998 assert_eq!(order.token_out(), &token_out);
999 assert_eq!(order.amount(), &amount);
1000 assert_eq!(order.sender(), &sender);
1001 assert!(order.receiver().is_none());
1002 assert_eq!(order.side(), OrderSide::Sell);
1003 }
1004
1005 #[test]
1006 fn order_with_explicit_receiver() {
1007 let receiver = Bytes::copy_from_slice(&[0xdd; 20]);
1008 let order = Order::new(
1009 Bytes::copy_from_slice(&[0xaa; 20]),
1010 Bytes::copy_from_slice(&[0xbb; 20]),
1011 BigUint::from(1u32),
1012 OrderSide::Sell,
1013 Bytes::copy_from_slice(&[0xcc; 20]),
1014 Some(receiver.clone()),
1015 );
1016 assert_eq!(order.receiver(), Some(&receiver));
1017 }
1018
1019 #[test]
1020 fn quote_options_builder() {
1021 let opts = QuoteOptions::default()
1022 .with_timeout_ms(500)
1023 .with_min_responses(2)
1024 .with_max_gas(BigUint::from(1_000_000u64));
1025
1026 assert_eq!(opts.timeout_ms(), Some(500));
1027 assert_eq!(opts.min_responses(), Some(2));
1028 assert_eq!(opts.max_gas(), Some(&BigUint::from(1_000_000u64)));
1029 }
1030
1031 #[test]
1032 fn quote_options_default_all_none() {
1033 let opts = QuoteOptions::default();
1034 assert!(opts.timeout_ms().is_none());
1035 assert!(opts.min_responses().is_none());
1036 assert!(opts.max_gas().is_none());
1037 }
1038
1039 #[test]
1040 fn encoding_options_with_permit2_sets_fields() {
1041 let token = Bytes::copy_from_slice(&[0xaa; 20]);
1042 let spender = Bytes::copy_from_slice(&[0xbb; 20]);
1043 let sig = Bytes::copy_from_slice(&[0xcc; 65]);
1044 let details = PermitDetails::new(
1045 token,
1046 BigUint::from(1_000u32),
1047 BigUint::from(9_999_999u32),
1048 BigUint::from(0u32),
1049 );
1050 let permit = PermitSingle::new(details, spender, BigUint::from(9_999_999u32));
1051
1052 let opts = EncodingOptions::new(0.005)
1053 .with_permit2(permit, sig.clone())
1054 .unwrap();
1055
1056 assert_eq!(opts.transfer_type, UserTransferType::TransferFromPermit2);
1057 assert!(opts.permit.is_some());
1058 assert_eq!(opts.permit2_signature.as_ref().unwrap(), &sig);
1059 }
1060
1061 #[test]
1062 fn encoding_options_with_permit2_rejects_wrong_signature_length() {
1063 let details = PermitDetails::new(
1064 Bytes::copy_from_slice(&[0xaa; 20]),
1065 BigUint::from(1_000u32),
1066 BigUint::from(9_999_999u32),
1067 BigUint::from(0u32),
1068 );
1069 let permit = PermitSingle::new(
1070 details,
1071 Bytes::copy_from_slice(&[0xbb; 20]),
1072 BigUint::from(9_999_999u32),
1073 );
1074 let bad_sig = Bytes::copy_from_slice(&[0xcc; 64]); assert!(matches!(
1076 EncodingOptions::new(0.005).with_permit2(permit, bad_sig),
1077 Err(crate::error::FyndError::Protocol(_))
1078 ));
1079 }
1080
1081 #[test]
1082 fn encoding_options_with_vault_funds_sets_variant() {
1083 let opts = EncodingOptions::new(0.005).with_vault_funds();
1084 assert_eq!(opts.transfer_type, UserTransferType::UseVaultsFunds);
1085 assert!(opts.permit.is_none());
1086 assert!(opts.permit2_signature.is_none());
1087 }
1088
1089 fn sample_permit_single() -> PermitSingle {
1090 let details = PermitDetails::new(
1091 Bytes::copy_from_slice(&[0xaa; 20]),
1092 BigUint::from(1_000u32),
1093 BigUint::from(9_999_999u32),
1094 BigUint::from(0u32),
1095 );
1096 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(9_999_999u32))
1097 }
1098
1099 #[test]
1100 fn eip712_signing_hash_returns_32_bytes() {
1101 let permit = sample_permit_single();
1102 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1103 let hash = permit
1104 .eip712_signing_hash(1, &permit2_addr)
1105 .unwrap();
1106 assert_eq!(hash.len(), 32);
1107 assert_ne!(hash, [0u8; 32]);
1109 }
1110
1111 #[test]
1112 fn eip712_signing_hash_is_deterministic() {
1113 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1114 let h1 = sample_permit_single()
1115 .eip712_signing_hash(1, &permit2_addr)
1116 .unwrap();
1117 let h2 = sample_permit_single()
1118 .eip712_signing_hash(1, &permit2_addr)
1119 .unwrap();
1120 assert_eq!(h1, h2);
1121 }
1122
1123 #[test]
1124 fn eip712_signing_hash_differs_by_chain_id() {
1125 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1126 let h1 = sample_permit_single()
1127 .eip712_signing_hash(1, &permit2_addr)
1128 .unwrap();
1129 let h137 = sample_permit_single()
1130 .eip712_signing_hash(137, &permit2_addr)
1131 .unwrap();
1132 assert_ne!(h1, h137);
1133 }
1134
1135 #[test]
1136 fn eip712_signing_hash_invalid_permit2_address() {
1137 let permit = sample_permit_single();
1138 let bad_addr = Bytes::copy_from_slice(&[0xcc; 4]);
1139 assert!(matches!(
1140 permit.eip712_signing_hash(1, &bad_addr),
1141 Err(crate::error::FyndError::Protocol(_))
1142 ));
1143 }
1144
1145 #[test]
1146 fn eip712_signing_hash_invalid_token_address() {
1147 let details = PermitDetails::new(
1148 Bytes::copy_from_slice(&[0xaa; 4]), BigUint::from(1u32),
1150 BigUint::from(1u32),
1151 BigUint::from(0u32),
1152 );
1153 let permit =
1154 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(1u32));
1155 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1156 assert!(matches!(
1157 permit.eip712_signing_hash(1, &permit2_addr),
1158 Err(crate::error::FyndError::Protocol(_))
1159 ));
1160 }
1161
1162 #[test]
1163 fn eip712_signing_hash_amount_exceeds_uint160() {
1164 let oversized_amount = BigUint::from_bytes_be(&[0x01; 21]);
1166 let details = PermitDetails::new(
1167 Bytes::copy_from_slice(&[0xaa; 20]),
1168 oversized_amount,
1169 BigUint::from(1u32),
1170 BigUint::from(0u32),
1171 );
1172 let permit =
1173 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(1u32));
1174 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1175 assert!(matches!(
1176 permit.eip712_signing_hash(1, &permit2_addr),
1177 Err(crate::error::FyndError::Protocol(_))
1178 ));
1179 }
1180
1181 fn sample_fee_receiver() -> Bytes {
1186 Bytes::copy_from_slice(&[0x44; 20])
1187 }
1188
1189 fn sample_router_address() -> Bytes {
1190 Bytes::copy_from_slice(&[0x33; 20])
1191 }
1192
1193 fn sample_fee_params(bps: u16, receiver: Bytes) -> ClientFeeParams {
1194 ClientFeeParams::new(bps, receiver, BigUint::ZERO, 1_893_456_000)
1195 }
1196
1197 #[test]
1198 fn client_fee_with_client_fee_sets_fields() {
1199 let fee = ClientFeeParams::new(
1200 100,
1201 sample_fee_receiver(),
1202 BigUint::from(500_000u64),
1203 1_893_456_000,
1204 );
1205 let opts = EncodingOptions::new(0.01).with_client_fee(fee);
1206 assert!(opts.client_fee_params.is_some());
1207 let stored = opts.client_fee_params.as_ref().unwrap();
1208 assert_eq!(stored.bps, 100);
1209 assert_eq!(stored.max_contribution, BigUint::from(500_000u64));
1210 }
1211
1212 #[test]
1213 fn client_fee_signing_hash_returns_32_bytes() {
1214 let fee = sample_fee_params(100, sample_fee_receiver());
1215 let hash = fee
1216 .eip712_signing_hash(1, &sample_router_address())
1217 .unwrap();
1218 assert_eq!(hash.len(), 32);
1219 assert_ne!(hash, [0u8; 32]);
1220 }
1221
1222 #[test]
1223 fn client_fee_signing_hash_is_deterministic() {
1224 let fee = sample_fee_params(100, sample_fee_receiver());
1225 let h1 = fee
1226 .eip712_signing_hash(1, &sample_router_address())
1227 .unwrap();
1228 let h2 = fee
1229 .eip712_signing_hash(1, &sample_router_address())
1230 .unwrap();
1231 assert_eq!(h1, h2);
1232 }
1233
1234 #[test]
1235 fn client_fee_signing_hash_differs_by_chain_id() {
1236 let fee = sample_fee_params(100, sample_fee_receiver());
1237 let h1 = fee
1238 .eip712_signing_hash(1, &sample_router_address())
1239 .unwrap();
1240 let h137 = fee
1241 .eip712_signing_hash(137, &sample_router_address())
1242 .unwrap();
1243 assert_ne!(h1, h137);
1244 }
1245
1246 #[test]
1247 fn client_fee_signing_hash_differs_by_bps() {
1248 let h100 = sample_fee_params(100, sample_fee_receiver())
1249 .eip712_signing_hash(1, &sample_router_address())
1250 .unwrap();
1251 let h200 = sample_fee_params(200, sample_fee_receiver())
1252 .eip712_signing_hash(1, &sample_router_address())
1253 .unwrap();
1254 assert_ne!(h100, h200);
1255 }
1256
1257 #[test]
1258 fn client_fee_signing_hash_differs_by_receiver() {
1259 let other_receiver = Bytes::copy_from_slice(&[0x55; 20]);
1260 let h1 = sample_fee_params(100, sample_fee_receiver())
1261 .eip712_signing_hash(1, &sample_router_address())
1262 .unwrap();
1263 let h2 = sample_fee_params(100, other_receiver)
1264 .eip712_signing_hash(1, &sample_router_address())
1265 .unwrap();
1266 assert_ne!(h1, h2);
1267 }
1268
1269 #[test]
1270 fn client_fee_signing_hash_rejects_bad_receiver_address() {
1271 let bad_addr = Bytes::copy_from_slice(&[0x44; 4]);
1272 let fee = sample_fee_params(100, bad_addr);
1273 assert!(matches!(
1274 fee.eip712_signing_hash(1, &sample_router_address()),
1275 Err(crate::error::FyndError::Protocol(_))
1276 ));
1277 }
1278
1279 #[test]
1280 fn client_fee_signing_hash_rejects_bad_router_address() {
1281 let bad_addr = Bytes::copy_from_slice(&[0x33; 4]);
1282 let fee = sample_fee_params(100, sample_fee_receiver());
1283 assert!(matches!(
1284 fee.eip712_signing_hash(1, &bad_addr),
1285 Err(crate::error::FyndError::Protocol(_))
1286 ));
1287 }
1288}