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 pub fn zero(receiver: Bytes, deadline: u64) -> Self {
159 Self::new(0, receiver, BigUint::ZERO, deadline)
160 }
161
162 #[allow(clippy::too_many_arguments)]
186 pub fn eip712_signing_hash(
187 &self,
188 chain_id: u64,
189 router_address: &Bytes,
190 amount_in: &num_bigint::BigUint,
191 token_in: &Bytes,
192 token_out: &Bytes,
193 expected_amount_out: &num_bigint::BigUint,
194 min_amount_out: &num_bigint::BigUint,
195 receiver: &Bytes,
196 swaps_hash: &[u8; 32],
197 ) -> Result<[u8; 32], crate::error::FyndError> {
198 let router_addr = p2_bytes_to_address(router_address, "router_address")?;
199 let fee_receiver = p2_bytes_to_address(&self.receiver, "receiver")?;
200 let max_contrib = biguint_to_u256(&self.max_contribution);
201 let dl = U256::from(self.deadline);
202 let amount_in_u256 = biguint_to_u256(amount_in);
203 let token_in_addr = p2_bytes_to_address(token_in, "token_in")?;
204 let token_out_addr = p2_bytes_to_address(token_out, "token_out")?;
205 let expected_amount_out_u256 = biguint_to_u256(expected_amount_out);
206 let min_amount_out_u256 = biguint_to_u256(min_amount_out);
207 let receiver_addr = p2_bytes_to_address(receiver, "receiver")?;
208 let swaps_b256 = alloy::primitives::B256::from(*swaps_hash);
209
210 let type_hash = keccak256(
211 b"ClientFee(uint32 clientFeeBps,address clientFeeReceiver,\
212uint256 maxClientContribution,uint256 deadline,\
213uint256 amountIn,address tokenIn,address tokenOut,\
214uint256 expectedAmountOut,uint256 minAmountOut,address receiver,bytes swaps)",
215 );
216
217 let domain_type_hash = keccak256(
218 b"EIP712Domain(string name,string version,\
219uint256 chainId,address verifyingContract)",
220 );
221 let domain_separator = keccak256(
222 (
223 domain_type_hash,
224 keccak256(b"TychoRouter"),
225 keccak256(b"1"),
226 U256::from(chain_id),
227 router_addr,
228 )
229 .abi_encode(),
230 );
231
232 let struct_hash = keccak256(
233 (
234 type_hash,
235 U256::from(self.bps as u64 * CLIENT_FEE_UNITS_PER_BPS),
236 fee_receiver,
237 max_contrib,
238 dl,
239 amount_in_u256,
240 token_in_addr,
241 token_out_addr,
242 expected_amount_out_u256,
243 min_amount_out_u256,
244 receiver_addr,
245 swaps_b256,
246 )
247 .abi_encode(),
248 );
249
250 let mut data = [0u8; 66];
251 data[0] = 0x19;
252 data[1] = 0x01;
253 data[2..34].copy_from_slice(domain_separator.as_ref());
254 data[34..66].copy_from_slice(struct_hash.as_ref());
255 Ok(keccak256(data).0)
256 }
257}
258
259mod permit2_sol {
264 use alloy::sol;
265
266 sol! {
267 struct PermitDetails {
268 address token;
269 uint160 amount;
270 uint48 expiration;
271 uint48 nonce;
272 }
273 struct PermitSingle {
274 PermitDetails details;
275 address spender;
276 uint256 sigDeadline;
277 }
278 }
279}
280
281fn p2_bytes_to_address(
282 b: &bytes::Bytes,
283 field: &str,
284) -> Result<alloy::primitives::Address, crate::error::FyndError> {
285 let arr: [u8; 20] = b.as_ref().try_into().map_err(|_| {
286 crate::error::FyndError::Protocol(format!(
287 "expected 20-byte address for {field}, got {} bytes",
288 b.len()
289 ))
290 })?;
291 Ok(alloy::primitives::Address::from(arr))
292}
293
294fn p2_biguint_to_uint160(
295 n: &num_bigint::BigUint,
296) -> Result<alloy::primitives::Uint<160, 3>, crate::error::FyndError> {
297 let bytes = n.to_bytes_be();
298 if bytes.len() > 20 {
299 return Err(crate::error::FyndError::Protocol(format!(
300 "permit amount exceeds uint160 ({} bytes)",
301 bytes.len()
302 )));
303 }
304 let mut arr = [0u8; 20];
305 arr[20 - bytes.len()..].copy_from_slice(&bytes);
306 Ok(alloy::primitives::Uint::<160, 3>::from_be_bytes(arr))
307}
308
309fn p2_biguint_to_uint48(
310 n: &num_bigint::BigUint,
311) -> Result<alloy::primitives::Uint<48, 1>, crate::error::FyndError> {
312 let bytes = n.to_bytes_be();
313 if bytes.len() > 6 {
314 return Err(crate::error::FyndError::Protocol(format!(
315 "permit value exceeds uint48 ({} bytes)",
316 bytes.len()
317 )));
318 }
319 let mut arr = [0u8; 6];
320 arr[6 - bytes.len()..].copy_from_slice(&bytes);
321 Ok(alloy::primitives::Uint::<48, 1>::from_be_bytes(arr))
322}
323
324#[derive(Debug, Clone)]
329pub struct EncodingOptions {
330 pub(crate) slippage: f64,
331 pub(crate) transfer_type: UserTransferType,
332 pub(crate) permit: Option<PermitSingle>,
333 pub(crate) permit2_signature: Option<Bytes>,
334 pub(crate) client_fee_params: Option<ClientFeeParams>,
335 pub(crate) price_guard: Option<PriceGuardConfig>,
336 pub(crate) simulate: bool,
337}
338
339impl EncodingOptions {
340 pub fn new(slippage: f64) -> Self {
345 Self {
346 slippage,
347 transfer_type: UserTransferType::TransferFrom,
348 permit: None,
349 permit2_signature: None,
350 client_fee_params: None,
351 price_guard: None,
352 simulate: false,
353 }
354 }
355
356 pub fn with_permit2(
365 mut self,
366 permit: PermitSingle,
367 signature: bytes::Bytes,
368 ) -> Result<Self, crate::error::FyndError> {
369 if signature.len() != 65 {
370 return Err(crate::error::FyndError::Protocol(format!(
371 "Permit2 signature must be exactly 65 bytes, got {}",
372 signature.len()
373 )));
374 }
375 self.transfer_type = UserTransferType::TransferFromPermit2;
376 self.permit = Some(permit);
377 self.permit2_signature = Some(signature);
378 Ok(self)
379 }
380
381 pub fn with_vault_funds(mut self) -> Self {
383 self.transfer_type = UserTransferType::UseVaultsFunds;
384 self
385 }
386
387 pub fn with_client_fee(mut self, params: ClientFeeParams) -> Self {
391 self.client_fee_params = Some(params);
392 self
393 }
394
395 pub fn with_simulation(mut self) -> Self {
397 self.simulate = true;
398 self
399 }
400
401 pub fn with_price_guard(mut self, config: PriceGuardConfig) -> Self {
405 self.price_guard = Some(config);
406 self
407 }
408}
409
410#[derive(Debug, Clone)]
414pub struct Transaction {
415 to: Bytes,
416 value: BigUint,
417 pub(crate) data: Vec<u8>,
418 pub(crate) client_fee_signature_offset: Option<usize>,
419}
420
421impl Transaction {
422 pub fn new(to: Bytes, value: BigUint, data: Vec<u8>) -> Self {
428 Self { to, value, data, client_fee_signature_offset: None }
429 }
430
431 pub fn to(&self) -> &Bytes {
433 &self.to
434 }
435
436 pub fn value(&self) -> &BigUint {
438 &self.value
439 }
440
441 pub fn data(&self) -> &[u8] {
443 &self.data
444 }
445
446 pub fn client_fee_signature_offset(&self) -> Option<usize> {
448 self.client_fee_signature_offset
449 }
450}
451
452#[non_exhaustive]
460#[derive(Debug, Clone, Copy, PartialEq, Eq)]
461pub enum OrderSide {
462 Sell,
464}
465
466#[derive(Debug, Clone)]
475pub struct Order {
476 token_in: Bytes,
477 token_out: Bytes,
478 amount: BigUint,
479 side: OrderSide,
480 sender: Bytes,
481 receiver: Option<Bytes>,
482}
483
484impl Order {
485 pub fn new(
494 token_in: Bytes,
495 token_out: Bytes,
496 amount: BigUint,
497 side: OrderSide,
498 sender: Bytes,
499 receiver: Option<Bytes>,
500 ) -> Self {
501 Self { token_in, token_out, amount, side, sender, receiver }
502 }
503
504 pub fn token_in(&self) -> &Bytes {
506 &self.token_in
507 }
508
509 pub fn token_out(&self) -> &Bytes {
511 &self.token_out
512 }
513
514 pub fn amount(&self) -> &BigUint {
516 &self.amount
517 }
518
519 pub fn side(&self) -> OrderSide {
521 self.side
522 }
523
524 pub fn sender(&self) -> &Bytes {
526 &self.sender
527 }
528
529 pub fn receiver(&self) -> Option<&Bytes> {
532 self.receiver.as_ref()
533 }
534}
535
536pub use fynd_rpc_types::PriceGuardConfig;
541pub use fynd_rpc_types::RouteFilter;
547
548#[derive(Debug, Clone, Default)]
552pub struct QuoteOptions {
553 pub(crate) timeout_ms: Option<u64>,
554 pub(crate) min_responses: Option<usize>,
555 pub(crate) max_gas: Option<BigUint>,
556 pub(crate) encoding_options: Option<EncodingOptions>,
557 pub(crate) route_filter: Option<RouteFilter>,
558}
559
560impl QuoteOptions {
561 pub fn with_timeout_ms(mut self, ms: u64) -> Self {
563 self.timeout_ms = Some(ms);
564 self
565 }
566
567 pub fn with_min_responses(mut self, n: usize) -> Self {
572 self.min_responses = Some(n);
573 self
574 }
575
576 pub fn with_max_gas(mut self, gas: BigUint) -> Self {
578 self.max_gas = Some(gas);
579 self
580 }
581
582 pub fn with_encoding_options(mut self, opts: EncodingOptions) -> Self {
585 self.encoding_options = Some(opts);
586 self
587 }
588
589 pub fn with_route_filter(mut self, filter: RouteFilter) -> Self {
591 self.route_filter = Some(filter);
592 self
593 }
594
595 pub fn timeout_ms(&self) -> Option<u64> {
597 self.timeout_ms
598 }
599
600 pub fn min_responses(&self) -> Option<usize> {
602 self.min_responses
603 }
604
605 pub fn max_gas(&self) -> Option<&BigUint> {
607 self.max_gas.as_ref()
608 }
609
610 pub fn route_filter(&self) -> Option<&RouteFilter> {
612 self.route_filter.as_ref()
613 }
614}
615
616#[derive(Debug, Clone)]
618pub struct QuoteParams {
619 pub(crate) order: Order,
620 pub(crate) options: QuoteOptions,
621}
622
623impl QuoteParams {
624 pub fn new(order: Order, options: QuoteOptions) -> Self {
626 Self { order, options }
627 }
628}
629
630#[derive(Debug, Clone)]
635pub struct BatchQuoteParams {
636 pub(crate) orders: Vec<Order>,
637 pub(crate) options: QuoteOptions,
638}
639
640impl BatchQuoteParams {
641 pub fn new(orders: Vec<Order>, options: QuoteOptions) -> Self {
646 Self { orders, options }
647 }
648}
649
650#[derive(Debug, Clone, Copy, PartialEq, Eq)]
656pub enum BackendKind {
657 Fynd,
659 Turbine,
661}
662
663#[derive(Debug, Clone, Copy, PartialEq, Eq)]
665pub enum QuoteStatus {
666 Success,
668 NoRouteFound,
670 InsufficientLiquidity,
672 Timeout,
674 NotReady,
676 PriceCheckFailed,
678 EncodingFailed,
681}
682
683#[derive(Debug, Clone)]
688pub struct BlockInfo {
689 number: u64,
690 hash: String,
691 timestamp: u64,
692}
693
694impl BlockInfo {
695 pub fn number(&self) -> u64 {
697 self.number
698 }
699
700 pub fn hash(&self) -> &str {
702 &self.hash
703 }
704
705 pub fn timestamp(&self) -> u64 {
707 self.timestamp
708 }
709
710 pub fn new(number: u64, hash: String, timestamp: u64) -> Self {
712 Self { number, hash, timestamp }
713 }
714}
715
716#[derive(Debug, Clone)]
718pub struct Swap {
719 component_id: String,
720 protocol: String,
721 token_in: Bytes,
722 token_out: Bytes,
723 amount_in: BigUint,
724 amount_out: BigUint,
725 gas_estimate: BigUint,
726 #[allow(dead_code)]
727 split: f64,
728}
729
730impl Swap {
731 pub fn component_id(&self) -> &str {
733 &self.component_id
734 }
735
736 pub fn protocol(&self) -> &str {
738 &self.protocol
739 }
740
741 pub fn token_in(&self) -> &Bytes {
743 &self.token_in
744 }
745
746 pub fn token_out(&self) -> &Bytes {
748 &self.token_out
749 }
750
751 pub fn amount_in(&self) -> &BigUint {
753 &self.amount_in
754 }
755
756 pub fn amount_out(&self) -> &BigUint {
758 &self.amount_out
759 }
760
761 pub fn gas_estimate(&self) -> &BigUint {
763 &self.gas_estimate
764 }
765
766 #[allow(clippy::too_many_arguments)]
768 pub fn new(
769 component_id: String,
770 protocol: String,
771 token_in: Bytes,
772 token_out: Bytes,
773 amount_in: BigUint,
774 amount_out: BigUint,
775 gas_estimate: BigUint,
776 split: f64,
777 ) -> Self {
778 Self {
779 component_id,
780 protocol,
781 token_in,
782 token_out,
783 amount_in,
784 amount_out,
785 gas_estimate,
786 split,
787 }
788 }
789}
790
791#[derive(Debug, Clone)]
795pub struct Route {
796 swaps: Vec<Swap>,
797}
798
799impl Route {
800 pub fn swaps(&self) -> &[Swap] {
802 &self.swaps
803 }
804
805 pub fn new(swaps: Vec<Swap>) -> Self {
807 Self { swaps }
808 }
809}
810
811#[derive(Debug, Clone)]
815pub struct FeeBreakdown {
816 router_fee: BigUint,
817 client_fee: BigUint,
818 max_slippage: BigUint,
819 min_amount_received: BigUint,
820 swaps_hash: Option<[u8; 32]>,
822}
823
824#[derive(Debug, Clone, PartialEq, Eq)]
826pub enum SimulationResult {
827 Success {
829 amount_out: BigUint,
831 gas_used: u64,
833 },
834 Failure {
836 reason: String,
838 },
839}
840
841impl FeeBreakdown {
842 pub(crate) fn new(
843 router_fee: BigUint,
844 client_fee: BigUint,
845 max_slippage: BigUint,
846 min_amount_received: BigUint,
847 swaps_hash: Option<[u8; 32]>,
848 ) -> Self {
849 Self { router_fee, client_fee, max_slippage, min_amount_received, swaps_hash }
850 }
851
852 pub fn router_fee(&self) -> &BigUint {
854 &self.router_fee
855 }
856
857 pub fn client_fee(&self) -> &BigUint {
859 &self.client_fee
860 }
861
862 pub fn max_slippage(&self) -> &BigUint {
864 &self.max_slippage
865 }
866
867 pub fn min_amount_received(&self) -> &BigUint {
870 &self.min_amount_received
871 }
872
873 pub fn swaps_hash(&self) -> Option<&[u8; 32]> {
879 self.swaps_hash.as_ref()
880 }
881}
882
883#[derive(Debug, Clone)]
885pub struct Quote {
886 order_id: String,
887 status: QuoteStatus,
888 backend: BackendKind,
889 route: Option<Route>,
890 amount_in: BigUint,
891 amount_out: BigUint,
892 gas_estimate: BigUint,
893 amount_out_net_gas: BigUint,
894 price_impact_bps: Option<i32>,
895 block: BlockInfo,
896 token_out: Bytes,
899 receiver: Bytes,
903 transaction: Option<Transaction>,
906 fee_breakdown: Option<FeeBreakdown>,
908 pub(crate) simulation_result: Option<SimulationResult>,
910 pub(crate) algorithm: Option<String>,
913 pub(crate) solve_time_ms: u64,
916}
917
918impl Quote {
919 pub fn order_id(&self) -> &str {
921 &self.order_id
922 }
923
924 pub fn status(&self) -> QuoteStatus {
926 self.status
927 }
928
929 pub fn backend(&self) -> BackendKind {
931 self.backend
932 }
933
934 pub fn route(&self) -> Option<&Route> {
936 self.route.as_ref()
937 }
938
939 pub fn amount_in(&self) -> &BigUint {
941 &self.amount_in
942 }
943
944 pub fn amount_out(&self) -> &BigUint {
946 &self.amount_out
947 }
948
949 pub fn gas_estimate(&self) -> &BigUint {
951 &self.gas_estimate
952 }
953
954 pub fn amount_out_net_gas(&self) -> &BigUint {
959 &self.amount_out_net_gas
960 }
961
962 pub fn price_impact_bps(&self) -> Option<i32> {
964 self.price_impact_bps
965 }
966
967 pub fn block(&self) -> &BlockInfo {
969 &self.block
970 }
971
972 pub fn token_out(&self) -> &Bytes {
977 &self.token_out
978 }
979
980 pub fn receiver(&self) -> &Bytes {
987 &self.receiver
988 }
989
990 pub fn transaction(&self) -> Option<&Transaction> {
995 self.transaction.as_ref()
996 }
997
998 pub fn fee_breakdown(&self) -> Option<&FeeBreakdown> {
1003 self.fee_breakdown.as_ref()
1004 }
1005
1006 pub fn simulation_result(&self) -> Option<&SimulationResult> {
1008 self.simulation_result.as_ref()
1009 }
1010
1011 pub fn algorithm(&self) -> Option<&str> {
1015 self.algorithm.as_deref()
1016 }
1017
1018 pub fn solve_time_ms(&self) -> u64 {
1022 self.solve_time_ms
1023 }
1024
1025 pub fn with_client_fee_signature(mut self, signature: &[u8]) -> Result<Self, FyndError> {
1042 if signature.len() != CLIENT_FEE_SIGNATURE_BYTES {
1043 return Err(FyndError::Protocol(format!(
1044 "client fee signature must be exactly {CLIENT_FEE_SIGNATURE_BYTES} bytes, got {}",
1045 signature.len()
1046 )));
1047 }
1048 let tx = self
1049 .transaction
1050 .as_mut()
1051 .ok_or_else(|| {
1052 FyndError::Protocol("transaction required for signature patching".into())
1053 })?;
1054 let offset = tx
1055 .client_fee_signature_offset()
1056 .ok_or_else(|| {
1057 FyndError::Protocol(
1058 "client_fee_signature_offset required for signature patching".into(),
1059 )
1060 })?;
1061 let calldata_len = tx.data.len();
1062 let slot = tx
1063 .data
1064 .get_mut(offset..offset + CLIENT_FEE_SIGNATURE_BYTES)
1065 .ok_or_else(|| {
1066 FyndError::Protocol(format!(
1067 "client fee signature at offset {offset} does not fit \
1068 {calldata_len}-byte calldata"
1069 ))
1070 })?;
1071 if slot.iter().any(|byte| *byte != 0) {
1075 return Err(FyndError::Protocol(format!(
1076 "client fee signature offset {offset} does not point at the zeroed placeholder"
1077 )));
1078 }
1079 slot.copy_from_slice(signature);
1080 Ok(self)
1081 }
1082
1083 #[allow(clippy::too_many_arguments)]
1085 pub fn new(
1086 order_id: String,
1087 status: QuoteStatus,
1088 backend: BackendKind,
1089 route: Option<Route>,
1090 amount_in: BigUint,
1091 amount_out: BigUint,
1092 gas_estimate: BigUint,
1093 amount_out_net_gas: BigUint,
1094 price_impact_bps: Option<i32>,
1095 block: BlockInfo,
1096 token_out: Bytes,
1097 receiver: Bytes,
1098 transaction: Option<Transaction>,
1099 fee_breakdown: Option<FeeBreakdown>,
1100 ) -> Self {
1101 Self {
1102 order_id,
1103 status,
1104 backend,
1105 route,
1106 amount_in,
1107 amount_out,
1108 gas_estimate,
1109 amount_out_net_gas,
1110 price_impact_bps,
1111 block,
1112 token_out,
1113 receiver,
1114 transaction,
1115 fee_breakdown,
1116 simulation_result: None,
1117 algorithm: None,
1118 solve_time_ms: 0,
1119 }
1120 }
1121}
1122
1123#[derive(Debug, Clone)]
1125pub struct InstanceInfo {
1126 router_address: Option<bytes::Bytes>,
1128 permit2_address: bytes::Bytes,
1130 chain_id: u64,
1132 version: String,
1134}
1135
1136impl InstanceInfo {
1137 pub(crate) fn new(
1138 router_address: Option<bytes::Bytes>,
1139 permit2_address: bytes::Bytes,
1140 chain_id: u64,
1141 version: String,
1142 ) -> Self {
1143 Self { router_address, permit2_address, chain_id, version }
1144 }
1145
1146 pub fn router_address(&self) -> Option<&bytes::Bytes> {
1148 self.router_address.as_ref()
1149 }
1150
1151 pub fn permit2_address(&self) -> &bytes::Bytes {
1153 &self.permit2_address
1154 }
1155
1156 pub fn chain_id(&self) -> u64 {
1158 self.chain_id
1159 }
1160
1161 pub fn version(&self) -> &str {
1163 &self.version
1164 }
1165}
1166
1167#[derive(Debug, Clone)]
1169pub struct HealthStatus {
1170 healthy: bool,
1171 last_update_ms: u64,
1172 num_solver_pools: usize,
1173 derived_data_ready: bool,
1174 gas_price_age_ms: Option<u64>,
1175}
1176
1177impl HealthStatus {
1178 pub fn healthy(&self) -> bool {
1180 self.healthy
1181 }
1182
1183 pub fn last_update_ms(&self) -> u64 {
1185 self.last_update_ms
1186 }
1187
1188 pub fn num_solver_pools(&self) -> usize {
1190 self.num_solver_pools
1191 }
1192
1193 pub fn derived_data_ready(&self) -> bool {
1199 self.derived_data_ready
1200 }
1201
1202 pub fn gas_price_age_ms(&self) -> Option<u64> {
1204 self.gas_price_age_ms
1205 }
1206
1207 pub(crate) fn new(
1208 healthy: bool,
1209 last_update_ms: u64,
1210 num_solver_pools: usize,
1211 derived_data_ready: bool,
1212 gas_price_age_ms: Option<u64>,
1213 ) -> Self {
1214 Self { healthy, last_update_ms, num_solver_pools, derived_data_ready, gas_price_age_ms }
1215 }
1216}
1217
1218#[cfg(test)]
1219mod tests {
1220 use num_bigint::BigUint;
1221
1222 use super::*;
1223
1224 fn addr(bytes: &[u8; 20]) -> Bytes {
1225 Bytes::copy_from_slice(bytes)
1226 }
1227
1228 fn quote_with_placeholder(offset: Option<usize>, prefix: &[u8], suffix: &[u8]) -> Quote {
1231 let mut data = prefix.to_vec();
1232 data.extend_from_slice(&[0u8; CLIENT_FEE_SIGNATURE_BYTES]);
1233 data.extend_from_slice(suffix);
1234 let mut tx = Transaction::new(addr(&[0x11; 20]), BigUint::from(0u32), data);
1235 tx.client_fee_signature_offset = offset;
1236 Quote::new(
1237 "order-1".to_string(),
1238 QuoteStatus::Success,
1239 BackendKind::Fynd,
1240 None,
1241 BigUint::from(1_000u32),
1242 BigUint::from(2_000u32),
1243 BigUint::from(150_000u32),
1244 BigUint::from(2_000u32),
1245 None,
1246 BlockInfo {
1247 number: 21_000_000,
1248 hash: "0xabcdef".to_string(),
1249 timestamp: 1_730_000_000,
1250 },
1251 addr(&[0x22; 20]),
1252 addr(&[0x33; 20]),
1253 Some(tx),
1254 None,
1255 )
1256 }
1257
1258 #[test]
1259 fn test_with_client_fee_signature_patches_the_placeholder() {
1260 let quote = quote_with_placeholder(Some(2), &[0xde, 0xad], &[0xbe, 0xef]);
1261 let patched = quote
1262 .with_client_fee_signature(&[0xab; CLIENT_FEE_SIGNATURE_BYTES])
1263 .unwrap();
1264
1265 let data = &patched.transaction().unwrap().data;
1266 assert_eq!(&data[..2], &[0xde, 0xad]);
1267 assert_eq!(&data[2..2 + CLIENT_FEE_SIGNATURE_BYTES], &[0xab; CLIENT_FEE_SIGNATURE_BYTES]);
1268 assert_eq!(&data[2 + CLIENT_FEE_SIGNATURE_BYTES..], &[0xbe, 0xef]);
1269 }
1270
1271 fn patch_error(quote: Quote) -> String {
1273 quote
1274 .with_client_fee_signature(&[0xab; CLIENT_FEE_SIGNATURE_BYTES])
1275 .expect_err("expected the patch to be rejected")
1276 .to_string()
1277 }
1278
1279 #[test]
1280 fn test_with_client_fee_signature_rejects_wrong_length() {
1281 let quote = quote_with_placeholder(Some(2), &[0xde, 0xad], &[0xbe, 0xef]);
1282 let err = quote
1283 .with_client_fee_signature(&[0xab; 64])
1284 .expect_err("expected the patch to be rejected")
1285 .to_string();
1286 assert!(err.contains("must be exactly 65 bytes"), "{err}");
1287 }
1288
1289 #[test]
1290 fn test_with_client_fee_signature_rejects_offset_past_the_calldata() {
1291 let err = patch_error(quote_with_placeholder(Some(5), &[0xde, 0xad], &[0xbe, 0xef]));
1293 assert!(err.contains("does not fit 69-byte calldata"), "{err}");
1294 }
1295
1296 #[test]
1297 fn test_with_client_fee_signature_rejects_offset_off_the_placeholder() {
1298 let err = patch_error(quote_with_placeholder(Some(0), &[0xde, 0xad], &[0xbe, 0xef]));
1300 assert!(err.contains("does not point at the zeroed placeholder"), "{err}");
1301 }
1302
1303 #[test]
1304 fn test_with_client_fee_signature_rejects_missing_offset() {
1305 let err = patch_error(quote_with_placeholder(None, &[0xde, 0xad], &[0xbe, 0xef]));
1306 assert!(err.contains("client_fee_signature_offset required"), "{err}");
1307 }
1308
1309 #[test]
1310 fn order_new_and_getters() {
1311 let token_in = addr(&[0xaa; 20]);
1312 let token_out = addr(&[0xbb; 20]);
1313 let amount = BigUint::from(1_000_000u64);
1314 let sender = addr(&[0xcc; 20]);
1315
1316 let order = Order::new(
1317 token_in.clone(),
1318 token_out.clone(),
1319 amount.clone(),
1320 OrderSide::Sell,
1321 sender.clone(),
1322 None,
1323 );
1324
1325 assert_eq!(order.token_in(), &token_in);
1326 assert_eq!(order.token_out(), &token_out);
1327 assert_eq!(order.amount(), &amount);
1328 assert_eq!(order.sender(), &sender);
1329 assert!(order.receiver().is_none());
1330 assert_eq!(order.side(), OrderSide::Sell);
1331 }
1332
1333 #[test]
1334 fn order_with_explicit_receiver() {
1335 let receiver = Bytes::copy_from_slice(&[0xdd; 20]);
1336 let order = Order::new(
1337 Bytes::copy_from_slice(&[0xaa; 20]),
1338 Bytes::copy_from_slice(&[0xbb; 20]),
1339 BigUint::from(1u32),
1340 OrderSide::Sell,
1341 Bytes::copy_from_slice(&[0xcc; 20]),
1342 Some(receiver.clone()),
1343 );
1344 assert_eq!(order.receiver(), Some(&receiver));
1345 }
1346
1347 #[test]
1348 fn quote_options_builder() {
1349 let opts = QuoteOptions::default()
1350 .with_timeout_ms(500)
1351 .with_min_responses(2)
1352 .with_max_gas(BigUint::from(1_000_000u64));
1353
1354 assert_eq!(opts.timeout_ms(), Some(500));
1355 assert_eq!(opts.min_responses(), Some(2));
1356 assert_eq!(opts.max_gas(), Some(&BigUint::from(1_000_000u64)));
1357 }
1358
1359 #[test]
1360 fn quote_options_default_all_none() {
1361 let opts = QuoteOptions::default();
1362 assert!(opts.timeout_ms().is_none());
1363 assert!(opts.min_responses().is_none());
1364 assert!(opts.max_gas().is_none());
1365 }
1366
1367 #[test]
1368 fn encoding_options_with_permit2_sets_fields() {
1369 let token = Bytes::copy_from_slice(&[0xaa; 20]);
1370 let spender = Bytes::copy_from_slice(&[0xbb; 20]);
1371 let sig = Bytes::copy_from_slice(&[0xcc; 65]);
1372 let details = PermitDetails::new(
1373 token,
1374 BigUint::from(1_000u32),
1375 BigUint::from(9_999_999u32),
1376 BigUint::from(0u32),
1377 );
1378 let permit = PermitSingle::new(details, spender, BigUint::from(9_999_999u32));
1379
1380 let opts = EncodingOptions::new(0.005)
1381 .with_permit2(permit, sig.clone())
1382 .unwrap();
1383
1384 assert_eq!(opts.transfer_type, UserTransferType::TransferFromPermit2);
1385 assert!(opts.permit.is_some());
1386 assert_eq!(opts.permit2_signature.as_ref().unwrap(), &sig);
1387 }
1388
1389 #[test]
1390 fn encoding_options_with_permit2_rejects_wrong_signature_length() {
1391 let details = PermitDetails::new(
1392 Bytes::copy_from_slice(&[0xaa; 20]),
1393 BigUint::from(1_000u32),
1394 BigUint::from(9_999_999u32),
1395 BigUint::from(0u32),
1396 );
1397 let permit = PermitSingle::new(
1398 details,
1399 Bytes::copy_from_slice(&[0xbb; 20]),
1400 BigUint::from(9_999_999u32),
1401 );
1402 let bad_sig = Bytes::copy_from_slice(&[0xcc; 64]); assert!(matches!(
1404 EncodingOptions::new(0.005).with_permit2(permit, bad_sig),
1405 Err(crate::error::FyndError::Protocol(_))
1406 ));
1407 }
1408
1409 #[test]
1410 fn encoding_options_with_vault_funds_sets_variant() {
1411 let opts = EncodingOptions::new(0.005).with_vault_funds();
1412 assert_eq!(opts.transfer_type, UserTransferType::UseVaultsFunds);
1413 assert!(opts.permit.is_none());
1414 assert!(opts.permit2_signature.is_none());
1415 }
1416
1417 fn sample_permit_single() -> PermitSingle {
1418 let details = PermitDetails::new(
1419 Bytes::copy_from_slice(&[0xaa; 20]),
1420 BigUint::from(1_000u32),
1421 BigUint::from(9_999_999u32),
1422 BigUint::from(0u32),
1423 );
1424 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(9_999_999u32))
1425 }
1426
1427 #[test]
1428 fn eip712_signing_hash_returns_32_bytes() {
1429 let permit = sample_permit_single();
1430 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1431 let hash = permit
1432 .eip712_signing_hash(1, &permit2_addr)
1433 .unwrap();
1434 assert_eq!(hash.len(), 32);
1435 assert_ne!(hash, [0u8; 32]);
1437 }
1438
1439 #[test]
1440 fn eip712_signing_hash_is_deterministic() {
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 h2 = sample_permit_single()
1446 .eip712_signing_hash(1, &permit2_addr)
1447 .unwrap();
1448 assert_eq!(h1, h2);
1449 }
1450
1451 #[test]
1452 fn eip712_signing_hash_differs_by_chain_id() {
1453 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1454 let h1 = sample_permit_single()
1455 .eip712_signing_hash(1, &permit2_addr)
1456 .unwrap();
1457 let h137 = sample_permit_single()
1458 .eip712_signing_hash(137, &permit2_addr)
1459 .unwrap();
1460 assert_ne!(h1, h137);
1461 }
1462
1463 #[test]
1464 fn eip712_signing_hash_invalid_permit2_address() {
1465 let permit = sample_permit_single();
1466 let bad_addr = Bytes::copy_from_slice(&[0xcc; 4]);
1467 assert!(matches!(
1468 permit.eip712_signing_hash(1, &bad_addr),
1469 Err(crate::error::FyndError::Protocol(_))
1470 ));
1471 }
1472
1473 #[test]
1474 fn eip712_signing_hash_invalid_token_address() {
1475 let details = PermitDetails::new(
1476 Bytes::copy_from_slice(&[0xaa; 4]), BigUint::from(1u32),
1478 BigUint::from(1u32),
1479 BigUint::from(0u32),
1480 );
1481 let permit =
1482 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(1u32));
1483 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1484 assert!(matches!(
1485 permit.eip712_signing_hash(1, &permit2_addr),
1486 Err(crate::error::FyndError::Protocol(_))
1487 ));
1488 }
1489
1490 #[test]
1491 fn eip712_signing_hash_amount_exceeds_uint160() {
1492 let oversized_amount = BigUint::from_bytes_be(&[0x01; 21]);
1494 let details = PermitDetails::new(
1495 Bytes::copy_from_slice(&[0xaa; 20]),
1496 oversized_amount,
1497 BigUint::from(1u32),
1498 BigUint::from(0u32),
1499 );
1500 let permit =
1501 PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(1u32));
1502 let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
1503 assert!(matches!(
1504 permit.eip712_signing_hash(1, &permit2_addr),
1505 Err(crate::error::FyndError::Protocol(_))
1506 ));
1507 }
1508
1509 fn sample_fee_receiver() -> Bytes {
1514 Bytes::copy_from_slice(&[0x44; 20])
1515 }
1516
1517 fn sample_router_address() -> Bytes {
1518 Bytes::copy_from_slice(&[0x33; 20])
1519 }
1520
1521 fn sample_fee_params(bps: u16, receiver: Bytes) -> ClientFeeParams {
1522 ClientFeeParams::new(bps, receiver, BigUint::ZERO, 1_893_456_000)
1523 }
1524
1525 fn sample_token_in() -> Bytes {
1526 Bytes::copy_from_slice(&[0x11; 20])
1527 }
1528
1529 fn sample_token_out() -> Bytes {
1530 Bytes::copy_from_slice(&[0x22; 20])
1531 }
1532
1533 fn sample_swap_receiver() -> Bytes {
1534 Bytes::copy_from_slice(&[0xAA; 20])
1535 }
1536
1537 fn sample_min_amount_out() -> BigUint {
1538 BigUint::from(1_000_000u64)
1539 }
1540
1541 fn sample_expected_amount_out() -> BigUint {
1542 BigUint::from(1_010_000u64)
1543 }
1544
1545 fn sample_amount_in() -> BigUint {
1546 BigUint::from(1_000_000_000_000_000_000u64)
1547 }
1548
1549 fn sample_swaps_hash() -> [u8; 32] {
1550 [0xAB; 32]
1551 }
1552
1553 #[test]
1554 fn client_fee_with_client_fee_sets_fields() {
1555 let fee = ClientFeeParams::new(
1556 100,
1557 sample_fee_receiver(),
1558 BigUint::from(500_000u64),
1559 1_893_456_000,
1560 );
1561 let opts = EncodingOptions::new(0.01).with_client_fee(fee);
1562 assert!(opts.client_fee_params.is_some());
1563 let stored = opts.client_fee_params.as_ref().unwrap();
1564 assert_eq!(stored.bps, 100);
1565 assert_eq!(stored.max_contribution, BigUint::from(500_000u64));
1566 }
1567
1568 #[test]
1569 fn test_client_fee_zero() {
1570 let fee = ClientFeeParams::zero(sample_fee_receiver(), 1_893_456_000);
1571 assert_eq!(fee.bps, 0);
1572 assert_eq!(fee.receiver, sample_fee_receiver());
1573 assert_eq!(fee.max_contribution, BigUint::ZERO);
1574 assert_eq!(fee.deadline, 1_893_456_000);
1575 }
1576
1577 #[test]
1578 fn client_fee_signing_hash_returns_32_bytes() {
1579 let fee = sample_fee_params(100, sample_fee_receiver());
1580 let hash = fee
1581 .eip712_signing_hash(
1582 1,
1583 &sample_router_address(),
1584 &sample_amount_in(),
1585 &sample_token_in(),
1586 &sample_token_out(),
1587 &sample_expected_amount_out(),
1588 &sample_min_amount_out(),
1589 &sample_swap_receiver(),
1590 &sample_swaps_hash(),
1591 )
1592 .unwrap();
1593 assert_eq!(hash.len(), 32);
1594 assert_ne!(hash, [0u8; 32]);
1595 }
1596
1597 #[test]
1598 fn client_fee_signing_hash_is_deterministic() {
1599 let fee = sample_fee_params(100, sample_fee_receiver());
1600 let h1 = fee
1601 .eip712_signing_hash(
1602 1,
1603 &sample_router_address(),
1604 &sample_amount_in(),
1605 &sample_token_in(),
1606 &sample_token_out(),
1607 &sample_expected_amount_out(),
1608 &sample_min_amount_out(),
1609 &sample_swap_receiver(),
1610 &sample_swaps_hash(),
1611 )
1612 .unwrap();
1613 let h2 = fee
1614 .eip712_signing_hash(
1615 1,
1616 &sample_router_address(),
1617 &sample_amount_in(),
1618 &sample_token_in(),
1619 &sample_token_out(),
1620 &sample_expected_amount_out(),
1621 &sample_min_amount_out(),
1622 &sample_swap_receiver(),
1623 &sample_swaps_hash(),
1624 )
1625 .unwrap();
1626 assert_eq!(h1, h2);
1627 }
1628
1629 #[test]
1630 fn client_fee_signing_hash_differs_by_chain_id() {
1631 let fee = sample_fee_params(100, sample_fee_receiver());
1632 let h1 = fee
1633 .eip712_signing_hash(
1634 1,
1635 &sample_router_address(),
1636 &sample_amount_in(),
1637 &sample_token_in(),
1638 &sample_token_out(),
1639 &sample_expected_amount_out(),
1640 &sample_min_amount_out(),
1641 &sample_swap_receiver(),
1642 &sample_swaps_hash(),
1643 )
1644 .unwrap();
1645 let h137 = fee
1646 .eip712_signing_hash(
1647 137,
1648 &sample_router_address(),
1649 &sample_amount_in(),
1650 &sample_token_in(),
1651 &sample_token_out(),
1652 &sample_expected_amount_out(),
1653 &sample_min_amount_out(),
1654 &sample_swap_receiver(),
1655 &sample_swaps_hash(),
1656 )
1657 .unwrap();
1658 assert_ne!(h1, h137);
1659 }
1660
1661 #[test]
1662 fn client_fee_signing_hash_differs_by_bps() {
1663 let h100 = sample_fee_params(100, sample_fee_receiver())
1664 .eip712_signing_hash(
1665 1,
1666 &sample_router_address(),
1667 &sample_amount_in(),
1668 &sample_token_in(),
1669 &sample_token_out(),
1670 &sample_expected_amount_out(),
1671 &sample_min_amount_out(),
1672 &sample_swap_receiver(),
1673 &sample_swaps_hash(),
1674 )
1675 .unwrap();
1676 let h200 = sample_fee_params(200, sample_fee_receiver())
1677 .eip712_signing_hash(
1678 1,
1679 &sample_router_address(),
1680 &sample_amount_in(),
1681 &sample_token_in(),
1682 &sample_token_out(),
1683 &sample_expected_amount_out(),
1684 &sample_min_amount_out(),
1685 &sample_swap_receiver(),
1686 &sample_swaps_hash(),
1687 )
1688 .unwrap();
1689 assert_ne!(h100, h200);
1690 }
1691
1692 #[test]
1693 fn client_fee_signing_hash_differs_by_expected_amount_out() {
1694 let fee = sample_fee_params(100, sample_fee_receiver());
1695 let quoted = fee
1696 .eip712_signing_hash(
1697 1,
1698 &sample_router_address(),
1699 &sample_amount_in(),
1700 &sample_token_in(),
1701 &sample_token_out(),
1702 &sample_expected_amount_out(),
1703 &sample_min_amount_out(),
1704 &sample_swap_receiver(),
1705 &sample_swaps_hash(),
1706 )
1707 .unwrap();
1708 let higher = fee
1709 .eip712_signing_hash(
1710 1,
1711 &sample_router_address(),
1712 &sample_amount_in(),
1713 &sample_token_in(),
1714 &sample_token_out(),
1715 &(sample_expected_amount_out() + BigUint::from(1u32)),
1716 &sample_min_amount_out(),
1717 &sample_swap_receiver(),
1718 &sample_swaps_hash(),
1719 )
1720 .unwrap();
1721 assert_ne!(quoted, higher);
1722 }
1723
1724 #[test]
1725 fn client_fee_signing_hash_differs_by_receiver() {
1726 let other_receiver = Bytes::copy_from_slice(&[0x55; 20]);
1727 let h1 = sample_fee_params(100, sample_fee_receiver())
1728 .eip712_signing_hash(
1729 1,
1730 &sample_router_address(),
1731 &sample_amount_in(),
1732 &sample_token_in(),
1733 &sample_token_out(),
1734 &sample_expected_amount_out(),
1735 &sample_min_amount_out(),
1736 &sample_swap_receiver(),
1737 &sample_swaps_hash(),
1738 )
1739 .unwrap();
1740 let h2 = sample_fee_params(100, other_receiver)
1741 .eip712_signing_hash(
1742 1,
1743 &sample_router_address(),
1744 &sample_amount_in(),
1745 &sample_token_in(),
1746 &sample_token_out(),
1747 &sample_expected_amount_out(),
1748 &sample_min_amount_out(),
1749 &sample_swap_receiver(),
1750 &sample_swaps_hash(),
1751 )
1752 .unwrap();
1753 assert_ne!(h1, h2);
1754 }
1755
1756 #[test]
1757 fn client_fee_signing_hash_rejects_bad_receiver_address() {
1758 let bad_addr = Bytes::copy_from_slice(&[0x44; 4]);
1759 let fee = sample_fee_params(100, bad_addr);
1760 assert!(matches!(
1761 fee.eip712_signing_hash(
1762 1,
1763 &sample_router_address(),
1764 &sample_amount_in(),
1765 &sample_token_in(),
1766 &sample_token_out(),
1767 &sample_expected_amount_out(),
1768 &sample_min_amount_out(),
1769 &sample_swap_receiver(),
1770 &sample_swaps_hash(),
1771 ),
1772 Err(crate::error::FyndError::Protocol(_))
1773 ));
1774 }
1775
1776 #[test]
1777 fn client_fee_signing_hash_rejects_bad_router_address() {
1778 let bad_addr = Bytes::copy_from_slice(&[0x33; 4]);
1779 let fee = sample_fee_params(100, sample_fee_receiver());
1780 assert!(matches!(
1781 fee.eip712_signing_hash(
1782 1,
1783 &bad_addr,
1784 &sample_amount_in(),
1785 &sample_token_in(),
1786 &sample_token_out(),
1787 &sample_expected_amount_out(),
1788 &sample_min_amount_out(),
1789 &sample_swap_receiver(),
1790 &sample_swaps_hash(),
1791 ),
1792 Err(crate::error::FyndError::Protocol(_))
1793 ));
1794 }
1795}