1use alloy::{
2 primitives::{keccak256, U256},
3 sol_types::SolValue,
4};
5use bytes::Bytes;
6use num_bigint::BigUint;
7
8use crate::{error::FyndError, mapping::biguint_to_u256};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum UserTransferType {
17 #[default]
19 TransferFrom,
20 TransferFromPermit2,
22 UseVaultsFunds,
24}
25
26#[derive(Debug, Clone)]
28pub struct PermitDetails {
29 pub(crate) token: bytes::Bytes,
30 pub(crate) amount: num_bigint::BigUint,
31 pub(crate) expiration: num_bigint::BigUint,
32 pub(crate) nonce: num_bigint::BigUint,
33}
34
35impl PermitDetails {
36 pub fn new(
44 token: bytes::Bytes,
45 amount: num_bigint::BigUint,
46 expiration: num_bigint::BigUint,
47 nonce: num_bigint::BigUint,
48 ) -> Self {
49 Self { token, amount, expiration, nonce }
50 }
51}
52
53#[derive(Debug, Clone)]
55pub struct PermitSingle {
56 pub(crate) details: PermitDetails,
57 pub(crate) spender: bytes::Bytes,
58 pub(crate) sig_deadline: num_bigint::BigUint,
59}
60
61impl PermitSingle {
62 pub fn new(
68 details: PermitDetails,
69 spender: bytes::Bytes,
70 sig_deadline: num_bigint::BigUint,
71 ) -> Self {
72 Self { details, spender, sig_deadline }
73 }
74
75 pub fn eip712_signing_hash(
88 &self,
89 chain_id: u64,
90 permit2_address: &bytes::Bytes,
91 ) -> Result<[u8; 32], crate::error::FyndError> {
92 use alloy::sol_types::{eip712_domain, SolStruct};
93
94 let permit2_addr = p2_bytes_to_address(permit2_address, "permit2_address")?;
95 let token = p2_bytes_to_address(&self.details.token, "token")?;
96 let spender = p2_bytes_to_address(&self.spender, "spender")?;
97
98 let amount = p2_biguint_to_uint160(&self.details.amount)?;
99 let expiration = p2_biguint_to_uint48(&self.details.expiration)?;
100 let nonce = p2_biguint_to_uint48(&self.details.nonce)?;
101 let sig_deadline = crate::mapping::biguint_to_u256(&self.sig_deadline);
102
103 let domain = eip712_domain! {
104 name: "Permit2",
105 chain_id: chain_id,
106 verifying_contract: permit2_addr,
107 };
108 #[allow(non_snake_case)]
109 let permit = permit2_sol::PermitSingle {
110 details: permit2_sol::PermitDetails { token, amount, expiration, nonce },
111 spender,
112 sigDeadline: sig_deadline,
113 };
114 Ok(permit.eip712_signing_hash(&domain).0)
115 }
116}
117
118const CLIENT_FEE_UNITS_PER_BPS: u64 = 10_000;
124
125const CLIENT_FEE_SIGNATURE_BYTES: usize = 65;
127
128#[derive(Debug, Clone)]
137pub struct ClientFeeParams {
138 pub(crate) bps: u16,
139 pub(crate) receiver: Bytes,
140 pub(crate) max_contribution: BigUint,
141 pub(crate) deadline: u64,
142}
143
144impl ClientFeeParams {
145 pub fn new(bps: u16, receiver: Bytes, max_contribution: BigUint, deadline: u64) -> Self {
147 Self { bps, receiver, max_contribution, deadline }
148 }
149
150 #[allow(clippy::too_many_arguments)]
174 pub fn eip712_signing_hash(
175 &self,
176 chain_id: u64,
177 router_address: &Bytes,
178 amount_in: &num_bigint::BigUint,
179 token_in: &Bytes,
180 token_out: &Bytes,
181 expected_amount_out: &num_bigint::BigUint,
182 min_amount_out: &num_bigint::BigUint,
183 receiver: &Bytes,
184 swaps_hash: &[u8; 32],
185 ) -> Result<[u8; 32], crate::error::FyndError> {
186 let router_addr = p2_bytes_to_address(router_address, "router_address")?;
187 let fee_receiver = p2_bytes_to_address(&self.receiver, "receiver")?;
188 let max_contrib = biguint_to_u256(&self.max_contribution);
189 let dl = U256::from(self.deadline);
190 let amount_in_u256 = biguint_to_u256(amount_in);
191 let token_in_addr = p2_bytes_to_address(token_in, "token_in")?;
192 let token_out_addr = p2_bytes_to_address(token_out, "token_out")?;
193 let expected_amount_out_u256 = biguint_to_u256(expected_amount_out);
194 let min_amount_out_u256 = biguint_to_u256(min_amount_out);
195 let receiver_addr = p2_bytes_to_address(receiver, "receiver")?;
196 let swaps_b256 = alloy::primitives::B256::from(*swaps_hash);
197
198 let type_hash = keccak256(
199 b"ClientFee(uint32 clientFeeBps,address clientFeeReceiver,\
200uint256 maxClientContribution,uint256 deadline,\
201uint256 amountIn,address tokenIn,address tokenOut,\
202uint256 expectedAmountOut,uint256 minAmountOut,address receiver,bytes swaps)",
203 );
204
205 let domain_type_hash = keccak256(
206 b"EIP712Domain(string name,string version,\
207uint256 chainId,address verifyingContract)",
208 );
209 let domain_separator = keccak256(
210 (
211 domain_type_hash,
212 keccak256(b"TychoRouter"),
213 keccak256(b"1"),
214 U256::from(chain_id),
215 router_addr,
216 )
217 .abi_encode(),
218 );
219
220 let struct_hash = keccak256(
221 (
222 type_hash,
223 U256::from(self.bps as u64 * CLIENT_FEE_UNITS_PER_BPS),
224 fee_receiver,
225 max_contrib,
226 dl,
227 amount_in_u256,
228 token_in_addr,
229 token_out_addr,
230 expected_amount_out_u256,
231 min_amount_out_u256,
232 receiver_addr,
233 swaps_b256,
234 )
235 .abi_encode(),
236 );
237
238 let mut data = [0u8; 66];
239 data[0] = 0x19;
240 data[1] = 0x01;
241 data[2..34].copy_from_slice(domain_separator.as_ref());
242 data[34..66].copy_from_slice(struct_hash.as_ref());
243 Ok(keccak256(data).0)
244 }
245}
246
247mod permit2_sol {
252 use alloy::sol;
253
254 sol! {
255 struct PermitDetails {
256 address token;
257 uint160 amount;
258 uint48 expiration;
259 uint48 nonce;
260 }
261 struct PermitSingle {
262 PermitDetails details;
263 address spender;
264 uint256 sigDeadline;
265 }
266 }
267}
268
269fn p2_bytes_to_address(
270 b: &bytes::Bytes,
271 field: &str,
272) -> Result<alloy::primitives::Address, crate::error::FyndError> {
273 let arr: [u8; 20] = b.as_ref().try_into().map_err(|_| {
274 crate::error::FyndError::Protocol(format!(
275 "expected 20-byte address for {field}, got {} bytes",
276 b.len()
277 ))
278 })?;
279 Ok(alloy::primitives::Address::from(arr))
280}
281
282fn p2_biguint_to_uint160(
283 n: &num_bigint::BigUint,
284) -> Result<alloy::primitives::Uint<160, 3>, crate::error::FyndError> {
285 let bytes = n.to_bytes_be();
286 if bytes.len() > 20 {
287 return Err(crate::error::FyndError::Protocol(format!(
288 "permit amount exceeds uint160 ({} bytes)",
289 bytes.len()
290 )));
291 }
292 let mut arr = [0u8; 20];
293 arr[20 - bytes.len()..].copy_from_slice(&bytes);
294 Ok(alloy::primitives::Uint::<160, 3>::from_be_bytes(arr))
295}
296
297fn p2_biguint_to_uint48(
298 n: &num_bigint::BigUint,
299) -> Result<alloy::primitives::Uint<48, 1>, crate::error::FyndError> {
300 let bytes = n.to_bytes_be();
301 if bytes.len() > 6 {
302 return Err(crate::error::FyndError::Protocol(format!(
303 "permit value exceeds uint48 ({} bytes)",
304 bytes.len()
305 )));
306 }
307 let mut arr = [0u8; 6];
308 arr[6 - bytes.len()..].copy_from_slice(&bytes);
309 Ok(alloy::primitives::Uint::<48, 1>::from_be_bytes(arr))
310}
311
312#[derive(Debug, Clone)]
317pub struct EncodingOptions {
318 pub(crate) slippage: f64,
319 pub(crate) transfer_type: UserTransferType,
320 pub(crate) permit: Option<PermitSingle>,
321 pub(crate) permit2_signature: Option<Bytes>,
322 pub(crate) client_fee_params: Option<ClientFeeParams>,
323 pub(crate) price_guard: Option<PriceGuardConfig>,
324 pub(crate) simulate: bool,
325}
326
327impl EncodingOptions {
328 pub fn new(slippage: f64) -> Self {
333 Self {
334 slippage,
335 transfer_type: UserTransferType::TransferFrom,
336 permit: None,
337 permit2_signature: None,
338 client_fee_params: None,
339 price_guard: None,
340 simulate: false,
341 }
342 }
343
344 pub fn with_permit2(
353 mut self,
354 permit: PermitSingle,
355 signature: bytes::Bytes,
356 ) -> Result<Self, crate::error::FyndError> {
357 if signature.len() != 65 {
358 return Err(crate::error::FyndError::Protocol(format!(
359 "Permit2 signature must be exactly 65 bytes, got {}",
360 signature.len()
361 )));
362 }
363 self.transfer_type = UserTransferType::TransferFromPermit2;
364 self.permit = Some(permit);
365 self.permit2_signature = Some(signature);
366 Ok(self)
367 }
368
369 pub fn with_vault_funds(mut self) -> Self {
371 self.transfer_type = UserTransferType::UseVaultsFunds;
372 self
373 }
374
375 pub fn with_client_fee(mut self, params: ClientFeeParams) -> Self {
379 self.client_fee_params = Some(params);
380 self
381 }
382
383 pub fn with_simulation(mut self) -> Self {
385 self.simulate = true;
386 self
387 }
388
389 pub fn with_price_guard(mut self, config: PriceGuardConfig) -> Self {
393 self.price_guard = Some(config);
394 self
395 }
396}
397
398#[derive(Debug, Clone)]
402pub struct Transaction {
403 to: Bytes,
404 value: BigUint,
405 pub(crate) data: Vec<u8>,
406 pub(crate) client_fee_signature_offset: Option<usize>,
407}
408
409impl Transaction {
410 pub fn new(to: Bytes, value: BigUint, data: Vec<u8>) -> Self {
416 Self { to, value, data, client_fee_signature_offset: None }
417 }
418
419 pub fn to(&self) -> &Bytes {
421 &self.to
422 }
423
424 pub fn value(&self) -> &BigUint {
426 &self.value
427 }
428
429 pub fn data(&self) -> &[u8] {
431 &self.data
432 }
433
434 pub fn client_fee_signature_offset(&self) -> Option<usize> {
436 self.client_fee_signature_offset
437 }
438}
439
440#[non_exhaustive]
448#[derive(Debug, Clone, Copy, PartialEq, Eq)]
449pub enum OrderSide {
450 Sell,
452}
453
454#[derive(Debug, Clone)]
463pub struct Order {
464 token_in: Bytes,
465 token_out: Bytes,
466 amount: BigUint,
467 side: OrderSide,
468 sender: Bytes,
469 receiver: Option<Bytes>,
470}
471
472impl Order {
473 pub fn new(
482 token_in: Bytes,
483 token_out: Bytes,
484 amount: BigUint,
485 side: OrderSide,
486 sender: Bytes,
487 receiver: Option<Bytes>,
488 ) -> Self {
489 Self { token_in, token_out, amount, side, sender, receiver }
490 }
491
492 pub fn token_in(&self) -> &Bytes {
494 &self.token_in
495 }
496
497 pub fn token_out(&self) -> &Bytes {
499 &self.token_out
500 }
501
502 pub fn amount(&self) -> &BigUint {
504 &self.amount
505 }
506
507 pub fn side(&self) -> OrderSide {
509 self.side
510 }
511
512 pub fn sender(&self) -> &Bytes {
514 &self.sender
515 }
516
517 pub fn receiver(&self) -> Option<&Bytes> {
520 self.receiver.as_ref()
521 }
522}
523
524pub use fynd_rpc_types::PriceGuardConfig;
529pub use fynd_rpc_types::RouteFilter;
535
536#[derive(Debug, Clone, Default)]
540pub struct QuoteOptions {
541 pub(crate) timeout_ms: Option<u64>,
542 pub(crate) min_responses: Option<usize>,
543 pub(crate) max_gas: Option<BigUint>,
544 pub(crate) encoding_options: Option<EncodingOptions>,
545 pub(crate) route_filter: Option<RouteFilter>,
546}
547
548impl QuoteOptions {
549 pub fn with_timeout_ms(mut self, ms: u64) -> Self {
551 self.timeout_ms = Some(ms);
552 self
553 }
554
555 pub fn with_min_responses(mut self, n: usize) -> Self {
560 self.min_responses = Some(n);
561 self
562 }
563
564 pub fn with_max_gas(mut self, gas: BigUint) -> Self {
566 self.max_gas = Some(gas);
567 self
568 }
569
570 pub fn with_encoding_options(mut self, opts: EncodingOptions) -> Self {
573 self.encoding_options = Some(opts);
574 self
575 }
576
577 pub fn with_route_filter(mut self, filter: RouteFilter) -> Self {
579 self.route_filter = Some(filter);
580 self
581 }
582
583 pub fn timeout_ms(&self) -> Option<u64> {
585 self.timeout_ms
586 }
587
588 pub fn min_responses(&self) -> Option<usize> {
590 self.min_responses
591 }
592
593 pub fn max_gas(&self) -> Option<&BigUint> {
595 self.max_gas.as_ref()
596 }
597
598 pub fn route_filter(&self) -> Option<&RouteFilter> {
600 self.route_filter.as_ref()
601 }
602}
603
604#[derive(Debug, Clone)]
606pub struct QuoteParams {
607 pub(crate) order: Order,
608 pub(crate) options: QuoteOptions,
609}
610
611impl QuoteParams {
612 pub fn new(order: Order, options: QuoteOptions) -> Self {
614 Self { order, options }
615 }
616}
617
618#[derive(Debug, Clone)]
623pub struct BatchQuoteParams {
624 pub(crate) orders: Vec<Order>,
625 pub(crate) options: QuoteOptions,
626}
627
628impl BatchQuoteParams {
629 pub fn new(orders: Vec<Order>, options: QuoteOptions) -> Self {
634 Self { orders, options }
635 }
636}
637
638#[derive(Debug, Clone, Copy, PartialEq, Eq)]
644pub enum BackendKind {
645 Fynd,
647 Turbine,
649}
650
651#[derive(Debug, Clone, Copy, PartialEq, Eq)]
653pub enum QuoteStatus {
654 Success,
656 NoRouteFound,
658 InsufficientLiquidity,
660 Timeout,
662 NotReady,
664 PriceCheckFailed,
666 EncodingFailed,
669}
670
671#[derive(Debug, Clone)]
676pub struct BlockInfo {
677 number: u64,
678 hash: String,
679 timestamp: u64,
680}
681
682impl BlockInfo {
683 pub fn number(&self) -> u64 {
685 self.number
686 }
687
688 pub fn hash(&self) -> &str {
690 &self.hash
691 }
692
693 pub fn timestamp(&self) -> u64 {
695 self.timestamp
696 }
697
698 pub fn new(number: u64, hash: String, timestamp: u64) -> Self {
700 Self { number, hash, timestamp }
701 }
702}
703
704#[derive(Debug, Clone)]
706pub struct Swap {
707 component_id: String,
708 protocol: String,
709 token_in: Bytes,
710 token_out: Bytes,
711 amount_in: BigUint,
712 amount_out: BigUint,
713 gas_estimate: BigUint,
714 #[allow(dead_code)]
715 split: f64,
716}
717
718impl Swap {
719 pub fn component_id(&self) -> &str {
721 &self.component_id
722 }
723
724 pub fn protocol(&self) -> &str {
726 &self.protocol
727 }
728
729 pub fn token_in(&self) -> &Bytes {
731 &self.token_in
732 }
733
734 pub fn token_out(&self) -> &Bytes {
736 &self.token_out
737 }
738
739 pub fn amount_in(&self) -> &BigUint {
741 &self.amount_in
742 }
743
744 pub fn amount_out(&self) -> &BigUint {
746 &self.amount_out
747 }
748
749 pub fn gas_estimate(&self) -> &BigUint {
751 &self.gas_estimate
752 }
753
754 #[allow(clippy::too_many_arguments)]
756 pub fn new(
757 component_id: String,
758 protocol: String,
759 token_in: Bytes,
760 token_out: Bytes,
761 amount_in: BigUint,
762 amount_out: BigUint,
763 gas_estimate: BigUint,
764 split: f64,
765 ) -> Self {
766 Self {
767 component_id,
768 protocol,
769 token_in,
770 token_out,
771 amount_in,
772 amount_out,
773 gas_estimate,
774 split,
775 }
776 }
777}
778
779#[derive(Debug, Clone)]
783pub struct Route {
784 swaps: Vec<Swap>,
785}
786
787impl Route {
788 pub fn swaps(&self) -> &[Swap] {
790 &self.swaps
791 }
792
793 pub fn new(swaps: Vec<Swap>) -> Self {
795 Self { swaps }
796 }
797}
798
799#[derive(Debug, Clone)]
803pub struct FeeBreakdown {
804 router_fee: BigUint,
805 client_fee: BigUint,
806 max_slippage: BigUint,
807 min_amount_received: BigUint,
808 swaps_hash: Option<[u8; 32]>,
810}
811
812#[derive(Debug, Clone, PartialEq, Eq)]
814pub enum SimulationResult {
815 Success {
817 amount_out: BigUint,
819 gas_used: u64,
821 },
822 Failure {
824 reason: String,
826 },
827}
828
829impl FeeBreakdown {
830 pub(crate) fn new(
831 router_fee: BigUint,
832 client_fee: BigUint,
833 max_slippage: BigUint,
834 min_amount_received: BigUint,
835 swaps_hash: Option<[u8; 32]>,
836 ) -> Self {
837 Self { router_fee, client_fee, max_slippage, min_amount_received, swaps_hash }
838 }
839
840 pub fn router_fee(&self) -> &BigUint {
842 &self.router_fee
843 }
844
845 pub fn client_fee(&self) -> &BigUint {
847 &self.client_fee
848 }
849
850 pub fn max_slippage(&self) -> &BigUint {
852 &self.max_slippage
853 }
854
855 pub fn min_amount_received(&self) -> &BigUint {
858 &self.min_amount_received
859 }
860
861 pub fn swaps_hash(&self) -> Option<&[u8; 32]> {
867 self.swaps_hash.as_ref()
868 }
869}
870
871#[derive(Debug, Clone)]
873pub struct Quote {
874 order_id: String,
875 status: QuoteStatus,
876 backend: BackendKind,
877 route: Option<Route>,
878 amount_in: BigUint,
879 amount_out: BigUint,
880 gas_estimate: BigUint,
881 amount_out_net_gas: BigUint,
882 price_impact_bps: Option<i32>,
883 block: BlockInfo,
884 token_out: Bytes,
887 receiver: Bytes,
891 transaction: Option<Transaction>,
894 fee_breakdown: Option<FeeBreakdown>,
896 pub(crate) simulation_result: Option<SimulationResult>,
898 pub(crate) algorithm: Option<String>,
901 pub(crate) solve_time_ms: u64,
904}
905
906impl Quote {
907 pub fn order_id(&self) -> &str {
909 &self.order_id
910 }
911
912 pub fn status(&self) -> QuoteStatus {
914 self.status
915 }
916
917 pub fn backend(&self) -> BackendKind {
919 self.backend
920 }
921
922 pub fn route(&self) -> Option<&Route> {
924 self.route.as_ref()
925 }
926
927 pub fn amount_in(&self) -> &BigUint {
929 &self.amount_in
930 }
931
932 pub fn amount_out(&self) -> &BigUint {
934 &self.amount_out
935 }
936
937 pub fn gas_estimate(&self) -> &BigUint {
939 &self.gas_estimate
940 }
941
942 pub fn amount_out_net_gas(&self) -> &BigUint {
947 &self.amount_out_net_gas
948 }
949
950 pub fn price_impact_bps(&self) -> Option<i32> {
952 self.price_impact_bps
953 }
954
955 pub fn block(&self) -> &BlockInfo {
957 &self.block
958 }
959
960 pub fn token_out(&self) -> &Bytes {
965 &self.token_out
966 }
967
968 pub fn receiver(&self) -> &Bytes {
975 &self.receiver
976 }
977
978 pub fn transaction(&self) -> Option<&Transaction> {
983 self.transaction.as_ref()
984 }
985
986 pub fn fee_breakdown(&self) -> Option<&FeeBreakdown> {
991 self.fee_breakdown.as_ref()
992 }
993
994 pub fn simulation_result(&self) -> Option<&SimulationResult> {
996 self.simulation_result.as_ref()
997 }
998
999 pub fn algorithm(&self) -> Option<&str> {
1003 self.algorithm.as_deref()
1004 }
1005
1006 pub fn solve_time_ms(&self) -> u64 {
1010 self.solve_time_ms
1011 }
1012
1013 pub fn with_client_fee_signature(mut self, signature: &[u8]) -> Result<Self, FyndError> {
1030 if signature.len() != CLIENT_FEE_SIGNATURE_BYTES {
1031 return Err(FyndError::Protocol(format!(
1032 "client fee signature must be exactly {CLIENT_FEE_SIGNATURE_BYTES} bytes, got {}",
1033 signature.len()
1034 )));
1035 }
1036 let tx = self
1037 .transaction
1038 .as_mut()
1039 .ok_or_else(|| {
1040 FyndError::Protocol("transaction required for signature patching".into())
1041 })?;
1042 let offset = tx
1043 .client_fee_signature_offset()
1044 .ok_or_else(|| {
1045 FyndError::Protocol(
1046 "client_fee_signature_offset required for signature patching".into(),
1047 )
1048 })?;
1049 let calldata_len = tx.data.len();
1050 let slot = tx
1051 .data
1052 .get_mut(offset..offset + CLIENT_FEE_SIGNATURE_BYTES)
1053 .ok_or_else(|| {
1054 FyndError::Protocol(format!(
1055 "client fee signature at offset {offset} does not fit \
1056 {calldata_len}-byte calldata"
1057 ))
1058 })?;
1059 if slot.iter().any(|byte| *byte != 0) {
1063 return Err(FyndError::Protocol(format!(
1064 "client fee signature offset {offset} does not point at the zeroed placeholder"
1065 )));
1066 }
1067 slot.copy_from_slice(signature);
1068 Ok(self)
1069 }
1070
1071 #[allow(clippy::too_many_arguments)]
1073 pub fn new(
1074 order_id: String,
1075 status: QuoteStatus,
1076 backend: BackendKind,
1077 route: Option<Route>,
1078 amount_in: BigUint,
1079 amount_out: BigUint,
1080 gas_estimate: BigUint,
1081 amount_out_net_gas: BigUint,
1082 price_impact_bps: Option<i32>,
1083 block: BlockInfo,
1084 token_out: Bytes,
1085 receiver: Bytes,
1086 transaction: Option<Transaction>,
1087 fee_breakdown: Option<FeeBreakdown>,
1088 ) -> Self {
1089 Self {
1090 order_id,
1091 status,
1092 backend,
1093 route,
1094 amount_in,
1095 amount_out,
1096 gas_estimate,
1097 amount_out_net_gas,
1098 price_impact_bps,
1099 block,
1100 token_out,
1101 receiver,
1102 transaction,
1103 fee_breakdown,
1104 simulation_result: None,
1105 algorithm: None,
1106 solve_time_ms: 0,
1107 }
1108 }
1109}
1110
1111#[derive(Debug, Clone)]
1113pub struct InstanceInfo {
1114 router_address: Option<bytes::Bytes>,
1116 permit2_address: bytes::Bytes,
1118 chain_id: u64,
1120 version: String,
1122}
1123
1124impl InstanceInfo {
1125 pub(crate) fn new(
1126 router_address: Option<bytes::Bytes>,
1127 permit2_address: bytes::Bytes,
1128 chain_id: u64,
1129 version: String,
1130 ) -> Self {
1131 Self { router_address, permit2_address, chain_id, version }
1132 }
1133
1134 pub fn router_address(&self) -> Option<&bytes::Bytes> {
1136 self.router_address.as_ref()
1137 }
1138
1139 pub fn permit2_address(&self) -> &bytes::Bytes {
1141 &self.permit2_address
1142 }
1143
1144 pub fn chain_id(&self) -> u64 {
1146 self.chain_id
1147 }
1148
1149 pub fn version(&self) -> &str {
1151 &self.version
1152 }
1153}
1154
1155#[derive(Debug, Clone)]
1157pub struct HealthStatus {
1158 healthy: bool,
1159 last_update_ms: u64,
1160 num_solver_pools: usize,
1161 derived_data_ready: bool,
1162 gas_price_age_ms: Option<u64>,
1163}
1164
1165impl HealthStatus {
1166 pub fn healthy(&self) -> bool {
1168 self.healthy
1169 }
1170
1171 pub fn last_update_ms(&self) -> u64 {
1173 self.last_update_ms
1174 }
1175
1176 pub fn num_solver_pools(&self) -> usize {
1178 self.num_solver_pools
1179 }
1180
1181 pub fn derived_data_ready(&self) -> bool {
1187 self.derived_data_ready
1188 }
1189
1190 pub fn gas_price_age_ms(&self) -> Option<u64> {
1192 self.gas_price_age_ms
1193 }
1194
1195 pub(crate) fn new(
1196 healthy: bool,
1197 last_update_ms: u64,
1198 num_solver_pools: usize,
1199 derived_data_ready: bool,
1200 gas_price_age_ms: Option<u64>,
1201 ) -> Self {
1202 Self { healthy, last_update_ms, num_solver_pools, derived_data_ready, gas_price_age_ms }
1203 }
1204}
1205
1206#[cfg(test)]
1207mod tests {
1208 use num_bigint::BigUint;
1209
1210 use super::*;
1211
1212 fn addr(bytes: &[u8; 20]) -> Bytes {
1213 Bytes::copy_from_slice(bytes)
1214 }
1215
1216 fn quote_with_placeholder(offset: Option<usize>, prefix: &[u8], suffix: &[u8]) -> Quote {
1219 let mut data = prefix.to_vec();
1220 data.extend_from_slice(&[0u8; CLIENT_FEE_SIGNATURE_BYTES]);
1221 data.extend_from_slice(suffix);
1222 let mut tx = Transaction::new(addr(&[0x11; 20]), BigUint::from(0u32), data);
1223 tx.client_fee_signature_offset = offset;
1224 Quote::new(
1225 "order-1".to_string(),
1226 QuoteStatus::Success,
1227 BackendKind::Fynd,
1228 None,
1229 BigUint::from(1_000u32),
1230 BigUint::from(2_000u32),
1231 BigUint::from(150_000u32),
1232 BigUint::from(2_000u32),
1233 None,
1234 BlockInfo {
1235 number: 21_000_000,
1236 hash: "0xabcdef".to_string(),
1237 timestamp: 1_730_000_000,
1238 },
1239 addr(&[0x22; 20]),
1240 addr(&[0x33; 20]),
1241 Some(tx),
1242 None,
1243 )
1244 }
1245
1246 #[test]
1247 fn test_with_client_fee_signature_patches_the_placeholder() {
1248 let quote = quote_with_placeholder(Some(2), &[0xde, 0xad], &[0xbe, 0xef]);
1249 let patched = quote
1250 .with_client_fee_signature(&[0xab; CLIENT_FEE_SIGNATURE_BYTES])
1251 .unwrap();
1252
1253 let data = &patched.transaction().unwrap().data;
1254 assert_eq!(&data[..2], &[0xde, 0xad]);
1255 assert_eq!(&data[2..2 + CLIENT_FEE_SIGNATURE_BYTES], &[0xab; CLIENT_FEE_SIGNATURE_BYTES]);
1256 assert_eq!(&data[2 + CLIENT_FEE_SIGNATURE_BYTES..], &[0xbe, 0xef]);
1257 }
1258
1259 fn patch_error(quote: Quote) -> String {
1261 quote
1262 .with_client_fee_signature(&[0xab; CLIENT_FEE_SIGNATURE_BYTES])
1263 .expect_err("expected the patch to be rejected")
1264 .to_string()
1265 }
1266
1267 #[test]
1268 fn test_with_client_fee_signature_rejects_wrong_length() {
1269 let quote = quote_with_placeholder(Some(2), &[0xde, 0xad], &[0xbe, 0xef]);
1270 let err = quote
1271 .with_client_fee_signature(&[0xab; 64])
1272 .expect_err("expected the patch to be rejected")
1273 .to_string();
1274 assert!(err.contains("must be exactly 65 bytes"), "{err}");
1275 }
1276
1277 #[test]
1278 fn test_with_client_fee_signature_rejects_offset_past_the_calldata() {
1279 let err = patch_error(quote_with_placeholder(Some(5), &[0xde, 0xad], &[0xbe, 0xef]));
1281 assert!(err.contains("does not fit 69-byte calldata"), "{err}");
1282 }
1283
1284 #[test]
1285 fn test_with_client_fee_signature_rejects_offset_off_the_placeholder() {
1286 let err = patch_error(quote_with_placeholder(Some(0), &[0xde, 0xad], &[0xbe, 0xef]));
1288 assert!(err.contains("does not point at the zeroed placeholder"), "{err}");
1289 }
1290
1291 #[test]
1292 fn test_with_client_fee_signature_rejects_missing_offset() {
1293 let err = patch_error(quote_with_placeholder(None, &[0xde, 0xad], &[0xbe, 0xef]));
1294 assert!(err.contains("client_fee_signature_offset required"), "{err}");
1295 }
1296
1297 #[test]
1298 fn order_new_and_getters() {
1299 let token_in = addr(&[0xaa; 20]);
1300 let token_out = addr(&[0xbb; 20]);
1301 let amount = BigUint::from(1_000_000u64);
1302 let sender = addr(&[0xcc; 20]);
1303
1304 let order = Order::new(
1305 token_in.clone(),
1306 token_out.clone(),
1307 amount.clone(),
1308 OrderSide::Sell,
1309 sender.clone(),
1310 None,
1311 );
1312
1313 assert_eq!(order.token_in(), &token_in);
1314 assert_eq!(order.token_out(), &token_out);
1315 assert_eq!(order.amount(), &amount);
1316 assert_eq!(order.sender(), &sender);
1317 assert!(order.receiver().is_none());
1318 assert_eq!(order.side(), OrderSide::Sell);
1319 }
1320
1321 #[test]
1322 fn order_with_explicit_receiver() {
1323 let receiver = Bytes::copy_from_slice(&[0xdd; 20]);
1324 let order = Order::new(
1325 Bytes::copy_from_slice(&[0xaa; 20]),
1326 Bytes::copy_from_slice(&[0xbb; 20]),
1327 BigUint::from(1u32),
1328 OrderSide::Sell,
1329 Bytes::copy_from_slice(&[0xcc; 20]),
1330 Some(receiver.clone()),
1331 );
1332 assert_eq!(order.receiver(), Some(&receiver));
1333 }
1334
1335 #[test]
1336 fn quote_options_builder() {
1337 let opts = QuoteOptions::default()
1338 .with_timeout_ms(500)
1339 .with_min_responses(2)
1340 .with_max_gas(BigUint::from(1_000_000u64));
1341
1342 assert_eq!(opts.timeout_ms(), Some(500));
1343 assert_eq!(opts.min_responses(), Some(2));
1344 assert_eq!(opts.max_gas(), Some(&BigUint::from(1_000_000u64)));
1345 }
1346
1347 #[test]
1348 fn quote_options_default_all_none() {
1349 let opts = QuoteOptions::default();
1350 assert!(opts.timeout_ms().is_none());
1351 assert!(opts.min_responses().is_none());
1352 assert!(opts.max_gas().is_none());
1353 }
1354
1355 #[test]
1356 fn encoding_options_with_permit2_sets_fields() {
1357 let token = Bytes::copy_from_slice(&[0xaa; 20]);
1358 let spender = Bytes::copy_from_slice(&[0xbb; 20]);
1359 let sig = Bytes::copy_from_slice(&[0xcc; 65]);
1360 let details = PermitDetails::new(
1361 token,
1362 BigUint::from(1_000u32),
1363 BigUint::from(9_999_999u32),
1364 BigUint::from(0u32),
1365 );
1366 let permit = PermitSingle::new(details, spender, BigUint::from(9_999_999u32));
1367
1368 let opts = EncodingOptions::new(0.005)
1369 .with_permit2(permit, sig.clone())
1370 .unwrap();
1371
1372 assert_eq!(opts.transfer_type, UserTransferType::TransferFromPermit2);
1373 assert!(opts.permit.is_some());
1374 assert_eq!(opts.permit2_signature.as_ref().unwrap(), &sig);
1375 }
1376
1377 #[test]
1378 fn encoding_options_with_permit2_rejects_wrong_signature_length() {
1379 let details = PermitDetails::new(
1380 Bytes::copy_from_slice(&[0xaa; 20]),
1381 BigUint::from(1_000u32),
1382 BigUint::from(9_999_999u32),
1383 BigUint::from(0u32),
1384 );
1385 let permit = PermitSingle::new(
1386 details,
1387 Bytes::copy_from_slice(&[0xbb; 20]),
1388 BigUint::from(9_999_999u32),
1389 );
1390 let bad_sig = Bytes::copy_from_slice(&[0xcc; 64]); assert!(matches!(
1392 EncodingOptions::new(0.005).with_permit2(permit, bad_sig),
1393 Err(crate::error::FyndError::Protocol(_))
1394 ));
1395 }
1396
1397 #[test]
1398 fn encoding_options_with_vault_funds_sets_variant() {
1399 let opts = EncodingOptions::new(0.005).with_vault_funds();
1400 assert_eq!(opts.transfer_type, UserTransferType::UseVaultsFunds);
1401 assert!(opts.permit.is_none());
1402 assert!(opts.permit2_signature.is_none());
1403 }
1404
1405 fn sample_permit_single() -> PermitSingle {
1406 let details = PermitDetails::new(
1407 Bytes::copy_from_slice(&[0xaa; 20]),
1408 BigUint::from(1_000u32),
1409 BigUint::from(9_999_999u32),
1410 BigUint::from(0u32),
1411 );
1412 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(9_999_999u32))
1413 }
1414
1415 #[test]
1416 fn eip712_signing_hash_returns_32_bytes() {
1417 let permit = sample_permit_single();
1418 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1419 let hash = permit
1420 .eip712_signing_hash(1, &permit2_addr)
1421 .unwrap();
1422 assert_eq!(hash.len(), 32);
1423 assert_ne!(hash, [0u8; 32]);
1425 }
1426
1427 #[test]
1428 fn eip712_signing_hash_is_deterministic() {
1429 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1430 let h1 = sample_permit_single()
1431 .eip712_signing_hash(1, &permit2_addr)
1432 .unwrap();
1433 let h2 = sample_permit_single()
1434 .eip712_signing_hash(1, &permit2_addr)
1435 .unwrap();
1436 assert_eq!(h1, h2);
1437 }
1438
1439 #[test]
1440 fn eip712_signing_hash_differs_by_chain_id() {
1441 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1442 let h1 = sample_permit_single()
1443 .eip712_signing_hash(1, &permit2_addr)
1444 .unwrap();
1445 let h137 = sample_permit_single()
1446 .eip712_signing_hash(137, &permit2_addr)
1447 .unwrap();
1448 assert_ne!(h1, h137);
1449 }
1450
1451 #[test]
1452 fn eip712_signing_hash_invalid_permit2_address() {
1453 let permit = sample_permit_single();
1454 let bad_addr = Bytes::copy_from_slice(&[0xcc; 4]);
1455 assert!(matches!(
1456 permit.eip712_signing_hash(1, &bad_addr),
1457 Err(crate::error::FyndError::Protocol(_))
1458 ));
1459 }
1460
1461 #[test]
1462 fn eip712_signing_hash_invalid_token_address() {
1463 let details = PermitDetails::new(
1464 Bytes::copy_from_slice(&[0xaa; 4]), BigUint::from(1u32),
1466 BigUint::from(1u32),
1467 BigUint::from(0u32),
1468 );
1469 let permit =
1470 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(1u32));
1471 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1472 assert!(matches!(
1473 permit.eip712_signing_hash(1, &permit2_addr),
1474 Err(crate::error::FyndError::Protocol(_))
1475 ));
1476 }
1477
1478 #[test]
1479 fn eip712_signing_hash_amount_exceeds_uint160() {
1480 let oversized_amount = BigUint::from_bytes_be(&[0x01; 21]);
1482 let details = PermitDetails::new(
1483 Bytes::copy_from_slice(&[0xaa; 20]),
1484 oversized_amount,
1485 BigUint::from(1u32),
1486 BigUint::from(0u32),
1487 );
1488 let permit =
1489 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(1u32));
1490 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1491 assert!(matches!(
1492 permit.eip712_signing_hash(1, &permit2_addr),
1493 Err(crate::error::FyndError::Protocol(_))
1494 ));
1495 }
1496
1497 fn sample_fee_receiver() -> Bytes {
1502 Bytes::copy_from_slice(&[0x44; 20])
1503 }
1504
1505 fn sample_router_address() -> Bytes {
1506 Bytes::copy_from_slice(&[0x33; 20])
1507 }
1508
1509 fn sample_fee_params(bps: u16, receiver: Bytes) -> ClientFeeParams {
1510 ClientFeeParams::new(bps, receiver, BigUint::ZERO, 1_893_456_000)
1511 }
1512
1513 fn sample_token_in() -> Bytes {
1514 Bytes::copy_from_slice(&[0x11; 20])
1515 }
1516
1517 fn sample_token_out() -> Bytes {
1518 Bytes::copy_from_slice(&[0x22; 20])
1519 }
1520
1521 fn sample_swap_receiver() -> Bytes {
1522 Bytes::copy_from_slice(&[0xAA; 20])
1523 }
1524
1525 fn sample_min_amount_out() -> BigUint {
1526 BigUint::from(1_000_000u64)
1527 }
1528
1529 fn sample_expected_amount_out() -> BigUint {
1530 BigUint::from(1_010_000u64)
1531 }
1532
1533 fn sample_amount_in() -> BigUint {
1534 BigUint::from(1_000_000_000_000_000_000u64)
1535 }
1536
1537 fn sample_swaps_hash() -> [u8; 32] {
1538 [0xAB; 32]
1539 }
1540
1541 #[test]
1542 fn client_fee_with_client_fee_sets_fields() {
1543 let fee = ClientFeeParams::new(
1544 100,
1545 sample_fee_receiver(),
1546 BigUint::from(500_000u64),
1547 1_893_456_000,
1548 );
1549 let opts = EncodingOptions::new(0.01).with_client_fee(fee);
1550 assert!(opts.client_fee_params.is_some());
1551 let stored = opts.client_fee_params.as_ref().unwrap();
1552 assert_eq!(stored.bps, 100);
1553 assert_eq!(stored.max_contribution, BigUint::from(500_000u64));
1554 }
1555
1556 #[test]
1557 fn client_fee_signing_hash_returns_32_bytes() {
1558 let fee = sample_fee_params(100, sample_fee_receiver());
1559 let hash = fee
1560 .eip712_signing_hash(
1561 1,
1562 &sample_router_address(),
1563 &sample_amount_in(),
1564 &sample_token_in(),
1565 &sample_token_out(),
1566 &sample_expected_amount_out(),
1567 &sample_min_amount_out(),
1568 &sample_swap_receiver(),
1569 &sample_swaps_hash(),
1570 )
1571 .unwrap();
1572 assert_eq!(hash.len(), 32);
1573 assert_ne!(hash, [0u8; 32]);
1574 }
1575
1576 #[test]
1577 fn client_fee_signing_hash_is_deterministic() {
1578 let fee = sample_fee_params(100, sample_fee_receiver());
1579 let h1 = fee
1580 .eip712_signing_hash(
1581 1,
1582 &sample_router_address(),
1583 &sample_amount_in(),
1584 &sample_token_in(),
1585 &sample_token_out(),
1586 &sample_expected_amount_out(),
1587 &sample_min_amount_out(),
1588 &sample_swap_receiver(),
1589 &sample_swaps_hash(),
1590 )
1591 .unwrap();
1592 let h2 = fee
1593 .eip712_signing_hash(
1594 1,
1595 &sample_router_address(),
1596 &sample_amount_in(),
1597 &sample_token_in(),
1598 &sample_token_out(),
1599 &sample_expected_amount_out(),
1600 &sample_min_amount_out(),
1601 &sample_swap_receiver(),
1602 &sample_swaps_hash(),
1603 )
1604 .unwrap();
1605 assert_eq!(h1, h2);
1606 }
1607
1608 #[test]
1609 fn client_fee_signing_hash_differs_by_chain_id() {
1610 let fee = sample_fee_params(100, sample_fee_receiver());
1611 let h1 = fee
1612 .eip712_signing_hash(
1613 1,
1614 &sample_router_address(),
1615 &sample_amount_in(),
1616 &sample_token_in(),
1617 &sample_token_out(),
1618 &sample_expected_amount_out(),
1619 &sample_min_amount_out(),
1620 &sample_swap_receiver(),
1621 &sample_swaps_hash(),
1622 )
1623 .unwrap();
1624 let h137 = fee
1625 .eip712_signing_hash(
1626 137,
1627 &sample_router_address(),
1628 &sample_amount_in(),
1629 &sample_token_in(),
1630 &sample_token_out(),
1631 &sample_expected_amount_out(),
1632 &sample_min_amount_out(),
1633 &sample_swap_receiver(),
1634 &sample_swaps_hash(),
1635 )
1636 .unwrap();
1637 assert_ne!(h1, h137);
1638 }
1639
1640 #[test]
1641 fn client_fee_signing_hash_differs_by_bps() {
1642 let h100 = sample_fee_params(100, sample_fee_receiver())
1643 .eip712_signing_hash(
1644 1,
1645 &sample_router_address(),
1646 &sample_amount_in(),
1647 &sample_token_in(),
1648 &sample_token_out(),
1649 &sample_expected_amount_out(),
1650 &sample_min_amount_out(),
1651 &sample_swap_receiver(),
1652 &sample_swaps_hash(),
1653 )
1654 .unwrap();
1655 let h200 = sample_fee_params(200, sample_fee_receiver())
1656 .eip712_signing_hash(
1657 1,
1658 &sample_router_address(),
1659 &sample_amount_in(),
1660 &sample_token_in(),
1661 &sample_token_out(),
1662 &sample_expected_amount_out(),
1663 &sample_min_amount_out(),
1664 &sample_swap_receiver(),
1665 &sample_swaps_hash(),
1666 )
1667 .unwrap();
1668 assert_ne!(h100, h200);
1669 }
1670
1671 #[test]
1672 fn client_fee_signing_hash_differs_by_expected_amount_out() {
1673 let fee = sample_fee_params(100, sample_fee_receiver());
1674 let quoted = fee
1675 .eip712_signing_hash(
1676 1,
1677 &sample_router_address(),
1678 &sample_amount_in(),
1679 &sample_token_in(),
1680 &sample_token_out(),
1681 &sample_expected_amount_out(),
1682 &sample_min_amount_out(),
1683 &sample_swap_receiver(),
1684 &sample_swaps_hash(),
1685 )
1686 .unwrap();
1687 let higher = fee
1688 .eip712_signing_hash(
1689 1,
1690 &sample_router_address(),
1691 &sample_amount_in(),
1692 &sample_token_in(),
1693 &sample_token_out(),
1694 &(sample_expected_amount_out() + BigUint::from(1u32)),
1695 &sample_min_amount_out(),
1696 &sample_swap_receiver(),
1697 &sample_swaps_hash(),
1698 )
1699 .unwrap();
1700 assert_ne!(quoted, higher);
1701 }
1702
1703 #[test]
1704 fn client_fee_signing_hash_differs_by_receiver() {
1705 let other_receiver = Bytes::copy_from_slice(&[0x55; 20]);
1706 let h1 = sample_fee_params(100, sample_fee_receiver())
1707 .eip712_signing_hash(
1708 1,
1709 &sample_router_address(),
1710 &sample_amount_in(),
1711 &sample_token_in(),
1712 &sample_token_out(),
1713 &sample_expected_amount_out(),
1714 &sample_min_amount_out(),
1715 &sample_swap_receiver(),
1716 &sample_swaps_hash(),
1717 )
1718 .unwrap();
1719 let h2 = sample_fee_params(100, other_receiver)
1720 .eip712_signing_hash(
1721 1,
1722 &sample_router_address(),
1723 &sample_amount_in(),
1724 &sample_token_in(),
1725 &sample_token_out(),
1726 &sample_expected_amount_out(),
1727 &sample_min_amount_out(),
1728 &sample_swap_receiver(),
1729 &sample_swaps_hash(),
1730 )
1731 .unwrap();
1732 assert_ne!(h1, h2);
1733 }
1734
1735 #[test]
1736 fn client_fee_signing_hash_rejects_bad_receiver_address() {
1737 let bad_addr = Bytes::copy_from_slice(&[0x44; 4]);
1738 let fee = sample_fee_params(100, bad_addr);
1739 assert!(matches!(
1740 fee.eip712_signing_hash(
1741 1,
1742 &sample_router_address(),
1743 &sample_amount_in(),
1744 &sample_token_in(),
1745 &sample_token_out(),
1746 &sample_expected_amount_out(),
1747 &sample_min_amount_out(),
1748 &sample_swap_receiver(),
1749 &sample_swaps_hash(),
1750 ),
1751 Err(crate::error::FyndError::Protocol(_))
1752 ));
1753 }
1754
1755 #[test]
1756 fn client_fee_signing_hash_rejects_bad_router_address() {
1757 let bad_addr = Bytes::copy_from_slice(&[0x33; 4]);
1758 let fee = sample_fee_params(100, sample_fee_receiver());
1759 assert!(matches!(
1760 fee.eip712_signing_hash(
1761 1,
1762 &bad_addr,
1763 &sample_amount_in(),
1764 &sample_token_in(),
1765 &sample_token_out(),
1766 &sample_expected_amount_out(),
1767 &sample_min_amount_out(),
1768 &sample_swap_receiver(),
1769 &sample_swaps_hash(),
1770 ),
1771 Err(crate::error::FyndError::Protocol(_))
1772 ));
1773 }
1774}