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
372#[derive(Debug, Clone)]
381pub struct Order {
382 token_in: Bytes,
383 token_out: Bytes,
384 amount: BigUint,
385 side: OrderSide,
386 sender: Bytes,
387 receiver: Option<Bytes>,
388}
389
390impl Order {
391 pub fn new(
400 token_in: Bytes,
401 token_out: Bytes,
402 amount: BigUint,
403 side: OrderSide,
404 sender: Bytes,
405 receiver: Option<Bytes>,
406 ) -> Self {
407 Self { token_in, token_out, amount, side, sender, receiver }
408 }
409
410 pub fn token_in(&self) -> &Bytes {
412 &self.token_in
413 }
414
415 pub fn token_out(&self) -> &Bytes {
417 &self.token_out
418 }
419
420 pub fn amount(&self) -> &BigUint {
422 &self.amount
423 }
424
425 pub fn side(&self) -> OrderSide {
427 self.side
428 }
429
430 pub fn sender(&self) -> &Bytes {
432 &self.sender
433 }
434
435 pub fn receiver(&self) -> Option<&Bytes> {
438 self.receiver.as_ref()
439 }
440}
441
442#[derive(Debug, Clone, Default)]
446pub struct QuoteOptions {
447 pub(crate) timeout_ms: Option<u64>,
448 pub(crate) min_responses: Option<usize>,
449 pub(crate) max_gas: Option<BigUint>,
450 pub(crate) encoding_options: Option<EncodingOptions>,
451}
452
453impl QuoteOptions {
454 pub fn with_timeout_ms(mut self, ms: u64) -> Self {
456 self.timeout_ms = Some(ms);
457 self
458 }
459
460 pub fn with_min_responses(mut self, n: usize) -> Self {
465 self.min_responses = Some(n);
466 self
467 }
468
469 pub fn with_max_gas(mut self, gas: BigUint) -> Self {
471 self.max_gas = Some(gas);
472 self
473 }
474
475 pub fn with_encoding_options(mut self, opts: EncodingOptions) -> Self {
478 self.encoding_options = Some(opts);
479 self
480 }
481
482 pub fn timeout_ms(&self) -> Option<u64> {
484 self.timeout_ms
485 }
486
487 pub fn min_responses(&self) -> Option<usize> {
489 self.min_responses
490 }
491
492 pub fn max_gas(&self) -> Option<&BigUint> {
494 self.max_gas.as_ref()
495 }
496}
497
498#[derive(Debug, Clone)]
500pub struct QuoteParams {
501 pub(crate) order: Order,
502 pub(crate) options: QuoteOptions,
503}
504
505impl QuoteParams {
506 pub fn new(order: Order, options: QuoteOptions) -> Self {
508 Self { order, options }
509 }
510}
511
512#[derive(Debug, Clone)]
517pub struct BatchQuoteParams {
518 pub(crate) orders: Vec<Order>,
519 pub(crate) options: QuoteOptions,
520}
521
522impl BatchQuoteParams {
523 pub fn new(orders: Vec<Order>, options: QuoteOptions) -> Self {
528 Self { orders, options }
529 }
530}
531
532#[derive(Debug, Clone, Copy, PartialEq, Eq)]
538pub enum BackendKind {
539 Fynd,
541 Turbine,
543}
544
545#[derive(Debug, Clone, Copy, PartialEq, Eq)]
547pub enum QuoteStatus {
548 Success,
550 NoRouteFound,
552 InsufficientLiquidity,
554 Timeout,
556 NotReady,
558}
559
560#[derive(Debug, Clone)]
565pub struct BlockInfo {
566 number: u64,
567 hash: String,
568 timestamp: u64,
569}
570
571impl BlockInfo {
572 pub fn number(&self) -> u64 {
574 self.number
575 }
576
577 pub fn hash(&self) -> &str {
579 &self.hash
580 }
581
582 pub fn timestamp(&self) -> u64 {
584 self.timestamp
585 }
586
587 pub(crate) fn new(number: u64, hash: String, timestamp: u64) -> Self {
588 Self { number, hash, timestamp }
589 }
590}
591
592#[derive(Debug, Clone)]
594pub struct Swap {
595 component_id: String,
596 protocol: String,
597 token_in: Bytes,
598 token_out: Bytes,
599 amount_in: BigUint,
600 amount_out: BigUint,
601 gas_estimate: BigUint,
602 #[allow(dead_code)]
603 split: f64,
604}
605
606impl Swap {
607 pub fn component_id(&self) -> &str {
609 &self.component_id
610 }
611
612 pub fn protocol(&self) -> &str {
614 &self.protocol
615 }
616
617 pub fn token_in(&self) -> &Bytes {
619 &self.token_in
620 }
621
622 pub fn token_out(&self) -> &Bytes {
624 &self.token_out
625 }
626
627 pub fn amount_in(&self) -> &BigUint {
629 &self.amount_in
630 }
631
632 pub fn amount_out(&self) -> &BigUint {
634 &self.amount_out
635 }
636
637 pub fn gas_estimate(&self) -> &BigUint {
639 &self.gas_estimate
640 }
641
642 #[allow(clippy::too_many_arguments)]
643 pub(crate) fn new(
644 component_id: String,
645 protocol: String,
646 token_in: Bytes,
647 token_out: Bytes,
648 amount_in: BigUint,
649 amount_out: BigUint,
650 gas_estimate: BigUint,
651 split: f64,
652 ) -> Self {
653 Self {
654 component_id,
655 protocol,
656 token_in,
657 token_out,
658 amount_in,
659 amount_out,
660 gas_estimate,
661 split,
662 }
663 }
664}
665
666#[derive(Debug, Clone)]
670pub struct Route {
671 swaps: Vec<Swap>,
672}
673
674impl Route {
675 pub fn swaps(&self) -> &[Swap] {
677 &self.swaps
678 }
679
680 pub(crate) fn new(swaps: Vec<Swap>) -> Self {
681 Self { swaps }
682 }
683}
684
685#[derive(Debug, Clone)]
689pub struct FeeBreakdown {
690 router_fee: BigUint,
691 client_fee: BigUint,
692 max_slippage: BigUint,
693 min_amount_received: BigUint,
694}
695
696impl FeeBreakdown {
697 pub(crate) fn new(
698 router_fee: BigUint,
699 client_fee: BigUint,
700 max_slippage: BigUint,
701 min_amount_received: BigUint,
702 ) -> Self {
703 Self { router_fee, client_fee, max_slippage, min_amount_received }
704 }
705
706 pub fn router_fee(&self) -> &BigUint {
708 &self.router_fee
709 }
710
711 pub fn client_fee(&self) -> &BigUint {
713 &self.client_fee
714 }
715
716 pub fn max_slippage(&self) -> &BigUint {
718 &self.max_slippage
719 }
720
721 pub fn min_amount_received(&self) -> &BigUint {
724 &self.min_amount_received
725 }
726}
727
728#[derive(Debug, Clone)]
730pub struct Quote {
731 order_id: String,
732 status: QuoteStatus,
733 backend: BackendKind,
734 route: Option<Route>,
735 amount_in: BigUint,
736 amount_out: BigUint,
737 gas_estimate: BigUint,
738 amount_out_net_gas: BigUint,
739 price_impact_bps: Option<i32>,
740 block: BlockInfo,
741 token_out: Bytes,
744 receiver: Bytes,
748 transaction: Option<Transaction>,
751 fee_breakdown: Option<FeeBreakdown>,
753 pub(crate) solve_time_ms: u64,
756}
757
758impl Quote {
759 pub fn order_id(&self) -> &str {
761 &self.order_id
762 }
763
764 pub fn status(&self) -> QuoteStatus {
766 self.status
767 }
768
769 pub fn backend(&self) -> BackendKind {
771 self.backend
772 }
773
774 pub fn route(&self) -> Option<&Route> {
776 self.route.as_ref()
777 }
778
779 pub fn amount_in(&self) -> &BigUint {
781 &self.amount_in
782 }
783
784 pub fn amount_out(&self) -> &BigUint {
786 &self.amount_out
787 }
788
789 pub fn gas_estimate(&self) -> &BigUint {
791 &self.gas_estimate
792 }
793
794 pub fn amount_out_net_gas(&self) -> &BigUint {
799 &self.amount_out_net_gas
800 }
801
802 pub fn price_impact_bps(&self) -> Option<i32> {
804 self.price_impact_bps
805 }
806
807 pub fn block(&self) -> &BlockInfo {
809 &self.block
810 }
811
812 pub fn token_out(&self) -> &Bytes {
817 &self.token_out
818 }
819
820 pub fn receiver(&self) -> &Bytes {
827 &self.receiver
828 }
829
830 pub fn transaction(&self) -> Option<&Transaction> {
835 self.transaction.as_ref()
836 }
837
838 pub fn fee_breakdown(&self) -> Option<&FeeBreakdown> {
843 self.fee_breakdown.as_ref()
844 }
845
846 pub fn solve_time_ms(&self) -> u64 {
850 self.solve_time_ms
851 }
852
853 #[allow(clippy::too_many_arguments)]
854 pub(crate) fn new(
855 order_id: String,
856 status: QuoteStatus,
857 backend: BackendKind,
858 route: Option<Route>,
859 amount_in: BigUint,
860 amount_out: BigUint,
861 gas_estimate: BigUint,
862 amount_out_net_gas: BigUint,
863 price_impact_bps: Option<i32>,
864 block: BlockInfo,
865 token_out: Bytes,
866 receiver: Bytes,
867 transaction: Option<Transaction>,
868 fee_breakdown: Option<FeeBreakdown>,
869 ) -> Self {
870 Self {
871 order_id,
872 status,
873 backend,
874 route,
875 amount_in,
876 amount_out,
877 gas_estimate,
878 amount_out_net_gas,
879 price_impact_bps,
880 block,
881 token_out,
882 receiver,
883 transaction,
884 fee_breakdown,
885 solve_time_ms: 0,
886 }
887 }
888}
889
890#[derive(Debug, Clone)]
892pub struct InstanceInfo {
893 router_address: bytes::Bytes,
895 permit2_address: bytes::Bytes,
897 chain_id: u64,
899}
900
901impl InstanceInfo {
902 pub(crate) fn new(
903 router_address: bytes::Bytes,
904 permit2_address: bytes::Bytes,
905 chain_id: u64,
906 ) -> Self {
907 Self { router_address, permit2_address, chain_id }
908 }
909
910 pub fn router_address(&self) -> &bytes::Bytes {
912 &self.router_address
913 }
914
915 pub fn permit2_address(&self) -> &bytes::Bytes {
917 &self.permit2_address
918 }
919
920 pub fn chain_id(&self) -> u64 {
922 self.chain_id
923 }
924}
925
926#[derive(Debug, Clone)]
928pub struct HealthStatus {
929 healthy: bool,
930 last_update_ms: u64,
931 num_solver_pools: usize,
932 derived_data_ready: bool,
933 gas_price_age_ms: Option<u64>,
934}
935
936impl HealthStatus {
937 pub fn healthy(&self) -> bool {
939 self.healthy
940 }
941
942 pub fn last_update_ms(&self) -> u64 {
944 self.last_update_ms
945 }
946
947 pub fn num_solver_pools(&self) -> usize {
949 self.num_solver_pools
950 }
951
952 pub fn derived_data_ready(&self) -> bool {
958 self.derived_data_ready
959 }
960
961 pub fn gas_price_age_ms(&self) -> Option<u64> {
963 self.gas_price_age_ms
964 }
965
966 pub(crate) fn new(
967 healthy: bool,
968 last_update_ms: u64,
969 num_solver_pools: usize,
970 derived_data_ready: bool,
971 gas_price_age_ms: Option<u64>,
972 ) -> Self {
973 Self { healthy, last_update_ms, num_solver_pools, derived_data_ready, gas_price_age_ms }
974 }
975}
976
977#[cfg(test)]
978mod tests {
979 use num_bigint::BigUint;
980
981 use super::*;
982
983 fn addr(bytes: &[u8; 20]) -> Bytes {
984 Bytes::copy_from_slice(bytes)
985 }
986
987 #[test]
988 fn order_new_and_getters() {
989 let token_in = addr(&[0xaa; 20]);
990 let token_out = addr(&[0xbb; 20]);
991 let amount = BigUint::from(1_000_000u64);
992 let sender = addr(&[0xcc; 20]);
993
994 let order = Order::new(
995 token_in.clone(),
996 token_out.clone(),
997 amount.clone(),
998 OrderSide::Sell,
999 sender.clone(),
1000 None,
1001 );
1002
1003 assert_eq!(order.token_in(), &token_in);
1004 assert_eq!(order.token_out(), &token_out);
1005 assert_eq!(order.amount(), &amount);
1006 assert_eq!(order.sender(), &sender);
1007 assert!(order.receiver().is_none());
1008 assert_eq!(order.side(), OrderSide::Sell);
1009 }
1010
1011 #[test]
1012 fn order_with_explicit_receiver() {
1013 let receiver = Bytes::copy_from_slice(&[0xdd; 20]);
1014 let order = Order::new(
1015 Bytes::copy_from_slice(&[0xaa; 20]),
1016 Bytes::copy_from_slice(&[0xbb; 20]),
1017 BigUint::from(1u32),
1018 OrderSide::Sell,
1019 Bytes::copy_from_slice(&[0xcc; 20]),
1020 Some(receiver.clone()),
1021 );
1022 assert_eq!(order.receiver(), Some(&receiver));
1023 }
1024
1025 #[test]
1026 fn quote_options_builder() {
1027 let opts = QuoteOptions::default()
1028 .with_timeout_ms(500)
1029 .with_min_responses(2)
1030 .with_max_gas(BigUint::from(1_000_000u64));
1031
1032 assert_eq!(opts.timeout_ms(), Some(500));
1033 assert_eq!(opts.min_responses(), Some(2));
1034 assert_eq!(opts.max_gas(), Some(&BigUint::from(1_000_000u64)));
1035 }
1036
1037 #[test]
1038 fn quote_options_default_all_none() {
1039 let opts = QuoteOptions::default();
1040 assert!(opts.timeout_ms().is_none());
1041 assert!(opts.min_responses().is_none());
1042 assert!(opts.max_gas().is_none());
1043 }
1044
1045 #[test]
1046 fn encoding_options_with_permit2_sets_fields() {
1047 let token = Bytes::copy_from_slice(&[0xaa; 20]);
1048 let spender = Bytes::copy_from_slice(&[0xbb; 20]);
1049 let sig = Bytes::copy_from_slice(&[0xcc; 65]);
1050 let details = PermitDetails::new(
1051 token,
1052 BigUint::from(1_000u32),
1053 BigUint::from(9_999_999u32),
1054 BigUint::from(0u32),
1055 );
1056 let permit = PermitSingle::new(details, spender, BigUint::from(9_999_999u32));
1057
1058 let opts = EncodingOptions::new(0.005)
1059 .with_permit2(permit, sig.clone())
1060 .unwrap();
1061
1062 assert_eq!(opts.transfer_type, UserTransferType::TransferFromPermit2);
1063 assert!(opts.permit.is_some());
1064 assert_eq!(opts.permit2_signature.as_ref().unwrap(), &sig);
1065 }
1066
1067 #[test]
1068 fn encoding_options_with_permit2_rejects_wrong_signature_length() {
1069 let details = PermitDetails::new(
1070 Bytes::copy_from_slice(&[0xaa; 20]),
1071 BigUint::from(1_000u32),
1072 BigUint::from(9_999_999u32),
1073 BigUint::from(0u32),
1074 );
1075 let permit = PermitSingle::new(
1076 details,
1077 Bytes::copy_from_slice(&[0xbb; 20]),
1078 BigUint::from(9_999_999u32),
1079 );
1080 let bad_sig = Bytes::copy_from_slice(&[0xcc; 64]); assert!(matches!(
1082 EncodingOptions::new(0.005).with_permit2(permit, bad_sig),
1083 Err(crate::error::FyndError::Protocol(_))
1084 ));
1085 }
1086
1087 #[test]
1088 fn encoding_options_with_vault_funds_sets_variant() {
1089 let opts = EncodingOptions::new(0.005).with_vault_funds();
1090 assert_eq!(opts.transfer_type, UserTransferType::UseVaultsFunds);
1091 assert!(opts.permit.is_none());
1092 assert!(opts.permit2_signature.is_none());
1093 }
1094
1095 fn sample_permit_single() -> PermitSingle {
1096 let details = PermitDetails::new(
1097 Bytes::copy_from_slice(&[0xaa; 20]),
1098 BigUint::from(1_000u32),
1099 BigUint::from(9_999_999u32),
1100 BigUint::from(0u32),
1101 );
1102 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(9_999_999u32))
1103 }
1104
1105 #[test]
1106 fn eip712_signing_hash_returns_32_bytes() {
1107 let permit = sample_permit_single();
1108 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1109 let hash = permit
1110 .eip712_signing_hash(1, &permit2_addr)
1111 .unwrap();
1112 assert_eq!(hash.len(), 32);
1113 assert_ne!(hash, [0u8; 32]);
1115 }
1116
1117 #[test]
1118 fn eip712_signing_hash_is_deterministic() {
1119 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1120 let h1 = sample_permit_single()
1121 .eip712_signing_hash(1, &permit2_addr)
1122 .unwrap();
1123 let h2 = sample_permit_single()
1124 .eip712_signing_hash(1, &permit2_addr)
1125 .unwrap();
1126 assert_eq!(h1, h2);
1127 }
1128
1129 #[test]
1130 fn eip712_signing_hash_differs_by_chain_id() {
1131 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1132 let h1 = sample_permit_single()
1133 .eip712_signing_hash(1, &permit2_addr)
1134 .unwrap();
1135 let h137 = sample_permit_single()
1136 .eip712_signing_hash(137, &permit2_addr)
1137 .unwrap();
1138 assert_ne!(h1, h137);
1139 }
1140
1141 #[test]
1142 fn eip712_signing_hash_invalid_permit2_address() {
1143 let permit = sample_permit_single();
1144 let bad_addr = Bytes::copy_from_slice(&[0xcc; 4]);
1145 assert!(matches!(
1146 permit.eip712_signing_hash(1, &bad_addr),
1147 Err(crate::error::FyndError::Protocol(_))
1148 ));
1149 }
1150
1151 #[test]
1152 fn eip712_signing_hash_invalid_token_address() {
1153 let details = PermitDetails::new(
1154 Bytes::copy_from_slice(&[0xaa; 4]), BigUint::from(1u32),
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 #[test]
1169 fn eip712_signing_hash_amount_exceeds_uint160() {
1170 let oversized_amount = BigUint::from_bytes_be(&[0x01; 21]);
1172 let details = PermitDetails::new(
1173 Bytes::copy_from_slice(&[0xaa; 20]),
1174 oversized_amount,
1175 BigUint::from(1u32),
1176 BigUint::from(0u32),
1177 );
1178 let permit =
1179 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(1u32));
1180 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1181 assert!(matches!(
1182 permit.eip712_signing_hash(1, &permit2_addr),
1183 Err(crate::error::FyndError::Protocol(_))
1184 ));
1185 }
1186
1187 fn sample_fee_receiver() -> Bytes {
1192 Bytes::copy_from_slice(&[0x44; 20])
1193 }
1194
1195 fn sample_router_address() -> Bytes {
1196 Bytes::copy_from_slice(&[0x33; 20])
1197 }
1198
1199 fn sample_fee_params(bps: u16, receiver: Bytes) -> ClientFeeParams {
1200 ClientFeeParams::new(bps, receiver, BigUint::ZERO, 1_893_456_000)
1201 }
1202
1203 #[test]
1204 fn client_fee_with_client_fee_sets_fields() {
1205 let fee = ClientFeeParams::new(
1206 100,
1207 sample_fee_receiver(),
1208 BigUint::from(500_000u64),
1209 1_893_456_000,
1210 );
1211 let opts = EncodingOptions::new(0.01).with_client_fee(fee);
1212 assert!(opts.client_fee_params.is_some());
1213 let stored = opts.client_fee_params.as_ref().unwrap();
1214 assert_eq!(stored.bps, 100);
1215 assert_eq!(stored.max_contribution, BigUint::from(500_000u64));
1216 }
1217
1218 #[test]
1219 fn client_fee_signing_hash_returns_32_bytes() {
1220 let fee = sample_fee_params(100, sample_fee_receiver());
1221 let hash = fee
1222 .eip712_signing_hash(1, &sample_router_address())
1223 .unwrap();
1224 assert_eq!(hash.len(), 32);
1225 assert_ne!(hash, [0u8; 32]);
1226 }
1227
1228 #[test]
1229 fn client_fee_signing_hash_is_deterministic() {
1230 let fee = sample_fee_params(100, sample_fee_receiver());
1231 let h1 = fee
1232 .eip712_signing_hash(1, &sample_router_address())
1233 .unwrap();
1234 let h2 = fee
1235 .eip712_signing_hash(1, &sample_router_address())
1236 .unwrap();
1237 assert_eq!(h1, h2);
1238 }
1239
1240 #[test]
1241 fn client_fee_signing_hash_differs_by_chain_id() {
1242 let fee = sample_fee_params(100, sample_fee_receiver());
1243 let h1 = fee
1244 .eip712_signing_hash(1, &sample_router_address())
1245 .unwrap();
1246 let h137 = fee
1247 .eip712_signing_hash(137, &sample_router_address())
1248 .unwrap();
1249 assert_ne!(h1, h137);
1250 }
1251
1252 #[test]
1253 fn client_fee_signing_hash_differs_by_bps() {
1254 let h100 = sample_fee_params(100, sample_fee_receiver())
1255 .eip712_signing_hash(1, &sample_router_address())
1256 .unwrap();
1257 let h200 = sample_fee_params(200, sample_fee_receiver())
1258 .eip712_signing_hash(1, &sample_router_address())
1259 .unwrap();
1260 assert_ne!(h100, h200);
1261 }
1262
1263 #[test]
1264 fn client_fee_signing_hash_differs_by_receiver() {
1265 let other_receiver = Bytes::copy_from_slice(&[0x55; 20]);
1266 let h1 = sample_fee_params(100, sample_fee_receiver())
1267 .eip712_signing_hash(1, &sample_router_address())
1268 .unwrap();
1269 let h2 = sample_fee_params(100, other_receiver)
1270 .eip712_signing_hash(1, &sample_router_address())
1271 .unwrap();
1272 assert_ne!(h1, h2);
1273 }
1274
1275 #[test]
1276 fn client_fee_signing_hash_rejects_bad_receiver_address() {
1277 let bad_addr = Bytes::copy_from_slice(&[0x44; 4]);
1278 let fee = sample_fee_params(100, bad_addr);
1279 assert!(matches!(
1280 fee.eip712_signing_hash(1, &sample_router_address()),
1281 Err(crate::error::FyndError::Protocol(_))
1282 ));
1283 }
1284
1285 #[test]
1286 fn client_fee_signing_hash_rejects_bad_router_address() {
1287 let bad_addr = Bytes::copy_from_slice(&[0x33; 4]);
1288 let fee = sample_fee_params(100, sample_fee_receiver());
1289 assert!(matches!(
1290 fee.eip712_signing_hash(1, &bad_addr),
1291 Err(crate::error::FyndError::Protocol(_))
1292 ));
1293 }
1294}