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 pub(crate) price_guard: Option<PriceGuardConfig>,
273}
274
275impl EncodingOptions {
276 pub fn new(slippage: f64) -> Self {
281 Self {
282 slippage,
283 transfer_type: UserTransferType::TransferFrom,
284 permit: None,
285 permit2_signature: None,
286 client_fee_params: None,
287 price_guard: None,
288 }
289 }
290
291 pub fn with_permit2(
300 mut self,
301 permit: PermitSingle,
302 signature: bytes::Bytes,
303 ) -> Result<Self, crate::error::FyndError> {
304 if signature.len() != 65 {
305 return Err(crate::error::FyndError::Protocol(format!(
306 "Permit2 signature must be exactly 65 bytes, got {}",
307 signature.len()
308 )));
309 }
310 self.transfer_type = UserTransferType::TransferFromPermit2;
311 self.permit = Some(permit);
312 self.permit2_signature = Some(signature);
313 Ok(self)
314 }
315
316 pub fn with_vault_funds(mut self) -> Self {
318 self.transfer_type = UserTransferType::UseVaultsFunds;
319 self
320 }
321
322 pub fn with_client_fee(mut self, params: ClientFeeParams) -> Self {
324 self.client_fee_params = Some(params);
325 self
326 }
327
328 pub fn with_price_guard(mut self, config: PriceGuardConfig) -> Self {
332 self.price_guard = Some(config);
333 self
334 }
335}
336
337#[derive(Debug, Clone)]
341pub struct Transaction {
342 to: Bytes,
343 value: BigUint,
344 data: Vec<u8>,
345}
346
347impl Transaction {
348 pub fn new(to: Bytes, value: BigUint, data: Vec<u8>) -> Self {
354 Self { to, value, data }
355 }
356
357 pub fn to(&self) -> &Bytes {
359 &self.to
360 }
361
362 pub fn value(&self) -> &BigUint {
364 &self.value
365 }
366
367 pub fn data(&self) -> &[u8] {
369 &self.data
370 }
371}
372
373#[non_exhaustive]
381#[derive(Debug, Clone, Copy, PartialEq, Eq)]
382pub enum OrderSide {
383 Sell,
385}
386
387#[derive(Debug, Clone)]
396pub struct Order {
397 token_in: Bytes,
398 token_out: Bytes,
399 amount: BigUint,
400 side: OrderSide,
401 sender: Bytes,
402 receiver: Option<Bytes>,
403}
404
405impl Order {
406 pub fn new(
415 token_in: Bytes,
416 token_out: Bytes,
417 amount: BigUint,
418 side: OrderSide,
419 sender: Bytes,
420 receiver: Option<Bytes>,
421 ) -> Self {
422 Self { token_in, token_out, amount, side, sender, receiver }
423 }
424
425 pub fn token_in(&self) -> &Bytes {
427 &self.token_in
428 }
429
430 pub fn token_out(&self) -> &Bytes {
432 &self.token_out
433 }
434
435 pub fn amount(&self) -> &BigUint {
437 &self.amount
438 }
439
440 pub fn side(&self) -> OrderSide {
442 self.side
443 }
444
445 pub fn sender(&self) -> &Bytes {
447 &self.sender
448 }
449
450 pub fn receiver(&self) -> Option<&Bytes> {
453 self.receiver.as_ref()
454 }
455}
456
457pub use fynd_rpc_types::PriceGuardConfig;
462
463#[derive(Debug, Clone, Default)]
467pub struct QuoteOptions {
468 pub(crate) timeout_ms: Option<u64>,
469 pub(crate) min_responses: Option<usize>,
470 pub(crate) max_gas: Option<BigUint>,
471 pub(crate) encoding_options: Option<EncodingOptions>,
472}
473
474impl QuoteOptions {
475 pub fn with_timeout_ms(mut self, ms: u64) -> Self {
477 self.timeout_ms = Some(ms);
478 self
479 }
480
481 pub fn with_min_responses(mut self, n: usize) -> Self {
486 self.min_responses = Some(n);
487 self
488 }
489
490 pub fn with_max_gas(mut self, gas: BigUint) -> Self {
492 self.max_gas = Some(gas);
493 self
494 }
495
496 pub fn with_encoding_options(mut self, opts: EncodingOptions) -> Self {
499 self.encoding_options = Some(opts);
500 self
501 }
502
503 pub fn timeout_ms(&self) -> Option<u64> {
505 self.timeout_ms
506 }
507
508 pub fn min_responses(&self) -> Option<usize> {
510 self.min_responses
511 }
512
513 pub fn max_gas(&self) -> Option<&BigUint> {
515 self.max_gas.as_ref()
516 }
517}
518
519#[derive(Debug, Clone)]
521pub struct QuoteParams {
522 pub(crate) order: Order,
523 pub(crate) options: QuoteOptions,
524}
525
526impl QuoteParams {
527 pub fn new(order: Order, options: QuoteOptions) -> Self {
529 Self { order, options }
530 }
531}
532
533#[derive(Debug, Clone)]
538pub struct BatchQuoteParams {
539 pub(crate) orders: Vec<Order>,
540 pub(crate) options: QuoteOptions,
541}
542
543impl BatchQuoteParams {
544 pub fn new(orders: Vec<Order>, options: QuoteOptions) -> Self {
549 Self { orders, options }
550 }
551}
552
553#[derive(Debug, Clone, Copy, PartialEq, Eq)]
559pub enum BackendKind {
560 Fynd,
562 Turbine,
564}
565
566#[derive(Debug, Clone, Copy, PartialEq, Eq)]
568pub enum QuoteStatus {
569 Success,
571 NoRouteFound,
573 InsufficientLiquidity,
575 Timeout,
577 NotReady,
579 PriceCheckFailed,
581}
582
583#[derive(Debug, Clone)]
588pub struct BlockInfo {
589 number: u64,
590 hash: String,
591 timestamp: u64,
592}
593
594impl BlockInfo {
595 pub fn number(&self) -> u64 {
597 self.number
598 }
599
600 pub fn hash(&self) -> &str {
602 &self.hash
603 }
604
605 pub fn timestamp(&self) -> u64 {
607 self.timestamp
608 }
609
610 pub fn new(number: u64, hash: String, timestamp: u64) -> Self {
612 Self { number, hash, timestamp }
613 }
614}
615
616#[derive(Debug, Clone)]
618pub struct Swap {
619 component_id: String,
620 protocol: String,
621 token_in: Bytes,
622 token_out: Bytes,
623 amount_in: BigUint,
624 amount_out: BigUint,
625 gas_estimate: BigUint,
626 #[allow(dead_code)]
627 split: f64,
628}
629
630impl Swap {
631 pub fn component_id(&self) -> &str {
633 &self.component_id
634 }
635
636 pub fn protocol(&self) -> &str {
638 &self.protocol
639 }
640
641 pub fn token_in(&self) -> &Bytes {
643 &self.token_in
644 }
645
646 pub fn token_out(&self) -> &Bytes {
648 &self.token_out
649 }
650
651 pub fn amount_in(&self) -> &BigUint {
653 &self.amount_in
654 }
655
656 pub fn amount_out(&self) -> &BigUint {
658 &self.amount_out
659 }
660
661 pub fn gas_estimate(&self) -> &BigUint {
663 &self.gas_estimate
664 }
665
666 #[allow(clippy::too_many_arguments)]
668 pub fn new(
669 component_id: String,
670 protocol: String,
671 token_in: Bytes,
672 token_out: Bytes,
673 amount_in: BigUint,
674 amount_out: BigUint,
675 gas_estimate: BigUint,
676 split: f64,
677 ) -> Self {
678 Self {
679 component_id,
680 protocol,
681 token_in,
682 token_out,
683 amount_in,
684 amount_out,
685 gas_estimate,
686 split,
687 }
688 }
689}
690
691#[derive(Debug, Clone)]
695pub struct Route {
696 swaps: Vec<Swap>,
697}
698
699impl Route {
700 pub fn swaps(&self) -> &[Swap] {
702 &self.swaps
703 }
704
705 pub fn new(swaps: Vec<Swap>) -> Self {
707 Self { swaps }
708 }
709}
710
711#[derive(Debug, Clone)]
715pub struct FeeBreakdown {
716 router_fee: BigUint,
717 client_fee: BigUint,
718 max_slippage: BigUint,
719 min_amount_received: BigUint,
720}
721
722impl FeeBreakdown {
723 pub(crate) fn new(
724 router_fee: BigUint,
725 client_fee: BigUint,
726 max_slippage: BigUint,
727 min_amount_received: BigUint,
728 ) -> Self {
729 Self { router_fee, client_fee, max_slippage, min_amount_received }
730 }
731
732 pub fn router_fee(&self) -> &BigUint {
734 &self.router_fee
735 }
736
737 pub fn client_fee(&self) -> &BigUint {
739 &self.client_fee
740 }
741
742 pub fn max_slippage(&self) -> &BigUint {
744 &self.max_slippage
745 }
746
747 pub fn min_amount_received(&self) -> &BigUint {
750 &self.min_amount_received
751 }
752}
753
754#[derive(Debug, Clone)]
756pub struct Quote {
757 order_id: String,
758 status: QuoteStatus,
759 backend: BackendKind,
760 route: Option<Route>,
761 amount_in: BigUint,
762 amount_out: BigUint,
763 gas_estimate: BigUint,
764 amount_out_net_gas: BigUint,
765 price_impact_bps: Option<i32>,
766 block: BlockInfo,
767 token_out: Bytes,
770 receiver: Bytes,
774 transaction: Option<Transaction>,
777 fee_breakdown: Option<FeeBreakdown>,
779 pub(crate) solve_time_ms: u64,
782}
783
784impl Quote {
785 pub fn order_id(&self) -> &str {
787 &self.order_id
788 }
789
790 pub fn status(&self) -> QuoteStatus {
792 self.status
793 }
794
795 pub fn backend(&self) -> BackendKind {
797 self.backend
798 }
799
800 pub fn route(&self) -> Option<&Route> {
802 self.route.as_ref()
803 }
804
805 pub fn amount_in(&self) -> &BigUint {
807 &self.amount_in
808 }
809
810 pub fn amount_out(&self) -> &BigUint {
812 &self.amount_out
813 }
814
815 pub fn gas_estimate(&self) -> &BigUint {
817 &self.gas_estimate
818 }
819
820 pub fn amount_out_net_gas(&self) -> &BigUint {
825 &self.amount_out_net_gas
826 }
827
828 pub fn price_impact_bps(&self) -> Option<i32> {
830 self.price_impact_bps
831 }
832
833 pub fn block(&self) -> &BlockInfo {
835 &self.block
836 }
837
838 pub fn token_out(&self) -> &Bytes {
843 &self.token_out
844 }
845
846 pub fn receiver(&self) -> &Bytes {
853 &self.receiver
854 }
855
856 pub fn transaction(&self) -> Option<&Transaction> {
861 self.transaction.as_ref()
862 }
863
864 pub fn fee_breakdown(&self) -> Option<&FeeBreakdown> {
869 self.fee_breakdown.as_ref()
870 }
871
872 pub fn solve_time_ms(&self) -> u64 {
876 self.solve_time_ms
877 }
878
879 #[allow(clippy::too_many_arguments)]
881 pub fn new(
882 order_id: String,
883 status: QuoteStatus,
884 backend: BackendKind,
885 route: Option<Route>,
886 amount_in: BigUint,
887 amount_out: BigUint,
888 gas_estimate: BigUint,
889 amount_out_net_gas: BigUint,
890 price_impact_bps: Option<i32>,
891 block: BlockInfo,
892 token_out: Bytes,
893 receiver: Bytes,
894 transaction: Option<Transaction>,
895 fee_breakdown: Option<FeeBreakdown>,
896 ) -> Self {
897 Self {
898 order_id,
899 status,
900 backend,
901 route,
902 amount_in,
903 amount_out,
904 gas_estimate,
905 amount_out_net_gas,
906 price_impact_bps,
907 block,
908 token_out,
909 receiver,
910 transaction,
911 fee_breakdown,
912 solve_time_ms: 0,
913 }
914 }
915}
916
917#[derive(Debug, Clone)]
919pub struct InstanceInfo {
920 router_address: bytes::Bytes,
922 permit2_address: bytes::Bytes,
924 chain_id: u64,
926}
927
928impl InstanceInfo {
929 pub(crate) fn new(
930 router_address: bytes::Bytes,
931 permit2_address: bytes::Bytes,
932 chain_id: u64,
933 ) -> Self {
934 Self { router_address, permit2_address, chain_id }
935 }
936
937 pub fn router_address(&self) -> &bytes::Bytes {
939 &self.router_address
940 }
941
942 pub fn permit2_address(&self) -> &bytes::Bytes {
944 &self.permit2_address
945 }
946
947 pub fn chain_id(&self) -> u64 {
949 self.chain_id
950 }
951}
952
953#[derive(Debug, Clone)]
955pub struct HealthStatus {
956 healthy: bool,
957 last_update_ms: u64,
958 num_solver_pools: usize,
959 derived_data_ready: bool,
960 gas_price_age_ms: Option<u64>,
961}
962
963impl HealthStatus {
964 pub fn healthy(&self) -> bool {
966 self.healthy
967 }
968
969 pub fn last_update_ms(&self) -> u64 {
971 self.last_update_ms
972 }
973
974 pub fn num_solver_pools(&self) -> usize {
976 self.num_solver_pools
977 }
978
979 pub fn derived_data_ready(&self) -> bool {
985 self.derived_data_ready
986 }
987
988 pub fn gas_price_age_ms(&self) -> Option<u64> {
990 self.gas_price_age_ms
991 }
992
993 pub(crate) fn new(
994 healthy: bool,
995 last_update_ms: u64,
996 num_solver_pools: usize,
997 derived_data_ready: bool,
998 gas_price_age_ms: Option<u64>,
999 ) -> Self {
1000 Self { healthy, last_update_ms, num_solver_pools, derived_data_ready, gas_price_age_ms }
1001 }
1002}
1003
1004#[cfg(test)]
1005mod tests {
1006 use num_bigint::BigUint;
1007
1008 use super::*;
1009
1010 fn addr(bytes: &[u8; 20]) -> Bytes {
1011 Bytes::copy_from_slice(bytes)
1012 }
1013
1014 #[test]
1015 fn order_new_and_getters() {
1016 let token_in = addr(&[0xaa; 20]);
1017 let token_out = addr(&[0xbb; 20]);
1018 let amount = BigUint::from(1_000_000u64);
1019 let sender = addr(&[0xcc; 20]);
1020
1021 let order = Order::new(
1022 token_in.clone(),
1023 token_out.clone(),
1024 amount.clone(),
1025 OrderSide::Sell,
1026 sender.clone(),
1027 None,
1028 );
1029
1030 assert_eq!(order.token_in(), &token_in);
1031 assert_eq!(order.token_out(), &token_out);
1032 assert_eq!(order.amount(), &amount);
1033 assert_eq!(order.sender(), &sender);
1034 assert!(order.receiver().is_none());
1035 assert_eq!(order.side(), OrderSide::Sell);
1036 }
1037
1038 #[test]
1039 fn order_with_explicit_receiver() {
1040 let receiver = Bytes::copy_from_slice(&[0xdd; 20]);
1041 let order = Order::new(
1042 Bytes::copy_from_slice(&[0xaa; 20]),
1043 Bytes::copy_from_slice(&[0xbb; 20]),
1044 BigUint::from(1u32),
1045 OrderSide::Sell,
1046 Bytes::copy_from_slice(&[0xcc; 20]),
1047 Some(receiver.clone()),
1048 );
1049 assert_eq!(order.receiver(), Some(&receiver));
1050 }
1051
1052 #[test]
1053 fn quote_options_builder() {
1054 let opts = QuoteOptions::default()
1055 .with_timeout_ms(500)
1056 .with_min_responses(2)
1057 .with_max_gas(BigUint::from(1_000_000u64));
1058
1059 assert_eq!(opts.timeout_ms(), Some(500));
1060 assert_eq!(opts.min_responses(), Some(2));
1061 assert_eq!(opts.max_gas(), Some(&BigUint::from(1_000_000u64)));
1062 }
1063
1064 #[test]
1065 fn quote_options_default_all_none() {
1066 let opts = QuoteOptions::default();
1067 assert!(opts.timeout_ms().is_none());
1068 assert!(opts.min_responses().is_none());
1069 assert!(opts.max_gas().is_none());
1070 }
1071
1072 #[test]
1073 fn encoding_options_with_permit2_sets_fields() {
1074 let token = Bytes::copy_from_slice(&[0xaa; 20]);
1075 let spender = Bytes::copy_from_slice(&[0xbb; 20]);
1076 let sig = Bytes::copy_from_slice(&[0xcc; 65]);
1077 let details = PermitDetails::new(
1078 token,
1079 BigUint::from(1_000u32),
1080 BigUint::from(9_999_999u32),
1081 BigUint::from(0u32),
1082 );
1083 let permit = PermitSingle::new(details, spender, BigUint::from(9_999_999u32));
1084
1085 let opts = EncodingOptions::new(0.005)
1086 .with_permit2(permit, sig.clone())
1087 .unwrap();
1088
1089 assert_eq!(opts.transfer_type, UserTransferType::TransferFromPermit2);
1090 assert!(opts.permit.is_some());
1091 assert_eq!(opts.permit2_signature.as_ref().unwrap(), &sig);
1092 }
1093
1094 #[test]
1095 fn encoding_options_with_permit2_rejects_wrong_signature_length() {
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 let permit = PermitSingle::new(
1103 details,
1104 Bytes::copy_from_slice(&[0xbb; 20]),
1105 BigUint::from(9_999_999u32),
1106 );
1107 let bad_sig = Bytes::copy_from_slice(&[0xcc; 64]); assert!(matches!(
1109 EncodingOptions::new(0.005).with_permit2(permit, bad_sig),
1110 Err(crate::error::FyndError::Protocol(_))
1111 ));
1112 }
1113
1114 #[test]
1115 fn encoding_options_with_vault_funds_sets_variant() {
1116 let opts = EncodingOptions::new(0.005).with_vault_funds();
1117 assert_eq!(opts.transfer_type, UserTransferType::UseVaultsFunds);
1118 assert!(opts.permit.is_none());
1119 assert!(opts.permit2_signature.is_none());
1120 }
1121
1122 fn sample_permit_single() -> PermitSingle {
1123 let details = PermitDetails::new(
1124 Bytes::copy_from_slice(&[0xaa; 20]),
1125 BigUint::from(1_000u32),
1126 BigUint::from(9_999_999u32),
1127 BigUint::from(0u32),
1128 );
1129 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(9_999_999u32))
1130 }
1131
1132 #[test]
1133 fn eip712_signing_hash_returns_32_bytes() {
1134 let permit = sample_permit_single();
1135 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1136 let hash = permit
1137 .eip712_signing_hash(1, &permit2_addr)
1138 .unwrap();
1139 assert_eq!(hash.len(), 32);
1140 assert_ne!(hash, [0u8; 32]);
1142 }
1143
1144 #[test]
1145 fn eip712_signing_hash_is_deterministic() {
1146 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1147 let h1 = sample_permit_single()
1148 .eip712_signing_hash(1, &permit2_addr)
1149 .unwrap();
1150 let h2 = sample_permit_single()
1151 .eip712_signing_hash(1, &permit2_addr)
1152 .unwrap();
1153 assert_eq!(h1, h2);
1154 }
1155
1156 #[test]
1157 fn eip712_signing_hash_differs_by_chain_id() {
1158 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1159 let h1 = sample_permit_single()
1160 .eip712_signing_hash(1, &permit2_addr)
1161 .unwrap();
1162 let h137 = sample_permit_single()
1163 .eip712_signing_hash(137, &permit2_addr)
1164 .unwrap();
1165 assert_ne!(h1, h137);
1166 }
1167
1168 #[test]
1169 fn eip712_signing_hash_invalid_permit2_address() {
1170 let permit = sample_permit_single();
1171 let bad_addr = Bytes::copy_from_slice(&[0xcc; 4]);
1172 assert!(matches!(
1173 permit.eip712_signing_hash(1, &bad_addr),
1174 Err(crate::error::FyndError::Protocol(_))
1175 ));
1176 }
1177
1178 #[test]
1179 fn eip712_signing_hash_invalid_token_address() {
1180 let details = PermitDetails::new(
1181 Bytes::copy_from_slice(&[0xaa; 4]), BigUint::from(1u32),
1183 BigUint::from(1u32),
1184 BigUint::from(0u32),
1185 );
1186 let permit =
1187 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(1u32));
1188 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1189 assert!(matches!(
1190 permit.eip712_signing_hash(1, &permit2_addr),
1191 Err(crate::error::FyndError::Protocol(_))
1192 ));
1193 }
1194
1195 #[test]
1196 fn eip712_signing_hash_amount_exceeds_uint160() {
1197 let oversized_amount = BigUint::from_bytes_be(&[0x01; 21]);
1199 let details = PermitDetails::new(
1200 Bytes::copy_from_slice(&[0xaa; 20]),
1201 oversized_amount,
1202 BigUint::from(1u32),
1203 BigUint::from(0u32),
1204 );
1205 let permit =
1206 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(1u32));
1207 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1208 assert!(matches!(
1209 permit.eip712_signing_hash(1, &permit2_addr),
1210 Err(crate::error::FyndError::Protocol(_))
1211 ));
1212 }
1213
1214 fn sample_fee_receiver() -> Bytes {
1219 Bytes::copy_from_slice(&[0x44; 20])
1220 }
1221
1222 fn sample_router_address() -> Bytes {
1223 Bytes::copy_from_slice(&[0x33; 20])
1224 }
1225
1226 fn sample_fee_params(bps: u16, receiver: Bytes) -> ClientFeeParams {
1227 ClientFeeParams::new(bps, receiver, BigUint::ZERO, 1_893_456_000)
1228 }
1229
1230 #[test]
1231 fn client_fee_with_client_fee_sets_fields() {
1232 let fee = ClientFeeParams::new(
1233 100,
1234 sample_fee_receiver(),
1235 BigUint::from(500_000u64),
1236 1_893_456_000,
1237 );
1238 let opts = EncodingOptions::new(0.01).with_client_fee(fee);
1239 assert!(opts.client_fee_params.is_some());
1240 let stored = opts.client_fee_params.as_ref().unwrap();
1241 assert_eq!(stored.bps, 100);
1242 assert_eq!(stored.max_contribution, BigUint::from(500_000u64));
1243 }
1244
1245 #[test]
1246 fn client_fee_signing_hash_returns_32_bytes() {
1247 let fee = sample_fee_params(100, sample_fee_receiver());
1248 let hash = fee
1249 .eip712_signing_hash(1, &sample_router_address())
1250 .unwrap();
1251 assert_eq!(hash.len(), 32);
1252 assert_ne!(hash, [0u8; 32]);
1253 }
1254
1255 #[test]
1256 fn client_fee_signing_hash_is_deterministic() {
1257 let fee = sample_fee_params(100, sample_fee_receiver());
1258 let h1 = fee
1259 .eip712_signing_hash(1, &sample_router_address())
1260 .unwrap();
1261 let h2 = fee
1262 .eip712_signing_hash(1, &sample_router_address())
1263 .unwrap();
1264 assert_eq!(h1, h2);
1265 }
1266
1267 #[test]
1268 fn client_fee_signing_hash_differs_by_chain_id() {
1269 let fee = sample_fee_params(100, sample_fee_receiver());
1270 let h1 = fee
1271 .eip712_signing_hash(1, &sample_router_address())
1272 .unwrap();
1273 let h137 = fee
1274 .eip712_signing_hash(137, &sample_router_address())
1275 .unwrap();
1276 assert_ne!(h1, h137);
1277 }
1278
1279 #[test]
1280 fn client_fee_signing_hash_differs_by_bps() {
1281 let h100 = sample_fee_params(100, sample_fee_receiver())
1282 .eip712_signing_hash(1, &sample_router_address())
1283 .unwrap();
1284 let h200 = sample_fee_params(200, sample_fee_receiver())
1285 .eip712_signing_hash(1, &sample_router_address())
1286 .unwrap();
1287 assert_ne!(h100, h200);
1288 }
1289
1290 #[test]
1291 fn client_fee_signing_hash_differs_by_receiver() {
1292 let other_receiver = Bytes::copy_from_slice(&[0x55; 20]);
1293 let h1 = sample_fee_params(100, sample_fee_receiver())
1294 .eip712_signing_hash(1, &sample_router_address())
1295 .unwrap();
1296 let h2 = sample_fee_params(100, other_receiver)
1297 .eip712_signing_hash(1, &sample_router_address())
1298 .unwrap();
1299 assert_ne!(h1, h2);
1300 }
1301
1302 #[test]
1303 fn client_fee_signing_hash_rejects_bad_receiver_address() {
1304 let bad_addr = Bytes::copy_from_slice(&[0x44; 4]);
1305 let fee = sample_fee_params(100, bad_addr);
1306 assert!(matches!(
1307 fee.eip712_signing_hash(1, &sample_router_address()),
1308 Err(crate::error::FyndError::Protocol(_))
1309 ));
1310 }
1311
1312 #[test]
1313 fn client_fee_signing_hash_rejects_bad_router_address() {
1314 let bad_addr = Bytes::copy_from_slice(&[0x33; 4]);
1315 let fee = sample_fee_params(100, sample_fee_receiver());
1316 assert!(matches!(
1317 fee.eip712_signing_hash(1, &bad_addr),
1318 Err(crate::error::FyndError::Protocol(_))
1319 ));
1320 }
1321}