1#![deny(missing_docs)]
2use num_bigint::BigUint;
16use serde::{Deserialize, Serialize};
17use serde_with::{serde_as, DisplayFromStr};
18use uuid::Uuid;
19
20mod hex_bytes_serde {
28 use serde::{Deserialize, Deserializer, Serializer};
29
30 pub fn serialize<S>(x: &bytes::Bytes, s: S) -> Result<S::Ok, S::Error>
31 where
32 S: Serializer,
33 {
34 s.serialize_str(&format!("0x{}", hex::encode(x.as_ref())))
35 }
36
37 pub fn deserialize<'de, D>(d: D) -> Result<bytes::Bytes, D::Error>
38 where
39 D: Deserializer<'de>,
40 {
41 let s = String::deserialize(d)?;
42 let stripped = s.strip_prefix("0x").unwrap_or(&s);
43 hex::decode(stripped)
44 .map(bytes::Bytes::from)
45 .map_err(serde::de::Error::custom)
46 }
47}
48
49#[derive(Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
56pub struct Bytes(#[serde(with = "hex_bytes_serde")] pub bytes::Bytes);
57
58impl Bytes {
59 pub fn len(&self) -> usize {
61 self.0.len()
62 }
63
64 pub fn is_empty(&self) -> bool {
66 self.0.is_empty()
67 }
68}
69
70impl std::fmt::Debug for Bytes {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 write!(f, "Bytes(0x{})", hex::encode(self.0.as_ref()))
73 }
74}
75
76impl AsRef<[u8]> for Bytes {
77 fn as_ref(&self) -> &[u8] {
78 self.0.as_ref()
79 }
80}
81
82impl From<&[u8]> for Bytes {
83 fn from(src: &[u8]) -> Self {
84 Self(bytes::Bytes::copy_from_slice(src))
85 }
86}
87
88impl From<Vec<u8>> for Bytes {
89 fn from(src: Vec<u8>) -> Self {
90 Self(src.into())
91 }
92}
93
94impl From<bytes::Bytes> for Bytes {
95 fn from(src: bytes::Bytes) -> Self {
96 Self(src)
97 }
98}
99
100impl<const N: usize> From<[u8; N]> for Bytes {
101 fn from(src: [u8; N]) -> Self {
102 Self(bytes::Bytes::copy_from_slice(&src))
103 }
104}
105
106pub type Address = Bytes;
108
109#[must_use]
115#[derive(Debug, Clone, Serialize, Deserialize)]
116#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
117pub struct QuoteRequest {
118 orders: Vec<Order>,
120 #[serde(default)]
122 options: QuoteOptions,
123}
124
125impl QuoteRequest {
126 pub fn new(orders: Vec<Order>) -> Self {
128 Self { orders, options: QuoteOptions::default() }
129 }
130
131 pub fn with_options(mut self, options: QuoteOptions) -> Self {
133 self.options = options;
134 self
135 }
136
137 pub fn orders(&self) -> &[Order] {
139 &self.orders
140 }
141
142 pub fn options(&self) -> &QuoteOptions {
144 &self.options
145 }
146}
147
148#[must_use]
150#[serde_as]
151#[derive(Debug, Clone, Default, Serialize, Deserialize)]
152#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
153pub struct QuoteOptions {
154 #[cfg_attr(feature = "openapi", schema(example = 2000))]
156 timeout_ms: Option<u64>,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
163 min_responses: Option<usize>,
164 #[serde_as(as = "Option<DisplayFromStr>")]
166 #[serde(default, skip_serializing_if = "Option::is_none")]
167 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "500000"))]
168 max_gas: Option<BigUint>,
169 encoding_options: Option<EncodingOptions>,
171}
172
173impl QuoteOptions {
174 pub fn with_timeout_ms(mut self, ms: u64) -> Self {
176 self.timeout_ms = Some(ms);
177 self
178 }
179
180 pub fn with_min_responses(mut self, n: usize) -> Self {
182 self.min_responses = Some(n);
183 self
184 }
185
186 pub fn with_max_gas(mut self, gas: BigUint) -> Self {
188 self.max_gas = Some(gas);
189 self
190 }
191
192 pub fn with_encoding_options(mut self, opts: EncodingOptions) -> Self {
194 self.encoding_options = Some(opts);
195 self
196 }
197
198 pub fn timeout_ms(&self) -> Option<u64> {
200 self.timeout_ms
201 }
202
203 pub fn min_responses(&self) -> Option<usize> {
205 self.min_responses
206 }
207
208 pub fn max_gas(&self) -> Option<&BigUint> {
210 self.max_gas.as_ref()
211 }
212
213 pub fn encoding_options(&self) -> Option<&EncodingOptions> {
215 self.encoding_options.as_ref()
216 }
217}
218
219#[derive(Debug, Clone, Default, Serialize, Deserialize)]
223#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
224pub struct PriceGuardConfig {
225 #[serde(default, skip_serializing_if = "Option::is_none")]
227 #[cfg_attr(feature = "openapi", schema(example = 300))]
228 lower_tolerance_bps: Option<u32>,
229 #[serde(default, skip_serializing_if = "Option::is_none")]
231 #[cfg_attr(feature = "openapi", schema(example = 10000))]
232 upper_tolerance_bps: Option<u32>,
233 #[serde(default, skip_serializing_if = "Option::is_none")]
235 fail_on_provider_error: Option<bool>,
236 #[serde(default, skip_serializing_if = "Option::is_none")]
238 fail_on_token_price_not_found: Option<bool>,
239 #[serde(default, skip_serializing_if = "Option::is_none")]
241 enabled: Option<bool>,
242}
243
244impl PriceGuardConfig {
245 pub fn with_lower_tolerance_bps(mut self, bps: u32) -> Self {
247 self.lower_tolerance_bps = Some(bps);
248 self
249 }
250
251 pub fn with_upper_tolerance_bps(mut self, bps: u32) -> Self {
253 self.upper_tolerance_bps = Some(bps);
254 self
255 }
256
257 pub fn with_fail_on_provider_error(mut self, fail: bool) -> Self {
259 self.fail_on_provider_error = Some(fail);
260 self
261 }
262
263 pub fn with_fail_on_token_price_not_found(mut self, fail: bool) -> Self {
265 self.fail_on_token_price_not_found = Some(fail);
266 self
267 }
268
269 pub fn with_enabled(mut self, enabled: bool) -> Self {
271 self.enabled = Some(enabled);
272 self
273 }
274
275 pub fn lower_tolerance_bps(&self) -> Option<u32> {
277 self.lower_tolerance_bps
278 }
279
280 pub fn upper_tolerance_bps(&self) -> Option<u32> {
282 self.upper_tolerance_bps
283 }
284
285 pub fn fail_on_provider_error(&self) -> Option<bool> {
287 self.fail_on_provider_error
288 }
289
290 pub fn fail_on_token_price_not_found(&self) -> Option<bool> {
292 self.fail_on_token_price_not_found
293 }
294
295 pub fn enabled(&self) -> Option<bool> {
297 self.enabled
298 }
299}
300
301#[non_exhaustive]
303#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
304#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
305#[serde(rename_all = "snake_case")]
306pub enum UserTransferType {
307 TransferFromPermit2,
309 #[default]
311 TransferFrom,
312 UseVaultsFunds,
314}
315
316#[serde_as]
321#[derive(Debug, Clone, Serialize, Deserialize)]
322#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
323pub struct ClientFeeParams {
324 #[cfg_attr(feature = "openapi", schema(example = 100))]
326 bps: u16,
327 #[cfg_attr(
329 feature = "openapi",
330 schema(value_type = String, example = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
331 )]
332 receiver: Bytes,
333 #[serde_as(as = "DisplayFromStr")]
335 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0"))]
336 max_contribution: BigUint,
337 #[cfg_attr(feature = "openapi", schema(example = 1893456000))]
339 deadline: u64,
340 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0xabcd..."))]
342 signature: Bytes,
343}
344
345impl ClientFeeParams {
346 pub fn new(
348 bps: u16,
349 receiver: Bytes,
350 max_contribution: BigUint,
351 deadline: u64,
352 signature: Bytes,
353 ) -> Self {
354 Self { bps, receiver, max_contribution, deadline, signature }
355 }
356
357 pub fn bps(&self) -> u16 {
359 self.bps
360 }
361
362 pub fn receiver(&self) -> &Bytes {
364 &self.receiver
365 }
366
367 pub fn max_contribution(&self) -> &BigUint {
369 &self.max_contribution
370 }
371
372 pub fn deadline(&self) -> u64 {
374 self.deadline
375 }
376
377 pub fn signature(&self) -> &Bytes {
379 &self.signature
380 }
381}
382
383#[serde_as]
387#[derive(Debug, Clone, Serialize, Deserialize)]
388#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
389pub struct FeeBreakdown {
390 #[serde_as(as = "DisplayFromStr")]
392 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "350000"))]
393 router_fee: BigUint,
394 #[serde_as(as = "DisplayFromStr")]
396 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "2800000"))]
397 client_fee: BigUint,
398 #[serde_as(as = "DisplayFromStr")]
400 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3496850"))]
401 max_slippage: BigUint,
402 #[serde_as(as = "DisplayFromStr")]
405 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3493353150"))]
406 min_amount_received: BigUint,
407 #[serde(default, skip_serializing_if = "Option::is_none")]
412 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = json!(null)))]
413 swaps_hash: Option<Bytes>,
414}
415
416impl FeeBreakdown {
417 pub fn router_fee(&self) -> &BigUint {
419 &self.router_fee
420 }
421
422 pub fn client_fee(&self) -> &BigUint {
424 &self.client_fee
425 }
426
427 pub fn max_slippage(&self) -> &BigUint {
429 &self.max_slippage
430 }
431
432 pub fn min_amount_received(&self) -> &BigUint {
434 &self.min_amount_received
435 }
436
437 pub fn swaps_hash(&self) -> Option<&Bytes> {
440 self.swaps_hash.as_ref()
441 }
442}
443
444#[must_use]
446#[serde_as]
447#[derive(Debug, Clone, Serialize, Deserialize)]
448#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
449pub struct EncodingOptions {
450 #[serde_as(as = "DisplayFromStr")]
451 #[cfg_attr(feature = "openapi", schema(example = "0.001"))]
452 slippage: f64,
453 #[serde(default)]
455 transfer_type: UserTransferType,
456 #[serde(default, skip_serializing_if = "Option::is_none")]
458 permit: Option<PermitSingle>,
459 #[serde(default, skip_serializing_if = "Option::is_none")]
461 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "0xabcd..."))]
462 permit2_signature: Option<Bytes>,
463 #[serde(default, skip_serializing_if = "Option::is_none")]
465 client_fee_params: Option<ClientFeeParams>,
466 #[serde(default, skip_serializing_if = "Option::is_none")]
468 price_guard: Option<PriceGuardConfig>,
469}
470
471impl EncodingOptions {
472 pub fn new(slippage: f64) -> Self {
474 Self {
475 slippage,
476 transfer_type: UserTransferType::default(),
477 permit: None,
478 permit2_signature: None,
479 client_fee_params: None,
480 price_guard: None,
481 }
482 }
483
484 pub fn with_transfer_type(mut self, t: UserTransferType) -> Self {
486 self.transfer_type = t;
487 self
488 }
489
490 pub fn with_permit2(mut self, permit: PermitSingle, sig: Bytes) -> Self {
492 self.permit = Some(permit);
493 self.permit2_signature = Some(sig);
494 self
495 }
496
497 pub fn slippage(&self) -> f64 {
499 self.slippage
500 }
501
502 pub fn transfer_type(&self) -> &UserTransferType {
504 &self.transfer_type
505 }
506
507 pub fn permit(&self) -> Option<&PermitSingle> {
509 self.permit.as_ref()
510 }
511
512 pub fn permit2_signature(&self) -> Option<&Bytes> {
514 self.permit2_signature.as_ref()
515 }
516
517 pub fn with_client_fee_params(mut self, params: ClientFeeParams) -> Self {
519 self.client_fee_params = Some(params);
520 self
521 }
522
523 pub fn client_fee_params(&self) -> Option<&ClientFeeParams> {
525 self.client_fee_params.as_ref()
526 }
527
528 pub fn with_price_guard(mut self, config: PriceGuardConfig) -> Self {
530 self.price_guard = Some(config);
531 self
532 }
533
534 pub fn price_guard(&self) -> Option<&PriceGuardConfig> {
536 self.price_guard.as_ref()
537 }
538}
539
540#[serde_as]
542#[derive(Debug, Clone, Serialize, Deserialize)]
543#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
544pub struct PermitSingle {
545 details: PermitDetails,
547 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"))]
549 spender: Bytes,
550 #[serde_as(as = "DisplayFromStr")]
552 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "1893456000"))]
553 sig_deadline: BigUint,
554}
555
556impl PermitSingle {
557 pub fn new(details: PermitDetails, spender: Bytes, sig_deadline: BigUint) -> Self {
559 Self { details, spender, sig_deadline }
560 }
561
562 pub fn details(&self) -> &PermitDetails {
564 &self.details
565 }
566
567 pub fn spender(&self) -> &Bytes {
569 &self.spender
570 }
571
572 pub fn sig_deadline(&self) -> &BigUint {
574 &self.sig_deadline
575 }
576}
577
578#[serde_as]
580#[derive(Debug, Clone, Serialize, Deserialize)]
581#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
582pub struct PermitDetails {
583 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"))]
585 token: Bytes,
586 #[serde_as(as = "DisplayFromStr")]
588 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "1000000000000000000"))]
589 amount: BigUint,
590 #[serde_as(as = "DisplayFromStr")]
592 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "1893456000"))]
593 expiration: BigUint,
594 #[serde_as(as = "DisplayFromStr")]
596 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0"))]
597 nonce: BigUint,
598}
599
600impl PermitDetails {
601 pub fn new(token: Bytes, amount: BigUint, expiration: BigUint, nonce: BigUint) -> Self {
603 Self { token, amount, expiration, nonce }
604 }
605
606 pub fn token(&self) -> &Bytes {
608 &self.token
609 }
610
611 pub fn amount(&self) -> &BigUint {
613 &self.amount
614 }
615
616 pub fn expiration(&self) -> &BigUint {
618 &self.expiration
619 }
620
621 pub fn nonce(&self) -> &BigUint {
623 &self.nonce
624 }
625}
626
627#[must_use]
636#[serde_as]
637#[derive(Debug, Clone, Serialize, Deserialize)]
638#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
639pub struct Quote {
640 orders: Vec<OrderQuote>,
642 #[serde_as(as = "DisplayFromStr")]
644 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "150000"))]
645 total_gas_estimate: BigUint,
646 #[cfg_attr(feature = "openapi", schema(example = 12))]
648 solve_time_ms: u64,
649}
650
651impl Quote {
652 pub fn new(orders: Vec<OrderQuote>, total_gas_estimate: BigUint, solve_time_ms: u64) -> Self {
654 Self { orders, total_gas_estimate, solve_time_ms }
655 }
656
657 pub fn orders(&self) -> &[OrderQuote] {
659 &self.orders
660 }
661
662 pub fn into_orders(self) -> Vec<OrderQuote> {
664 self.orders
665 }
666
667 pub fn total_gas_estimate(&self) -> &BigUint {
669 &self.total_gas_estimate
670 }
671
672 pub fn solve_time_ms(&self) -> u64 {
674 self.solve_time_ms
675 }
676}
677
678#[must_use]
682#[serde_as]
683#[derive(Debug, Clone, Serialize, Deserialize)]
684#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
685pub struct Order {
686 #[serde(default = "generate_order_id", skip_deserializing)]
690 id: String,
691 #[cfg_attr(
693 feature = "openapi",
694 schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
695 )]
696 token_in: Address,
697 #[cfg_attr(
699 feature = "openapi",
700 schema(value_type = String, example = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
701 )]
702 token_out: Address,
703 #[serde_as(as = "DisplayFromStr")]
705 #[cfg_attr(
706 feature = "openapi",
707 schema(value_type = String, example = "1000000000000000000")
708 )]
709 amount: BigUint,
710 side: OrderSide,
712 #[cfg_attr(
714 feature = "openapi",
715 schema(value_type = String, example = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
716 )]
717 sender: Address,
718 #[serde(default, skip_serializing_if = "Option::is_none")]
722 #[cfg_attr(
723 feature = "openapi",
724 schema(value_type = Option<String>, example = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
725 )]
726 receiver: Option<Address>,
727}
728
729impl Order {
730 pub fn new(
732 token_in: Address,
733 token_out: Address,
734 amount: BigUint,
735 side: OrderSide,
736 sender: Address,
737 ) -> Self {
738 Self { id: String::new(), token_in, token_out, amount, side, sender, receiver: None }
739 }
740
741 pub fn with_id(mut self, id: impl Into<String>) -> Self {
743 self.id = id.into();
744 self
745 }
746
747 pub fn with_receiver(mut self, receiver: Address) -> Self {
749 self.receiver = Some(receiver);
750 self
751 }
752
753 pub fn id(&self) -> &str {
755 &self.id
756 }
757
758 pub fn token_in(&self) -> &Address {
760 &self.token_in
761 }
762
763 pub fn token_out(&self) -> &Address {
765 &self.token_out
766 }
767
768 pub fn amount(&self) -> &BigUint {
770 &self.amount
771 }
772
773 pub fn side(&self) -> OrderSide {
775 self.side
776 }
777
778 pub fn sender(&self) -> &Address {
780 &self.sender
781 }
782
783 pub fn receiver(&self) -> Option<&Address> {
785 self.receiver.as_ref()
786 }
787}
788
789#[non_exhaustive]
793#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
794#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
795#[serde(rename_all = "snake_case")]
796pub enum OrderSide {
797 Sell,
799}
800
801#[must_use]
806#[serde_as]
807#[derive(Debug, Clone, Serialize, Deserialize)]
808#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
809pub struct OrderQuote {
810 #[cfg_attr(feature = "openapi", schema(example = "f47ac10b-58cc-4372-a567-0e02b2c3d479"))]
812 order_id: String,
813 status: QuoteStatus,
815 #[serde(skip_serializing_if = "Option::is_none")]
817 route: Option<Route>,
818 #[serde_as(as = "DisplayFromStr")]
820 #[cfg_attr(
821 feature = "openapi",
822 schema(value_type = String, example = "1000000000000000000")
823 )]
824 amount_in: BigUint,
825 #[serde_as(as = "DisplayFromStr")]
827 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3500000000"))]
828 amount_out: BigUint,
829 #[serde_as(as = "DisplayFromStr")]
831 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "150000"))]
832 gas_estimate: BigUint,
833 #[serde(skip_serializing_if = "Option::is_none")]
835 price_impact_bps: Option<i32>,
836 #[serde_as(as = "DisplayFromStr")]
839 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3498000000"))]
840 amount_out_net_gas: BigUint,
841 block: BlockInfo,
843 #[serde_as(as = "Option<DisplayFromStr>")]
845 #[serde(skip_serializing_if = "Option::is_none")]
846 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "20000000000"))]
847 gas_price: Option<BigUint>,
848 transaction: Option<Transaction>,
850 #[serde(skip_serializing_if = "Option::is_none")]
852 fee_breakdown: Option<FeeBreakdown>,
853}
854
855impl OrderQuote {
856 pub fn order_id(&self) -> &str {
858 &self.order_id
859 }
860
861 pub fn status(&self) -> QuoteStatus {
863 self.status
864 }
865
866 pub fn route(&self) -> Option<&Route> {
868 self.route.as_ref()
869 }
870
871 pub fn amount_in(&self) -> &BigUint {
873 &self.amount_in
874 }
875
876 pub fn amount_out(&self) -> &BigUint {
878 &self.amount_out
879 }
880
881 pub fn gas_estimate(&self) -> &BigUint {
883 &self.gas_estimate
884 }
885
886 pub fn price_impact_bps(&self) -> Option<i32> {
888 self.price_impact_bps
889 }
890
891 pub fn amount_out_net_gas(&self) -> &BigUint {
893 &self.amount_out_net_gas
894 }
895
896 pub fn block(&self) -> &BlockInfo {
898 &self.block
899 }
900
901 pub fn gas_price(&self) -> Option<&BigUint> {
903 self.gas_price.as_ref()
904 }
905
906 pub fn transaction(&self) -> Option<&Transaction> {
908 self.transaction.as_ref()
909 }
910
911 pub fn fee_breakdown(&self) -> Option<&FeeBreakdown> {
913 self.fee_breakdown.as_ref()
914 }
915}
916
917#[non_exhaustive]
919#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
920#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
921#[serde(rename_all = "snake_case")]
922pub enum QuoteStatus {
923 Success,
925 NoRouteFound,
927 InsufficientLiquidity,
929 Timeout,
931 NotReady,
933 PriceCheckFailed,
935}
936
937#[derive(Debug, Clone, Serialize, Deserialize)]
942#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
943pub struct BlockInfo {
944 #[cfg_attr(feature = "openapi", schema(example = 21000000))]
946 number: u64,
947 #[cfg_attr(
949 feature = "openapi",
950 schema(example = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd")
951 )]
952 hash: String,
953 #[cfg_attr(feature = "openapi", schema(example = 1730000000))]
955 timestamp: u64,
956}
957
958impl BlockInfo {
959 pub fn new(number: u64, hash: String, timestamp: u64) -> Self {
961 Self { number, hash, timestamp }
962 }
963
964 pub fn number(&self) -> u64 {
966 self.number
967 }
968
969 pub fn hash(&self) -> &str {
971 &self.hash
972 }
973
974 pub fn timestamp(&self) -> u64 {
976 self.timestamp
977 }
978}
979
980#[must_use]
989#[derive(Debug, Clone, Serialize, Deserialize)]
990#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
991pub struct Route {
992 swaps: Vec<Swap>,
994}
995
996impl Route {
997 pub fn new(swaps: Vec<Swap>) -> Self {
999 Self { swaps }
1000 }
1001
1002 pub fn swaps(&self) -> &[Swap] {
1004 &self.swaps
1005 }
1006
1007 pub fn into_swaps(self) -> Vec<Swap> {
1009 self.swaps
1010 }
1011}
1012
1013#[serde_as]
1017#[derive(Debug, Clone, Serialize, Deserialize)]
1018#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1019pub struct Swap {
1020 #[cfg_attr(
1022 feature = "openapi",
1023 schema(example = "0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc")
1024 )]
1025 component_id: String,
1026 #[cfg_attr(feature = "openapi", schema(example = "uniswap_v2"))]
1028 protocol: String,
1029 #[cfg_attr(
1031 feature = "openapi",
1032 schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
1033 )]
1034 token_in: Address,
1035 #[cfg_attr(
1037 feature = "openapi",
1038 schema(value_type = String, example = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
1039 )]
1040 token_out: Address,
1041 #[serde_as(as = "DisplayFromStr")]
1043 #[cfg_attr(
1044 feature = "openapi",
1045 schema(value_type = String, example = "1000000000000000000")
1046 )]
1047 amount_in: BigUint,
1048 #[serde_as(as = "DisplayFromStr")]
1050 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3500000000"))]
1051 amount_out: BigUint,
1052 #[serde_as(as = "DisplayFromStr")]
1054 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "150000"))]
1055 gas_estimate: BigUint,
1056 #[serde_as(as = "DisplayFromStr")]
1058 #[cfg_attr(feature = "openapi", schema(example = "0.0"))]
1059 split: f64,
1060}
1061
1062impl Swap {
1063 #[allow(clippy::too_many_arguments)]
1065 pub fn new(
1066 component_id: String,
1067 protocol: String,
1068 token_in: Address,
1069 token_out: Address,
1070 amount_in: BigUint,
1071 amount_out: BigUint,
1072 gas_estimate: BigUint,
1073 split: f64,
1074 ) -> Self {
1075 Self {
1076 component_id,
1077 protocol,
1078 token_in,
1079 token_out,
1080 amount_in,
1081 amount_out,
1082 gas_estimate,
1083 split,
1084 }
1085 }
1086
1087 pub fn component_id(&self) -> &str {
1089 &self.component_id
1090 }
1091
1092 pub fn protocol(&self) -> &str {
1094 &self.protocol
1095 }
1096
1097 pub fn token_in(&self) -> &Address {
1099 &self.token_in
1100 }
1101
1102 pub fn token_out(&self) -> &Address {
1104 &self.token_out
1105 }
1106
1107 pub fn amount_in(&self) -> &BigUint {
1109 &self.amount_in
1110 }
1111
1112 pub fn amount_out(&self) -> &BigUint {
1114 &self.amount_out
1115 }
1116
1117 pub fn gas_estimate(&self) -> &BigUint {
1119 &self.gas_estimate
1120 }
1121
1122 pub fn split(&self) -> f64 {
1124 self.split
1125 }
1126}
1127
1128#[derive(Debug, Clone, Serialize, Deserialize)]
1134#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1135pub struct HealthStatus {
1136 #[cfg_attr(feature = "openapi", schema(example = true))]
1138 healthy: bool,
1139 #[cfg_attr(feature = "openapi", schema(example = 1250))]
1141 last_update_ms: u64,
1142 #[cfg_attr(feature = "openapi", schema(example = 2))]
1147 num_solver_pools: usize,
1148 #[serde(default)]
1154 #[cfg_attr(feature = "openapi", schema(example = true))]
1155 derived_data_ready: bool,
1156 #[serde(default, skip_serializing_if = "Option::is_none")]
1158 #[cfg_attr(feature = "openapi", schema(example = 12000))]
1159 gas_price_age_ms: Option<u64>,
1160}
1161
1162impl HealthStatus {
1163 pub fn new(
1165 healthy: bool,
1166 last_update_ms: u64,
1167 num_solver_pools: usize,
1168 derived_data_ready: bool,
1169 gas_price_age_ms: Option<u64>,
1170 ) -> Self {
1171 Self { healthy, last_update_ms, num_solver_pools, derived_data_ready, gas_price_age_ms }
1172 }
1173
1174 pub fn healthy(&self) -> bool {
1176 self.healthy
1177 }
1178
1179 pub fn last_update_ms(&self) -> u64 {
1181 self.last_update_ms
1182 }
1183
1184 pub fn num_solver_pools(&self) -> usize {
1186 self.num_solver_pools
1187 }
1188
1189 pub fn derived_data_ready(&self) -> bool {
1191 self.derived_data_ready
1192 }
1193
1194 pub fn gas_price_age_ms(&self) -> Option<u64> {
1196 self.gas_price_age_ms
1197 }
1198}
1199
1200#[derive(Debug, Clone, Serialize, Deserialize)]
1210#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1211#[non_exhaustive]
1212pub struct InstanceInfo {
1213 #[cfg_attr(feature = "openapi", schema(example = 1))]
1215 chain_id: u64,
1216 #[cfg_attr(
1218 feature = "openapi",
1219 schema(value_type = Option<String>, example = "0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35")
1220 )]
1221 router_address: Option<Bytes>,
1222 #[cfg_attr(
1224 feature = "openapi",
1225 schema(value_type = String, example = "0x000000000022D473030F116dDEE9F6B43aC78BA3")
1226 )]
1227 permit2_address: Bytes,
1228 #[serde(default)]
1232 #[cfg_attr(feature = "openapi", schema(example = "0.89.1"))]
1233 version: String,
1234}
1235
1236impl InstanceInfo {
1237 pub fn builder(
1239 chain_id: u64,
1240 router_address: Option<Bytes>,
1241 permit2_address: Bytes,
1242 ) -> InstanceInfoBuilder {
1243 InstanceInfoBuilder { chain_id, router_address, permit2_address, version: String::new() }
1244 }
1245
1246 pub fn chain_id(&self) -> u64 {
1248 self.chain_id
1249 }
1250
1251 pub fn router_address(&self) -> Option<&Bytes> {
1253 self.router_address.as_ref()
1254 }
1255
1256 pub fn permit2_address(&self) -> &Bytes {
1258 &self.permit2_address
1259 }
1260
1261 pub fn version(&self) -> &str {
1263 &self.version
1264 }
1265}
1266
1267#[derive(Debug, Clone)]
1269pub struct InstanceInfoBuilder {
1270 chain_id: u64,
1271 router_address: Option<Bytes>,
1272 permit2_address: Bytes,
1273 version: String,
1274}
1275
1276impl InstanceInfoBuilder {
1277 pub fn version(mut self, version: impl Into<String>) -> Self {
1279 self.version = version.into();
1280 self
1281 }
1282
1283 pub fn build(self) -> InstanceInfo {
1285 InstanceInfo {
1286 chain_id: self.chain_id,
1287 router_address: self.router_address,
1288 permit2_address: self.permit2_address,
1289 version: self.version,
1290 }
1291 }
1292}
1293
1294#[must_use]
1296#[derive(Debug, Serialize, Deserialize)]
1297#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1298pub struct ErrorResponse {
1299 #[cfg_attr(feature = "openapi", schema(example = "bad request: no orders provided"))]
1300 error: String,
1301 #[cfg_attr(feature = "openapi", schema(example = "BAD_REQUEST"))]
1302 code: String,
1303 #[serde(skip_serializing_if = "Option::is_none")]
1304 details: Option<serde_json::Value>,
1305}
1306
1307impl ErrorResponse {
1308 pub fn new(error: String, code: String) -> Self {
1310 Self { error, code, details: None }
1311 }
1312
1313 pub fn with_details(mut self, details: serde_json::Value) -> Self {
1315 self.details = Some(details);
1316 self
1317 }
1318
1319 pub fn error(&self) -> &str {
1321 &self.error
1322 }
1323
1324 pub fn code(&self) -> &str {
1326 &self.code
1327 }
1328
1329 pub fn details(&self) -> Option<&serde_json::Value> {
1331 self.details.as_ref()
1332 }
1333}
1334
1335#[serde_as]
1341#[derive(Debug, Clone, Serialize, Deserialize)]
1342#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1343pub struct Transaction {
1344 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"))]
1346 to: Bytes,
1347 #[serde_as(as = "DisplayFromStr")]
1349 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0"))]
1350 value: BigUint,
1351 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0x1234567890abcdef"))]
1353 #[serde(serialize_with = "serialize_bytes_hex", deserialize_with = "deserialize_bytes_hex")]
1354 data: Vec<u8>,
1355 #[serde(default, skip_serializing_if = "Option::is_none")]
1358 #[cfg_attr(feature = "openapi", schema(example = json!(null)))]
1359 client_fee_signature_offset: Option<usize>,
1360}
1361
1362impl Transaction {
1363 pub fn new(to: Bytes, value: BigUint, data: Vec<u8>) -> Self {
1365 Self { to, value, data, client_fee_signature_offset: None }
1366 }
1367
1368 pub fn to(&self) -> &Bytes {
1370 &self.to
1371 }
1372
1373 pub fn value(&self) -> &BigUint {
1375 &self.value
1376 }
1377
1378 pub fn data(&self) -> &[u8] {
1380 &self.data
1381 }
1382
1383 pub fn client_fee_signature_offset(&self) -> Option<usize> {
1385 self.client_fee_signature_offset
1386 }
1387}
1388
1389fn serialize_bytes_hex<S>(bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
1395where
1396 S: serde::Serializer,
1397{
1398 serializer.serialize_str(&format!("0x{}", hex::encode(bytes)))
1399}
1400
1401fn deserialize_bytes_hex<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
1403where
1404 D: serde::Deserializer<'de>,
1405{
1406 let s = String::deserialize(deserializer)?;
1407 let s = s.strip_prefix("0x").unwrap_or(&s);
1408 hex::decode(s).map_err(serde::de::Error::custom)
1409}
1410
1411fn generate_order_id() -> String {
1417 Uuid::new_v4().to_string()
1418}
1419
1420#[cfg(test)]
1429mod wire_format_tests {
1430 use num_bigint::BigUint;
1431
1432 use super::*;
1433
1434 #[test]
1441 fn bytes_deserializes_without_0x_prefix() {
1442 let b: Bytes = serde_json::from_str(r#""deadbeef""#).unwrap();
1443 assert_eq!(b.as_ref(), [0xDE, 0xAD, 0xBE, 0xEF]);
1444 }
1445
1446 #[test]
1453 fn order_serializes_to_full_json() {
1454 let order = Order::new(
1455 Bytes::from([0xAAu8; 20]),
1456 Bytes::from([0xBBu8; 20]),
1457 BigUint::from(1_000_000_000_000_000_000u64),
1458 OrderSide::Sell,
1459 Bytes::from([0xCCu8; 20]),
1460 )
1461 .with_id("abc");
1462
1463 assert_eq!(
1464 serde_json::to_value(&order).unwrap(),
1465 serde_json::json!({
1466 "id": "abc",
1467 "token_in": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1468 "token_out": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1469 "amount": "1000000000000000000",
1470 "side": "sell",
1471 "sender": "0xcccccccccccccccccccccccccccccccccccccccc"
1472 })
1473 );
1474 }
1475
1476 #[test]
1483 fn order_quote_deserializes_from_json() {
1484 let json = r#"{
1485 "order_id": "order-1",
1486 "status": "success",
1487 "amount_in": "1000000000000000000",
1488 "amount_out": "2000000000",
1489 "gas_estimate": "150000",
1490 "amount_out_net_gas": "1999000000",
1491 "price_impact_bps": 5,
1492 "block": { "number": 21000000, "hash": "0xdeadbeef", "timestamp": 1700000000 },
1493 "route": { "swaps": [{
1494 "component_id": "pool-1",
1495 "protocol": "uniswap_v3",
1496 "token_in": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1497 "token_out": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1498 "amount_in": "1000000000000000000",
1499 "amount_out": "2000000000",
1500 "gas_estimate": "150000",
1501 "split": "0"
1502 }]}
1503 }"#;
1504
1505 let quote: OrderQuote = serde_json::from_str(json).unwrap();
1506
1507 assert_eq!(quote.status(), QuoteStatus::Success);
1508 assert_eq!(*quote.amount_in(), BigUint::from(1_000_000_000_000_000_000u64));
1509 assert_eq!(quote.price_impact_bps(), Some(5));
1510 assert_eq!(quote.block().number(), 21_000_000);
1511
1512 let swap = "e.route().unwrap().swaps()[0];
1513 assert_eq!(swap.token_in().as_ref(), [0xAAu8; 20]);
1514 assert_eq!(swap.token_out().as_ref(), [0xBBu8; 20]);
1515 assert_eq!(swap.split(), 0.0);
1516 }
1517
1518 #[test]
1525 fn encoding_options_serializes_to_full_json() {
1526 assert_eq!(
1527 serde_json::to_value(EncodingOptions::new(0.005)).unwrap(),
1528 serde_json::json!({
1529 "slippage": "0.005",
1530 "transfer_type": "transfer_from"
1531 })
1532 );
1533 }
1534
1535 #[test]
1542 fn instance_info_deserializes_and_ignores_unknown_fields() {
1543 let json = r#"{
1544 "version": "1.2.3",
1545 "chain_id": 1,
1546 "router_address": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1547 "permit2_address": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1548 "future_field": "ignored"
1549 }"#;
1550
1551 let info: InstanceInfo = serde_json::from_str(json).unwrap();
1552 assert_eq!(info.version(), "1.2.3");
1553 assert_eq!(info.chain_id(), 1);
1554 assert_eq!(info.router_address().unwrap().as_ref(), [0xAAu8; 20]);
1555 assert_eq!(info.permit2_address().as_ref(), [0xBBu8; 20]);
1556 }
1557
1558 #[test]
1559 fn instance_info_builder_sets_fields() {
1560 let info =
1561 InstanceInfo::builder(1, Some(Bytes::from([0xAAu8; 20])), Bytes::from([0xBBu8; 20]))
1562 .version("0.1.0")
1563 .build();
1564
1565 assert_eq!(info.version(), "0.1.0");
1566 assert_eq!(info.chain_id(), 1);
1567 assert_eq!(info.router_address().unwrap().as_ref(), [0xAAu8; 20]);
1568 assert_eq!(info.permit2_address().as_ref(), [0xBBu8; 20]);
1569 }
1570
1571 #[test]
1572 fn instance_info_deserializes_without_version() {
1573 let json = r#"{
1575 "chain_id": 1,
1576 "router_address": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1577 "permit2_address": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
1578 }"#;
1579
1580 let info: InstanceInfo = serde_json::from_str(json).unwrap();
1581 assert_eq!(info.version(), "");
1582 assert_eq!(info.chain_id(), 1);
1583 }
1584}
1585
1586#[cfg(feature = "core")]
1597mod conversions {
1598 use tycho_simulation::tycho_core::Bytes as TychoBytes;
1599
1600 use super::*;
1601
1602 impl From<TychoBytes> for Bytes {
1608 fn from(b: TychoBytes) -> Self {
1609 Self(b.0)
1610 }
1611 }
1612
1613 impl From<Bytes> for TychoBytes {
1614 fn from(b: Bytes) -> Self {
1615 Self(b.0)
1616 }
1617 }
1618
1619 impl Into<fynd_core::QuoteRequest> for QuoteRequest {
1624 fn into(self) -> fynd_core::QuoteRequest {
1625 fynd_core::QuoteRequest::new(
1626 self.orders
1627 .into_iter()
1628 .map(Into::into)
1629 .collect(),
1630 self.options.into(),
1631 )
1632 }
1633 }
1634
1635 impl Into<fynd_core::QuoteOptions> for QuoteOptions {
1636 fn into(self) -> fynd_core::QuoteOptions {
1637 let mut opts = fynd_core::QuoteOptions::default();
1638 if let Some(ms) = self.timeout_ms {
1639 opts = opts.with_timeout_ms(ms);
1640 }
1641 if let Some(n) = self.min_responses {
1642 opts = opts.with_min_responses(n);
1643 }
1644 if let Some(gas) = self.max_gas {
1645 opts = opts.with_max_gas(gas);
1646 }
1647 if let Some(enc) = self.encoding_options {
1648 opts = opts.with_encoding_options(enc.into());
1649 }
1650 opts
1651 }
1652 }
1653
1654 impl Into<fynd_core::PriceGuardConfig> for PriceGuardConfig {
1655 fn into(self) -> fynd_core::PriceGuardConfig {
1656 let mut config = fynd_core::PriceGuardConfig::default();
1657 if let Some(bps) = self.lower_tolerance_bps {
1658 config = config.with_lower_tolerance_bps(bps);
1659 }
1660 if let Some(bps) = self.upper_tolerance_bps {
1661 config = config.with_upper_tolerance_bps(bps);
1662 }
1663 if let Some(fail) = self.fail_on_provider_error {
1664 config = config.with_fail_on_provider_error(fail);
1665 }
1666 if let Some(fail) = self.fail_on_token_price_not_found {
1667 config = config.with_fail_on_token_price_not_found(fail);
1668 }
1669 if let Some(enabled) = self.enabled {
1670 config = config.with_enabled(enabled);
1671 }
1672 config
1673 }
1674 }
1675
1676 impl Into<fynd_core::EncodingOptions> for EncodingOptions {
1677 fn into(self) -> fynd_core::EncodingOptions {
1678 let mut opts = fynd_core::EncodingOptions::new(self.slippage)
1679 .with_transfer_type(self.transfer_type.into());
1680 if let (Some(permit), Some(sig)) = (self.permit, self.permit2_signature) {
1681 opts = opts
1682 .with_permit(permit.into())
1683 .with_signature(sig.into());
1684 }
1685 if let Some(fee) = self.client_fee_params {
1686 opts = opts.with_client_fee_params(fee.into());
1687 }
1688 if let Some(pg) = self.price_guard {
1689 opts = opts.with_price_guard(pg.into());
1690 }
1691 opts
1692 }
1693 }
1694
1695 impl Into<fynd_core::ClientFeeParams> for ClientFeeParams {
1696 fn into(self) -> fynd_core::ClientFeeParams {
1697 fynd_core::ClientFeeParams::new(
1698 self.bps,
1699 self.receiver.into(),
1700 self.max_contribution,
1701 self.deadline,
1702 self.signature.into(),
1703 )
1704 }
1705 }
1706
1707 impl Into<fynd_core::UserTransferType> for UserTransferType {
1708 fn into(self) -> fynd_core::UserTransferType {
1709 match self {
1710 UserTransferType::TransferFromPermit2 => {
1711 fynd_core::UserTransferType::TransferFromPermit2
1712 }
1713 UserTransferType::TransferFrom => fynd_core::UserTransferType::TransferFrom,
1714 UserTransferType::UseVaultsFunds => fynd_core::UserTransferType::UseVaultsFunds,
1715 }
1716 }
1717 }
1718
1719 impl Into<fynd_core::PermitSingle> for PermitSingle {
1720 fn into(self) -> fynd_core::PermitSingle {
1721 fynd_core::PermitSingle::new(
1722 self.details.into(),
1723 self.spender.into(),
1724 self.sig_deadline,
1725 )
1726 }
1727 }
1728
1729 impl Into<fynd_core::PermitDetails> for PermitDetails {
1730 fn into(self) -> fynd_core::PermitDetails {
1731 fynd_core::PermitDetails::new(
1732 self.token.into(),
1733 self.amount,
1734 self.expiration,
1735 self.nonce,
1736 )
1737 }
1738 }
1739
1740 impl Into<fynd_core::Order> for Order {
1741 fn into(self) -> fynd_core::Order {
1742 let mut order = fynd_core::Order::new(
1743 self.token_in.into(),
1744 self.token_out.into(),
1745 self.amount,
1746 self.side.into(),
1747 self.sender.into(),
1748 )
1749 .with_id(self.id);
1750 if let Some(r) = self.receiver {
1751 order = order.with_receiver(r.into());
1752 }
1753 order
1754 }
1755 }
1756
1757 impl Into<fynd_core::OrderSide> for OrderSide {
1758 fn into(self) -> fynd_core::OrderSide {
1759 match self {
1760 OrderSide::Sell => fynd_core::OrderSide::Sell,
1761 }
1762 }
1763 }
1764
1765 impl From<fynd_core::Quote> for Quote {
1770 fn from(core: fynd_core::Quote) -> Self {
1771 let solve_time_ms = core.solve_time_ms();
1772 let total_gas_estimate = core.total_gas_estimate().clone();
1773 Self {
1774 orders: core
1775 .into_orders()
1776 .into_iter()
1777 .map(Into::into)
1778 .collect(),
1779 total_gas_estimate,
1780 solve_time_ms,
1781 }
1782 }
1783 }
1784
1785 impl From<fynd_core::OrderQuote> for OrderQuote {
1786 fn from(core: fynd_core::OrderQuote) -> Self {
1787 let order_id = core.order_id().to_string();
1788 let status = core.status().into();
1789 let amount_in = core.amount_in().clone();
1790 let amount_out = core.amount_out().clone();
1791 let gas_estimate = core.gas_estimate().clone();
1792 let price_impact_bps = core.price_impact_bps();
1793 let amount_out_net_gas = core.amount_out_net_gas().clone();
1794 let block = core.block().clone().into();
1795 let gas_price = core.gas_price().cloned();
1796 let transaction = core
1797 .transaction()
1798 .cloned()
1799 .map(Into::into);
1800 let fee_breakdown = core
1801 .fee_breakdown()
1802 .cloned()
1803 .map(Into::into);
1804 let route = core.into_route().map(Into::into);
1805 Self {
1806 order_id,
1807 status,
1808 route,
1809 amount_in,
1810 amount_out,
1811 gas_estimate,
1812 price_impact_bps,
1813 amount_out_net_gas,
1814 block,
1815 gas_price,
1816 transaction,
1817 fee_breakdown,
1818 }
1819 }
1820 }
1821
1822 impl From<fynd_core::QuoteStatus> for QuoteStatus {
1823 fn from(core: fynd_core::QuoteStatus) -> Self {
1824 match core {
1825 fynd_core::QuoteStatus::Success => Self::Success,
1826 fynd_core::QuoteStatus::NoRouteFound => Self::NoRouteFound,
1827 fynd_core::QuoteStatus::InsufficientLiquidity => Self::InsufficientLiquidity,
1828 fynd_core::QuoteStatus::Timeout => Self::Timeout,
1829 fynd_core::QuoteStatus::NotReady => Self::NotReady,
1830 fynd_core::QuoteStatus::PriceCheckFailed => Self::PriceCheckFailed,
1831 _ => Self::NotReady,
1833 }
1834 }
1835 }
1836
1837 impl From<fynd_core::BlockInfo> for BlockInfo {
1838 fn from(core: fynd_core::BlockInfo) -> Self {
1839 Self {
1840 number: core.number(),
1841 hash: core.hash().to_string(),
1842 timestamp: core.timestamp(),
1843 }
1844 }
1845 }
1846
1847 impl From<fynd_core::Route> for Route {
1848 fn from(core: fynd_core::Route) -> Self {
1849 Self {
1850 swaps: core
1851 .into_swaps()
1852 .into_iter()
1853 .map(Into::into)
1854 .collect(),
1855 }
1856 }
1857 }
1858
1859 impl From<fynd_core::Swap> for Swap {
1860 fn from(core: fynd_core::Swap) -> Self {
1861 Self {
1862 component_id: core.component_id().to_string(),
1863 protocol: core.protocol().to_string(),
1864 token_in: core.token_in().clone().into(),
1865 token_out: core.token_out().clone().into(),
1866 amount_in: core.amount_in().clone(),
1867 amount_out: core.amount_out().clone(),
1868 gas_estimate: core.gas_estimate().clone(),
1869 split: *core.split(),
1870 }
1871 }
1872 }
1873
1874 impl From<fynd_core::Transaction> for Transaction {
1875 fn from(core: fynd_core::Transaction) -> Self {
1876 Self {
1877 to: core.to().clone().into(),
1878 value: core.value().clone(),
1879 data: core.data().to_vec(),
1880 client_fee_signature_offset: core.client_fee_signature_offset(),
1881 }
1882 }
1883 }
1884
1885 impl From<fynd_core::FeeBreakdown> for FeeBreakdown {
1886 fn from(core: fynd_core::FeeBreakdown) -> Self {
1887 let swaps_hash = core
1888 .swaps_hash()
1889 .map(|h| Bytes(bytes::Bytes::copy_from_slice(h.as_ref())));
1890 Self {
1891 router_fee: core.router_fee().clone(),
1892 client_fee: core.client_fee().clone(),
1893 max_slippage: core.max_slippage().clone(),
1894 min_amount_received: core.min_amount_received().clone(),
1895 swaps_hash,
1896 }
1897 }
1898 }
1899
1900 #[cfg(test)]
1901 mod tests {
1902 use num_bigint::BigUint;
1903
1904 use super::*;
1905
1906 fn make_address(byte: u8) -> Address {
1907 Address::from([byte; 20])
1908 }
1909
1910 #[test]
1911 fn test_quote_request_roundtrip() {
1912 let dto = QuoteRequest {
1913 orders: vec![Order {
1914 id: "test-id".to_string(),
1915 token_in: make_address(0x01),
1916 token_out: make_address(0x02),
1917 amount: BigUint::from(1000u64),
1918 side: OrderSide::Sell,
1919 sender: make_address(0xAA),
1920 receiver: None,
1921 }],
1922 options: QuoteOptions {
1923 timeout_ms: Some(5000),
1924 min_responses: None,
1925 max_gas: None,
1926 encoding_options: None,
1927 },
1928 };
1929
1930 let core: fynd_core::QuoteRequest = dto.clone().into();
1931 assert_eq!(core.orders().len(), 1);
1932 assert_eq!(core.orders()[0].id(), "test-id");
1933 assert_eq!(core.options().timeout_ms(), Some(5000));
1934 }
1935
1936 #[test]
1937 fn test_quote_from_core() {
1938 let core: fynd_core::Quote = serde_json::from_str(
1939 r#"{"orders":[],"total_gas_estimate":"100000","solve_time_ms":50}"#,
1940 )
1941 .unwrap();
1942
1943 let dto = Quote::from(core);
1944 assert_eq!(dto.total_gas_estimate, BigUint::from(100_000u64));
1945 assert_eq!(dto.solve_time_ms, 50);
1946 }
1947
1948 #[test]
1949 fn test_order_side_into_core() {
1950 let core: fynd_core::OrderSide = OrderSide::Sell.into();
1951 assert_eq!(core, fynd_core::OrderSide::Sell);
1952 }
1953
1954 #[test]
1955 fn test_client_fee_params_into_core() {
1956 let dto = ClientFeeParams::new(
1957 200,
1958 Bytes::from(make_address(0xBB).as_ref()),
1959 BigUint::from(1_000_000u64),
1960 1_893_456_000u64,
1961 Bytes::from(vec![0xABu8; 65]),
1962 );
1963 let core: fynd_core::ClientFeeParams = dto.into();
1964 assert_eq!(core.bps(), 200);
1965 assert_eq!(*core.max_contribution(), BigUint::from(1_000_000u64));
1966 assert_eq!(core.deadline(), 1_893_456_000u64);
1967 assert_eq!(core.signature().len(), 65);
1968 }
1969
1970 #[test]
1971 fn test_encoding_options_with_client_fee_into_core() {
1972 let fee = ClientFeeParams::new(
1973 100,
1974 Bytes::from(make_address(0xCC).as_ref()),
1975 BigUint::from(500u64),
1976 9_999u64,
1977 Bytes::from(vec![0xDEu8; 65]),
1978 );
1979 let dto = EncodingOptions::new(0.005).with_client_fee_params(fee);
1980 let core: fynd_core::EncodingOptions = dto.into();
1981
1982 assert!(core.client_fee_params().is_some());
1983 let core_fee = core.client_fee_params().unwrap();
1984 assert_eq!(core_fee.bps(), 100);
1985 assert_eq!(*core_fee.max_contribution(), BigUint::from(500u64));
1986 }
1987
1988 #[test]
1989 fn test_client_fee_params_serde_roundtrip() {
1990 let fee = ClientFeeParams::new(
1991 150,
1992 Bytes::from(make_address(0xDD).as_ref()),
1993 BigUint::from(999_999u64),
1994 1_700_000_000u64,
1995 Bytes::from(vec![0xFFu8; 65]),
1996 );
1997 let json = serde_json::to_string(&fee).unwrap();
1998 assert!(json.contains(r#""max_contribution":"999999""#));
1999 assert!(json.contains(r#""deadline":1700000000"#));
2000
2001 let deserialized: ClientFeeParams = serde_json::from_str(&json).unwrap();
2002 assert_eq!(deserialized.bps(), 150);
2003 assert_eq!(*deserialized.max_contribution(), BigUint::from(999_999u64));
2004 }
2005
2006 #[test]
2007 fn test_price_guard_config_into_core() {
2008 let dto = PriceGuardConfig::default()
2009 .with_lower_tolerance_bps(200)
2010 .with_upper_tolerance_bps(5000)
2011 .with_fail_on_provider_error(false)
2012 .with_enabled(false);
2013
2014 let config: fynd_core::PriceGuardConfig = dto.into();
2015 assert_eq!(config.lower_tolerance_bps(), 200);
2016 assert_eq!(config.upper_tolerance_bps(), 5000);
2017 assert!(!config.fail_on_provider_error());
2018 assert!(!config.enabled());
2019 }
2020
2021 #[test]
2022 fn test_encoding_options_with_price_guard_roundtrip() {
2023 let enc = EncodingOptions::new(0.01)
2024 .with_price_guard(PriceGuardConfig::default().with_enabled(false));
2025 let dto = QuoteRequest {
2026 orders: vec![Order {
2027 id: "pg-test".to_string(),
2028 token_in: make_address(0x01),
2029 token_out: make_address(0x02),
2030 amount: BigUint::from(1000u64),
2031 side: OrderSide::Sell,
2032 sender: make_address(0xAA),
2033 receiver: None,
2034 }],
2035 options: QuoteOptions::default().with_encoding_options(enc),
2036 };
2037
2038 let core: fynd_core::QuoteRequest = dto.into();
2039 let config = core
2040 .options()
2041 .encoding_options()
2042 .expect("encoding_options should be set")
2043 .price_guard();
2044 assert!(!config.enabled());
2045 }
2046
2047 #[test]
2048 fn test_quote_status_from_core() {
2049 let cases = [
2050 (fynd_core::QuoteStatus::Success, QuoteStatus::Success),
2051 (fynd_core::QuoteStatus::NoRouteFound, QuoteStatus::NoRouteFound),
2052 (fynd_core::QuoteStatus::InsufficientLiquidity, QuoteStatus::InsufficientLiquidity),
2053 (fynd_core::QuoteStatus::Timeout, QuoteStatus::Timeout),
2054 (fynd_core::QuoteStatus::NotReady, QuoteStatus::NotReady),
2055 ];
2056
2057 for (core, expected) in cases {
2058 assert_eq!(QuoteStatus::from(core), expected);
2059 }
2060 }
2061 }
2062}