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}
408
409impl FeeBreakdown {
410 pub fn router_fee(&self) -> &BigUint {
412 &self.router_fee
413 }
414
415 pub fn client_fee(&self) -> &BigUint {
417 &self.client_fee
418 }
419
420 pub fn max_slippage(&self) -> &BigUint {
422 &self.max_slippage
423 }
424
425 pub fn min_amount_received(&self) -> &BigUint {
427 &self.min_amount_received
428 }
429}
430
431#[must_use]
433#[serde_as]
434#[derive(Debug, Clone, Serialize, Deserialize)]
435#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
436pub struct EncodingOptions {
437 #[serde_as(as = "DisplayFromStr")]
438 #[cfg_attr(feature = "openapi", schema(example = "0.001"))]
439 slippage: f64,
440 #[serde(default)]
442 transfer_type: UserTransferType,
443 #[serde(default, skip_serializing_if = "Option::is_none")]
445 permit: Option<PermitSingle>,
446 #[serde(default, skip_serializing_if = "Option::is_none")]
448 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "0xabcd..."))]
449 permit2_signature: Option<Bytes>,
450 #[serde(default, skip_serializing_if = "Option::is_none")]
452 client_fee_params: Option<ClientFeeParams>,
453 #[serde(default, skip_serializing_if = "Option::is_none")]
455 price_guard: Option<PriceGuardConfig>,
456}
457
458impl EncodingOptions {
459 pub fn new(slippage: f64) -> Self {
461 Self {
462 slippage,
463 transfer_type: UserTransferType::default(),
464 permit: None,
465 permit2_signature: None,
466 client_fee_params: None,
467 price_guard: None,
468 }
469 }
470
471 pub fn with_transfer_type(mut self, t: UserTransferType) -> Self {
473 self.transfer_type = t;
474 self
475 }
476
477 pub fn with_permit2(mut self, permit: PermitSingle, sig: Bytes) -> Self {
479 self.permit = Some(permit);
480 self.permit2_signature = Some(sig);
481 self
482 }
483
484 pub fn slippage(&self) -> f64 {
486 self.slippage
487 }
488
489 pub fn transfer_type(&self) -> &UserTransferType {
491 &self.transfer_type
492 }
493
494 pub fn permit(&self) -> Option<&PermitSingle> {
496 self.permit.as_ref()
497 }
498
499 pub fn permit2_signature(&self) -> Option<&Bytes> {
501 self.permit2_signature.as_ref()
502 }
503
504 pub fn with_client_fee_params(mut self, params: ClientFeeParams) -> Self {
506 self.client_fee_params = Some(params);
507 self
508 }
509
510 pub fn client_fee_params(&self) -> Option<&ClientFeeParams> {
512 self.client_fee_params.as_ref()
513 }
514
515 pub fn with_price_guard(mut self, config: PriceGuardConfig) -> Self {
517 self.price_guard = Some(config);
518 self
519 }
520
521 pub fn price_guard(&self) -> Option<&PriceGuardConfig> {
523 self.price_guard.as_ref()
524 }
525}
526
527#[serde_as]
529#[derive(Debug, Clone, Serialize, Deserialize)]
530#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
531pub struct PermitSingle {
532 details: PermitDetails,
534 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"))]
536 spender: Bytes,
537 #[serde_as(as = "DisplayFromStr")]
539 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "1893456000"))]
540 sig_deadline: BigUint,
541}
542
543impl PermitSingle {
544 pub fn new(details: PermitDetails, spender: Bytes, sig_deadline: BigUint) -> Self {
546 Self { details, spender, sig_deadline }
547 }
548
549 pub fn details(&self) -> &PermitDetails {
551 &self.details
552 }
553
554 pub fn spender(&self) -> &Bytes {
556 &self.spender
557 }
558
559 pub fn sig_deadline(&self) -> &BigUint {
561 &self.sig_deadline
562 }
563}
564
565#[serde_as]
567#[derive(Debug, Clone, Serialize, Deserialize)]
568#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
569pub struct PermitDetails {
570 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"))]
572 token: Bytes,
573 #[serde_as(as = "DisplayFromStr")]
575 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "1000000000000000000"))]
576 amount: BigUint,
577 #[serde_as(as = "DisplayFromStr")]
579 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "1893456000"))]
580 expiration: BigUint,
581 #[serde_as(as = "DisplayFromStr")]
583 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0"))]
584 nonce: BigUint,
585}
586
587impl PermitDetails {
588 pub fn new(token: Bytes, amount: BigUint, expiration: BigUint, nonce: BigUint) -> Self {
590 Self { token, amount, expiration, nonce }
591 }
592
593 pub fn token(&self) -> &Bytes {
595 &self.token
596 }
597
598 pub fn amount(&self) -> &BigUint {
600 &self.amount
601 }
602
603 pub fn expiration(&self) -> &BigUint {
605 &self.expiration
606 }
607
608 pub fn nonce(&self) -> &BigUint {
610 &self.nonce
611 }
612}
613
614#[must_use]
623#[serde_as]
624#[derive(Debug, Clone, Serialize, Deserialize)]
625#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
626pub struct Quote {
627 orders: Vec<OrderQuote>,
629 #[serde_as(as = "DisplayFromStr")]
631 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "150000"))]
632 total_gas_estimate: BigUint,
633 #[cfg_attr(feature = "openapi", schema(example = 12))]
635 solve_time_ms: u64,
636}
637
638impl Quote {
639 pub fn new(orders: Vec<OrderQuote>, total_gas_estimate: BigUint, solve_time_ms: u64) -> Self {
641 Self { orders, total_gas_estimate, solve_time_ms }
642 }
643
644 pub fn orders(&self) -> &[OrderQuote] {
646 &self.orders
647 }
648
649 pub fn into_orders(self) -> Vec<OrderQuote> {
651 self.orders
652 }
653
654 pub fn total_gas_estimate(&self) -> &BigUint {
656 &self.total_gas_estimate
657 }
658
659 pub fn solve_time_ms(&self) -> u64 {
661 self.solve_time_ms
662 }
663}
664
665#[must_use]
669#[serde_as]
670#[derive(Debug, Clone, Serialize, Deserialize)]
671#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
672pub struct Order {
673 #[serde(default = "generate_order_id", skip_deserializing)]
677 id: String,
678 #[cfg_attr(
680 feature = "openapi",
681 schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
682 )]
683 token_in: Address,
684 #[cfg_attr(
686 feature = "openapi",
687 schema(value_type = String, example = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
688 )]
689 token_out: Address,
690 #[serde_as(as = "DisplayFromStr")]
692 #[cfg_attr(
693 feature = "openapi",
694 schema(value_type = String, example = "1000000000000000000")
695 )]
696 amount: BigUint,
697 side: OrderSide,
699 #[cfg_attr(
701 feature = "openapi",
702 schema(value_type = String, example = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
703 )]
704 sender: Address,
705 #[serde(default, skip_serializing_if = "Option::is_none")]
709 #[cfg_attr(
710 feature = "openapi",
711 schema(value_type = Option<String>, example = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
712 )]
713 receiver: Option<Address>,
714}
715
716impl Order {
717 pub fn new(
719 token_in: Address,
720 token_out: Address,
721 amount: BigUint,
722 side: OrderSide,
723 sender: Address,
724 ) -> Self {
725 Self { id: String::new(), token_in, token_out, amount, side, sender, receiver: None }
726 }
727
728 pub fn with_id(mut self, id: impl Into<String>) -> Self {
730 self.id = id.into();
731 self
732 }
733
734 pub fn with_receiver(mut self, receiver: Address) -> Self {
736 self.receiver = Some(receiver);
737 self
738 }
739
740 pub fn id(&self) -> &str {
742 &self.id
743 }
744
745 pub fn token_in(&self) -> &Address {
747 &self.token_in
748 }
749
750 pub fn token_out(&self) -> &Address {
752 &self.token_out
753 }
754
755 pub fn amount(&self) -> &BigUint {
757 &self.amount
758 }
759
760 pub fn side(&self) -> OrderSide {
762 self.side
763 }
764
765 pub fn sender(&self) -> &Address {
767 &self.sender
768 }
769
770 pub fn receiver(&self) -> Option<&Address> {
772 self.receiver.as_ref()
773 }
774}
775
776#[non_exhaustive]
780#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
781#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
782#[serde(rename_all = "snake_case")]
783pub enum OrderSide {
784 Sell,
786}
787
788#[must_use]
793#[serde_as]
794#[derive(Debug, Clone, Serialize, Deserialize)]
795#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
796pub struct OrderQuote {
797 #[cfg_attr(feature = "openapi", schema(example = "f47ac10b-58cc-4372-a567-0e02b2c3d479"))]
799 order_id: String,
800 status: QuoteStatus,
802 #[serde(skip_serializing_if = "Option::is_none")]
804 route: Option<Route>,
805 #[serde_as(as = "DisplayFromStr")]
807 #[cfg_attr(
808 feature = "openapi",
809 schema(value_type = String, example = "1000000000000000000")
810 )]
811 amount_in: BigUint,
812 #[serde_as(as = "DisplayFromStr")]
814 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3500000000"))]
815 amount_out: BigUint,
816 #[serde_as(as = "DisplayFromStr")]
818 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "150000"))]
819 gas_estimate: BigUint,
820 #[serde(skip_serializing_if = "Option::is_none")]
822 price_impact_bps: Option<i32>,
823 #[serde_as(as = "DisplayFromStr")]
826 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3498000000"))]
827 amount_out_net_gas: BigUint,
828 block: BlockInfo,
830 #[serde_as(as = "Option<DisplayFromStr>")]
832 #[serde(skip_serializing_if = "Option::is_none")]
833 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "20000000000"))]
834 gas_price: Option<BigUint>,
835 transaction: Option<Transaction>,
837 #[serde(skip_serializing_if = "Option::is_none")]
839 fee_breakdown: Option<FeeBreakdown>,
840}
841
842impl OrderQuote {
843 pub fn order_id(&self) -> &str {
845 &self.order_id
846 }
847
848 pub fn status(&self) -> QuoteStatus {
850 self.status
851 }
852
853 pub fn route(&self) -> Option<&Route> {
855 self.route.as_ref()
856 }
857
858 pub fn amount_in(&self) -> &BigUint {
860 &self.amount_in
861 }
862
863 pub fn amount_out(&self) -> &BigUint {
865 &self.amount_out
866 }
867
868 pub fn gas_estimate(&self) -> &BigUint {
870 &self.gas_estimate
871 }
872
873 pub fn price_impact_bps(&self) -> Option<i32> {
875 self.price_impact_bps
876 }
877
878 pub fn amount_out_net_gas(&self) -> &BigUint {
880 &self.amount_out_net_gas
881 }
882
883 pub fn block(&self) -> &BlockInfo {
885 &self.block
886 }
887
888 pub fn gas_price(&self) -> Option<&BigUint> {
890 self.gas_price.as_ref()
891 }
892
893 pub fn transaction(&self) -> Option<&Transaction> {
895 self.transaction.as_ref()
896 }
897
898 pub fn fee_breakdown(&self) -> Option<&FeeBreakdown> {
900 self.fee_breakdown.as_ref()
901 }
902}
903
904#[non_exhaustive]
906#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
907#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
908#[serde(rename_all = "snake_case")]
909pub enum QuoteStatus {
910 Success,
912 NoRouteFound,
914 InsufficientLiquidity,
916 Timeout,
918 NotReady,
920 PriceCheckFailed,
922}
923
924#[derive(Debug, Clone, Serialize, Deserialize)]
929#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
930pub struct BlockInfo {
931 #[cfg_attr(feature = "openapi", schema(example = 21000000))]
933 number: u64,
934 #[cfg_attr(
936 feature = "openapi",
937 schema(example = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd")
938 )]
939 hash: String,
940 #[cfg_attr(feature = "openapi", schema(example = 1730000000))]
942 timestamp: u64,
943}
944
945impl BlockInfo {
946 pub fn new(number: u64, hash: String, timestamp: u64) -> Self {
948 Self { number, hash, timestamp }
949 }
950
951 pub fn number(&self) -> u64 {
953 self.number
954 }
955
956 pub fn hash(&self) -> &str {
958 &self.hash
959 }
960
961 pub fn timestamp(&self) -> u64 {
963 self.timestamp
964 }
965}
966
967#[must_use]
976#[derive(Debug, Clone, Serialize, Deserialize)]
977#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
978pub struct Route {
979 swaps: Vec<Swap>,
981}
982
983impl Route {
984 pub fn new(swaps: Vec<Swap>) -> Self {
986 Self { swaps }
987 }
988
989 pub fn swaps(&self) -> &[Swap] {
991 &self.swaps
992 }
993
994 pub fn into_swaps(self) -> Vec<Swap> {
996 self.swaps
997 }
998}
999
1000#[serde_as]
1004#[derive(Debug, Clone, Serialize, Deserialize)]
1005#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1006pub struct Swap {
1007 #[cfg_attr(
1009 feature = "openapi",
1010 schema(example = "0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc")
1011 )]
1012 component_id: String,
1013 #[cfg_attr(feature = "openapi", schema(example = "uniswap_v2"))]
1015 protocol: String,
1016 #[cfg_attr(
1018 feature = "openapi",
1019 schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
1020 )]
1021 token_in: Address,
1022 #[cfg_attr(
1024 feature = "openapi",
1025 schema(value_type = String, example = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
1026 )]
1027 token_out: Address,
1028 #[serde_as(as = "DisplayFromStr")]
1030 #[cfg_attr(
1031 feature = "openapi",
1032 schema(value_type = String, example = "1000000000000000000")
1033 )]
1034 amount_in: BigUint,
1035 #[serde_as(as = "DisplayFromStr")]
1037 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3500000000"))]
1038 amount_out: BigUint,
1039 #[serde_as(as = "DisplayFromStr")]
1041 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "150000"))]
1042 gas_estimate: BigUint,
1043 #[serde_as(as = "DisplayFromStr")]
1045 #[cfg_attr(feature = "openapi", schema(example = "0.0"))]
1046 split: f64,
1047}
1048
1049impl Swap {
1050 #[allow(clippy::too_many_arguments)]
1052 pub fn new(
1053 component_id: String,
1054 protocol: String,
1055 token_in: Address,
1056 token_out: Address,
1057 amount_in: BigUint,
1058 amount_out: BigUint,
1059 gas_estimate: BigUint,
1060 split: f64,
1061 ) -> Self {
1062 Self {
1063 component_id,
1064 protocol,
1065 token_in,
1066 token_out,
1067 amount_in,
1068 amount_out,
1069 gas_estimate,
1070 split,
1071 }
1072 }
1073
1074 pub fn component_id(&self) -> &str {
1076 &self.component_id
1077 }
1078
1079 pub fn protocol(&self) -> &str {
1081 &self.protocol
1082 }
1083
1084 pub fn token_in(&self) -> &Address {
1086 &self.token_in
1087 }
1088
1089 pub fn token_out(&self) -> &Address {
1091 &self.token_out
1092 }
1093
1094 pub fn amount_in(&self) -> &BigUint {
1096 &self.amount_in
1097 }
1098
1099 pub fn amount_out(&self) -> &BigUint {
1101 &self.amount_out
1102 }
1103
1104 pub fn gas_estimate(&self) -> &BigUint {
1106 &self.gas_estimate
1107 }
1108
1109 pub fn split(&self) -> f64 {
1111 self.split
1112 }
1113}
1114
1115#[derive(Debug, Clone, Serialize, Deserialize)]
1121#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1122pub struct HealthStatus {
1123 #[cfg_attr(feature = "openapi", schema(example = true))]
1125 healthy: bool,
1126 #[cfg_attr(feature = "openapi", schema(example = 1250))]
1128 last_update_ms: u64,
1129 #[cfg_attr(feature = "openapi", schema(example = 2))]
1131 num_solver_pools: usize,
1132 #[serde(default)]
1138 #[cfg_attr(feature = "openapi", schema(example = true))]
1139 derived_data_ready: bool,
1140 #[serde(default, skip_serializing_if = "Option::is_none")]
1142 #[cfg_attr(feature = "openapi", schema(example = 12000))]
1143 gas_price_age_ms: Option<u64>,
1144}
1145
1146impl HealthStatus {
1147 pub fn new(
1149 healthy: bool,
1150 last_update_ms: u64,
1151 num_solver_pools: usize,
1152 derived_data_ready: bool,
1153 gas_price_age_ms: Option<u64>,
1154 ) -> Self {
1155 Self { healthy, last_update_ms, num_solver_pools, derived_data_ready, gas_price_age_ms }
1156 }
1157
1158 pub fn healthy(&self) -> bool {
1160 self.healthy
1161 }
1162
1163 pub fn last_update_ms(&self) -> u64 {
1165 self.last_update_ms
1166 }
1167
1168 pub fn num_solver_pools(&self) -> usize {
1170 self.num_solver_pools
1171 }
1172
1173 pub fn derived_data_ready(&self) -> bool {
1175 self.derived_data_ready
1176 }
1177
1178 pub fn gas_price_age_ms(&self) -> Option<u64> {
1180 self.gas_price_age_ms
1181 }
1182}
1183
1184#[derive(Debug, Clone, Serialize, Deserialize)]
1186#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1187pub struct InstanceInfo {
1188 #[cfg_attr(feature = "openapi", schema(example = 1))]
1190 chain_id: u64,
1191 #[cfg_attr(
1193 feature = "openapi",
1194 schema(value_type = String, example = "0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35")
1195 )]
1196 router_address: Bytes,
1197 #[cfg_attr(
1199 feature = "openapi",
1200 schema(value_type = String, example = "0x000000000022D473030F116dDEE9F6B43aC78BA3")
1201 )]
1202 permit2_address: Bytes,
1203}
1204
1205impl InstanceInfo {
1206 pub fn new(chain_id: u64, router_address: Bytes, permit2_address: Bytes) -> Self {
1208 Self { chain_id, router_address, permit2_address }
1209 }
1210
1211 pub fn chain_id(&self) -> u64 {
1213 self.chain_id
1214 }
1215
1216 pub fn router_address(&self) -> &Bytes {
1218 &self.router_address
1219 }
1220
1221 pub fn permit2_address(&self) -> &Bytes {
1223 &self.permit2_address
1224 }
1225}
1226
1227#[must_use]
1229#[derive(Debug, Serialize, Deserialize)]
1230#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1231pub struct ErrorResponse {
1232 #[cfg_attr(feature = "openapi", schema(example = "bad request: no orders provided"))]
1233 error: String,
1234 #[cfg_attr(feature = "openapi", schema(example = "BAD_REQUEST"))]
1235 code: String,
1236 #[serde(skip_serializing_if = "Option::is_none")]
1237 details: Option<serde_json::Value>,
1238}
1239
1240impl ErrorResponse {
1241 pub fn new(error: String, code: String) -> Self {
1243 Self { error, code, details: None }
1244 }
1245
1246 pub fn with_details(mut self, details: serde_json::Value) -> Self {
1248 self.details = Some(details);
1249 self
1250 }
1251
1252 pub fn error(&self) -> &str {
1254 &self.error
1255 }
1256
1257 pub fn code(&self) -> &str {
1259 &self.code
1260 }
1261
1262 pub fn details(&self) -> Option<&serde_json::Value> {
1264 self.details.as_ref()
1265 }
1266}
1267
1268#[serde_as]
1274#[derive(Debug, Clone, Serialize, Deserialize)]
1275#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1276pub struct Transaction {
1277 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"))]
1279 to: Bytes,
1280 #[serde_as(as = "DisplayFromStr")]
1282 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0"))]
1283 value: BigUint,
1284 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0x1234567890abcdef"))]
1286 #[serde(serialize_with = "serialize_bytes_hex", deserialize_with = "deserialize_bytes_hex")]
1287 data: Vec<u8>,
1288}
1289
1290impl Transaction {
1291 pub fn new(to: Bytes, value: BigUint, data: Vec<u8>) -> Self {
1293 Self { to, value, data }
1294 }
1295
1296 pub fn to(&self) -> &Bytes {
1298 &self.to
1299 }
1300
1301 pub fn value(&self) -> &BigUint {
1303 &self.value
1304 }
1305
1306 pub fn data(&self) -> &[u8] {
1308 &self.data
1309 }
1310}
1311
1312fn serialize_bytes_hex<S>(bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
1318where
1319 S: serde::Serializer,
1320{
1321 serializer.serialize_str(&format!("0x{}", hex::encode(bytes)))
1322}
1323
1324fn deserialize_bytes_hex<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
1326where
1327 D: serde::Deserializer<'de>,
1328{
1329 let s = String::deserialize(deserializer)?;
1330 let s = s.strip_prefix("0x").unwrap_or(&s);
1331 hex::decode(s).map_err(serde::de::Error::custom)
1332}
1333
1334fn generate_order_id() -> String {
1340 Uuid::new_v4().to_string()
1341}
1342
1343#[cfg(test)]
1352mod wire_format_tests {
1353 use num_bigint::BigUint;
1354
1355 use super::*;
1356
1357 #[test]
1364 fn bytes_deserializes_without_0x_prefix() {
1365 let b: Bytes = serde_json::from_str(r#""deadbeef""#).unwrap();
1366 assert_eq!(b.as_ref(), [0xDE, 0xAD, 0xBE, 0xEF]);
1367 }
1368
1369 #[test]
1376 fn order_serializes_to_full_json() {
1377 let order = Order::new(
1378 Bytes::from([0xAAu8; 20]),
1379 Bytes::from([0xBBu8; 20]),
1380 BigUint::from(1_000_000_000_000_000_000u64),
1381 OrderSide::Sell,
1382 Bytes::from([0xCCu8; 20]),
1383 )
1384 .with_id("abc");
1385
1386 assert_eq!(
1387 serde_json::to_value(&order).unwrap(),
1388 serde_json::json!({
1389 "id": "abc",
1390 "token_in": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1391 "token_out": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1392 "amount": "1000000000000000000",
1393 "side": "sell",
1394 "sender": "0xcccccccccccccccccccccccccccccccccccccccc"
1395 })
1396 );
1397 }
1398
1399 #[test]
1406 fn order_quote_deserializes_from_json() {
1407 let json = r#"{
1408 "order_id": "order-1",
1409 "status": "success",
1410 "amount_in": "1000000000000000000",
1411 "amount_out": "2000000000",
1412 "gas_estimate": "150000",
1413 "amount_out_net_gas": "1999000000",
1414 "price_impact_bps": 5,
1415 "block": { "number": 21000000, "hash": "0xdeadbeef", "timestamp": 1700000000 },
1416 "route": { "swaps": [{
1417 "component_id": "pool-1",
1418 "protocol": "uniswap_v3",
1419 "token_in": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1420 "token_out": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1421 "amount_in": "1000000000000000000",
1422 "amount_out": "2000000000",
1423 "gas_estimate": "150000",
1424 "split": "0"
1425 }]}
1426 }"#;
1427
1428 let quote: OrderQuote = serde_json::from_str(json).unwrap();
1429
1430 assert_eq!(quote.status(), QuoteStatus::Success);
1431 assert_eq!(*quote.amount_in(), BigUint::from(1_000_000_000_000_000_000u64));
1432 assert_eq!(quote.price_impact_bps(), Some(5));
1433 assert_eq!(quote.block().number(), 21_000_000);
1434
1435 let swap = "e.route().unwrap().swaps()[0];
1436 assert_eq!(swap.token_in().as_ref(), [0xAAu8; 20]);
1437 assert_eq!(swap.token_out().as_ref(), [0xBBu8; 20]);
1438 assert_eq!(swap.split(), 0.0);
1439 }
1440
1441 #[test]
1448 fn encoding_options_serializes_to_full_json() {
1449 assert_eq!(
1450 serde_json::to_value(EncodingOptions::new(0.005)).unwrap(),
1451 serde_json::json!({
1452 "slippage": "0.005",
1453 "transfer_type": "transfer_from"
1454 })
1455 );
1456 }
1457
1458 #[test]
1465 fn instance_info_deserializes_and_ignores_unknown_fields() {
1466 let json = r#"{
1467 "chain_id": 1,
1468 "router_address": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1469 "permit2_address": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1470 "future_field": "ignored"
1471 }"#;
1472
1473 let info: InstanceInfo = serde_json::from_str(json).unwrap();
1474 assert_eq!(info.chain_id(), 1);
1475 assert_eq!(info.router_address().as_ref(), [0xAAu8; 20]);
1476 assert_eq!(info.permit2_address().as_ref(), [0xBBu8; 20]);
1477 }
1478}
1479
1480#[cfg(feature = "core")]
1491mod conversions {
1492 use tycho_simulation::tycho_core::Bytes as TychoBytes;
1493
1494 use super::*;
1495
1496 impl From<TychoBytes> for Bytes {
1502 fn from(b: TychoBytes) -> Self {
1503 Self(b.0)
1504 }
1505 }
1506
1507 impl From<Bytes> for TychoBytes {
1508 fn from(b: Bytes) -> Self {
1509 Self(b.0)
1510 }
1511 }
1512
1513 impl Into<fynd_core::QuoteRequest> for QuoteRequest {
1518 fn into(self) -> fynd_core::QuoteRequest {
1519 fynd_core::QuoteRequest::new(
1520 self.orders
1521 .into_iter()
1522 .map(Into::into)
1523 .collect(),
1524 self.options.into(),
1525 )
1526 }
1527 }
1528
1529 impl Into<fynd_core::QuoteOptions> for QuoteOptions {
1530 fn into(self) -> fynd_core::QuoteOptions {
1531 let mut opts = fynd_core::QuoteOptions::default();
1532 if let Some(ms) = self.timeout_ms {
1533 opts = opts.with_timeout_ms(ms);
1534 }
1535 if let Some(n) = self.min_responses {
1536 opts = opts.with_min_responses(n);
1537 }
1538 if let Some(gas) = self.max_gas {
1539 opts = opts.with_max_gas(gas);
1540 }
1541 if let Some(enc) = self.encoding_options {
1542 opts = opts.with_encoding_options(enc.into());
1543 }
1544 opts
1545 }
1546 }
1547
1548 impl Into<fynd_core::PriceGuardConfig> for PriceGuardConfig {
1549 fn into(self) -> fynd_core::PriceGuardConfig {
1550 let mut config = fynd_core::PriceGuardConfig::default();
1551 if let Some(bps) = self.lower_tolerance_bps {
1552 config = config.with_lower_tolerance_bps(bps);
1553 }
1554 if let Some(bps) = self.upper_tolerance_bps {
1555 config = config.with_upper_tolerance_bps(bps);
1556 }
1557 if let Some(fail) = self.fail_on_provider_error {
1558 config = config.with_fail_on_provider_error(fail);
1559 }
1560 if let Some(fail) = self.fail_on_token_price_not_found {
1561 config = config.with_fail_on_token_price_not_found(fail);
1562 }
1563 if let Some(enabled) = self.enabled {
1564 config = config.with_enabled(enabled);
1565 }
1566 config
1567 }
1568 }
1569
1570 impl Into<fynd_core::EncodingOptions> for EncodingOptions {
1571 fn into(self) -> fynd_core::EncodingOptions {
1572 let mut opts = fynd_core::EncodingOptions::new(self.slippage)
1573 .with_transfer_type(self.transfer_type.into());
1574 if let (Some(permit), Some(sig)) = (self.permit, self.permit2_signature) {
1575 opts = opts
1576 .with_permit(permit.into())
1577 .with_signature(sig.into());
1578 }
1579 if let Some(fee) = self.client_fee_params {
1580 opts = opts.with_client_fee_params(fee.into());
1581 }
1582 if let Some(pg) = self.price_guard {
1583 opts = opts.with_price_guard(pg.into());
1584 }
1585 opts
1586 }
1587 }
1588
1589 impl Into<fynd_core::ClientFeeParams> for ClientFeeParams {
1590 fn into(self) -> fynd_core::ClientFeeParams {
1591 fynd_core::ClientFeeParams::new(
1592 self.bps,
1593 self.receiver.into(),
1594 self.max_contribution,
1595 self.deadline,
1596 self.signature.into(),
1597 )
1598 }
1599 }
1600
1601 impl Into<fynd_core::UserTransferType> for UserTransferType {
1602 fn into(self) -> fynd_core::UserTransferType {
1603 match self {
1604 UserTransferType::TransferFromPermit2 => {
1605 fynd_core::UserTransferType::TransferFromPermit2
1606 }
1607 UserTransferType::TransferFrom => fynd_core::UserTransferType::TransferFrom,
1608 UserTransferType::UseVaultsFunds => fynd_core::UserTransferType::UseVaultsFunds,
1609 }
1610 }
1611 }
1612
1613 impl Into<fynd_core::PermitSingle> for PermitSingle {
1614 fn into(self) -> fynd_core::PermitSingle {
1615 fynd_core::PermitSingle::new(
1616 self.details.into(),
1617 self.spender.into(),
1618 self.sig_deadline,
1619 )
1620 }
1621 }
1622
1623 impl Into<fynd_core::PermitDetails> for PermitDetails {
1624 fn into(self) -> fynd_core::PermitDetails {
1625 fynd_core::PermitDetails::new(
1626 self.token.into(),
1627 self.amount,
1628 self.expiration,
1629 self.nonce,
1630 )
1631 }
1632 }
1633
1634 impl Into<fynd_core::Order> for Order {
1635 fn into(self) -> fynd_core::Order {
1636 let mut order = fynd_core::Order::new(
1637 self.token_in.into(),
1638 self.token_out.into(),
1639 self.amount,
1640 self.side.into(),
1641 self.sender.into(),
1642 )
1643 .with_id(self.id);
1644 if let Some(r) = self.receiver {
1645 order = order.with_receiver(r.into());
1646 }
1647 order
1648 }
1649 }
1650
1651 impl Into<fynd_core::OrderSide> for OrderSide {
1652 fn into(self) -> fynd_core::OrderSide {
1653 match self {
1654 OrderSide::Sell => fynd_core::OrderSide::Sell,
1655 }
1656 }
1657 }
1658
1659 impl From<fynd_core::Quote> for Quote {
1664 fn from(core: fynd_core::Quote) -> Self {
1665 let solve_time_ms = core.solve_time_ms();
1666 let total_gas_estimate = core.total_gas_estimate().clone();
1667 Self {
1668 orders: core
1669 .into_orders()
1670 .into_iter()
1671 .map(Into::into)
1672 .collect(),
1673 total_gas_estimate,
1674 solve_time_ms,
1675 }
1676 }
1677 }
1678
1679 impl From<fynd_core::OrderQuote> for OrderQuote {
1680 fn from(core: fynd_core::OrderQuote) -> Self {
1681 let order_id = core.order_id().to_string();
1682 let status = core.status().into();
1683 let amount_in = core.amount_in().clone();
1684 let amount_out = core.amount_out().clone();
1685 let gas_estimate = core.gas_estimate().clone();
1686 let price_impact_bps = core.price_impact_bps();
1687 let amount_out_net_gas = core.amount_out_net_gas().clone();
1688 let block = core.block().clone().into();
1689 let gas_price = core.gas_price().cloned();
1690 let transaction = core
1691 .transaction()
1692 .cloned()
1693 .map(Into::into);
1694 let fee_breakdown = core
1695 .fee_breakdown()
1696 .cloned()
1697 .map(Into::into);
1698 let route = core.into_route().map(Into::into);
1699 Self {
1700 order_id,
1701 status,
1702 route,
1703 amount_in,
1704 amount_out,
1705 gas_estimate,
1706 price_impact_bps,
1707 amount_out_net_gas,
1708 block,
1709 gas_price,
1710 transaction,
1711 fee_breakdown,
1712 }
1713 }
1714 }
1715
1716 impl From<fynd_core::QuoteStatus> for QuoteStatus {
1717 fn from(core: fynd_core::QuoteStatus) -> Self {
1718 match core {
1719 fynd_core::QuoteStatus::Success => Self::Success,
1720 fynd_core::QuoteStatus::NoRouteFound => Self::NoRouteFound,
1721 fynd_core::QuoteStatus::InsufficientLiquidity => Self::InsufficientLiquidity,
1722 fynd_core::QuoteStatus::Timeout => Self::Timeout,
1723 fynd_core::QuoteStatus::NotReady => Self::NotReady,
1724 fynd_core::QuoteStatus::PriceCheckFailed => Self::PriceCheckFailed,
1725 _ => Self::NotReady,
1727 }
1728 }
1729 }
1730
1731 impl From<fynd_core::BlockInfo> for BlockInfo {
1732 fn from(core: fynd_core::BlockInfo) -> Self {
1733 Self {
1734 number: core.number(),
1735 hash: core.hash().to_string(),
1736 timestamp: core.timestamp(),
1737 }
1738 }
1739 }
1740
1741 impl From<fynd_core::Route> for Route {
1742 fn from(core: fynd_core::Route) -> Self {
1743 Self {
1744 swaps: core
1745 .into_swaps()
1746 .into_iter()
1747 .map(Into::into)
1748 .collect(),
1749 }
1750 }
1751 }
1752
1753 impl From<fynd_core::Swap> for Swap {
1754 fn from(core: fynd_core::Swap) -> Self {
1755 Self {
1756 component_id: core.component_id().to_string(),
1757 protocol: core.protocol().to_string(),
1758 token_in: core.token_in().clone().into(),
1759 token_out: core.token_out().clone().into(),
1760 amount_in: core.amount_in().clone(),
1761 amount_out: core.amount_out().clone(),
1762 gas_estimate: core.gas_estimate().clone(),
1763 split: *core.split(),
1764 }
1765 }
1766 }
1767
1768 impl From<fynd_core::Transaction> for Transaction {
1769 fn from(core: fynd_core::Transaction) -> Self {
1770 Self {
1771 to: core.to().clone().into(),
1772 value: core.value().clone(),
1773 data: core.data().to_vec(),
1774 }
1775 }
1776 }
1777
1778 impl From<fynd_core::FeeBreakdown> for FeeBreakdown {
1779 fn from(core: fynd_core::FeeBreakdown) -> Self {
1780 Self {
1781 router_fee: core.router_fee().clone(),
1782 client_fee: core.client_fee().clone(),
1783 max_slippage: core.max_slippage().clone(),
1784 min_amount_received: core.min_amount_received().clone(),
1785 }
1786 }
1787 }
1788
1789 #[cfg(test)]
1790 mod tests {
1791 use num_bigint::BigUint;
1792
1793 use super::*;
1794
1795 fn make_address(byte: u8) -> Address {
1796 Address::from([byte; 20])
1797 }
1798
1799 #[test]
1800 fn test_quote_request_roundtrip() {
1801 let dto = QuoteRequest {
1802 orders: vec![Order {
1803 id: "test-id".to_string(),
1804 token_in: make_address(0x01),
1805 token_out: make_address(0x02),
1806 amount: BigUint::from(1000u64),
1807 side: OrderSide::Sell,
1808 sender: make_address(0xAA),
1809 receiver: None,
1810 }],
1811 options: QuoteOptions {
1812 timeout_ms: Some(5000),
1813 min_responses: None,
1814 max_gas: None,
1815 encoding_options: None,
1816 },
1817 };
1818
1819 let core: fynd_core::QuoteRequest = dto.clone().into();
1820 assert_eq!(core.orders().len(), 1);
1821 assert_eq!(core.orders()[0].id(), "test-id");
1822 assert_eq!(core.options().timeout_ms(), Some(5000));
1823 }
1824
1825 #[test]
1826 fn test_quote_from_core() {
1827 let core: fynd_core::Quote = serde_json::from_str(
1828 r#"{"orders":[],"total_gas_estimate":"100000","solve_time_ms":50}"#,
1829 )
1830 .unwrap();
1831
1832 let dto = Quote::from(core);
1833 assert_eq!(dto.total_gas_estimate, BigUint::from(100_000u64));
1834 assert_eq!(dto.solve_time_ms, 50);
1835 }
1836
1837 #[test]
1838 fn test_order_side_into_core() {
1839 let core: fynd_core::OrderSide = OrderSide::Sell.into();
1840 assert_eq!(core, fynd_core::OrderSide::Sell);
1841 }
1842
1843 #[test]
1844 fn test_client_fee_params_into_core() {
1845 let dto = ClientFeeParams::new(
1846 200,
1847 Bytes::from(make_address(0xBB).as_ref()),
1848 BigUint::from(1_000_000u64),
1849 1_893_456_000u64,
1850 Bytes::from(vec![0xABu8; 65]),
1851 );
1852 let core: fynd_core::ClientFeeParams = dto.into();
1853 assert_eq!(core.bps(), 200);
1854 assert_eq!(*core.max_contribution(), BigUint::from(1_000_000u64));
1855 assert_eq!(core.deadline(), 1_893_456_000u64);
1856 assert_eq!(core.signature().len(), 65);
1857 }
1858
1859 #[test]
1860 fn test_encoding_options_with_client_fee_into_core() {
1861 let fee = ClientFeeParams::new(
1862 100,
1863 Bytes::from(make_address(0xCC).as_ref()),
1864 BigUint::from(500u64),
1865 9_999u64,
1866 Bytes::from(vec![0xDEu8; 65]),
1867 );
1868 let dto = EncodingOptions::new(0.005).with_client_fee_params(fee);
1869 let core: fynd_core::EncodingOptions = dto.into();
1870
1871 assert!(core.client_fee_params().is_some());
1872 let core_fee = core.client_fee_params().unwrap();
1873 assert_eq!(core_fee.bps(), 100);
1874 assert_eq!(*core_fee.max_contribution(), BigUint::from(500u64));
1875 }
1876
1877 #[test]
1878 fn test_client_fee_params_serde_roundtrip() {
1879 let fee = ClientFeeParams::new(
1880 150,
1881 Bytes::from(make_address(0xDD).as_ref()),
1882 BigUint::from(999_999u64),
1883 1_700_000_000u64,
1884 Bytes::from(vec![0xFFu8; 65]),
1885 );
1886 let json = serde_json::to_string(&fee).unwrap();
1887 assert!(json.contains(r#""max_contribution":"999999""#));
1888 assert!(json.contains(r#""deadline":1700000000"#));
1889
1890 let deserialized: ClientFeeParams = serde_json::from_str(&json).unwrap();
1891 assert_eq!(deserialized.bps(), 150);
1892 assert_eq!(*deserialized.max_contribution(), BigUint::from(999_999u64));
1893 }
1894
1895 #[test]
1896 fn test_price_guard_config_into_core() {
1897 let dto = PriceGuardConfig::default()
1898 .with_lower_tolerance_bps(200)
1899 .with_upper_tolerance_bps(5000)
1900 .with_fail_on_provider_error(false)
1901 .with_enabled(false);
1902
1903 let config: fynd_core::PriceGuardConfig = dto.into();
1904 assert_eq!(config.lower_tolerance_bps(), 200);
1905 assert_eq!(config.upper_tolerance_bps(), 5000);
1906 assert!(!config.fail_on_provider_error());
1907 assert!(!config.enabled());
1908 }
1909
1910 #[test]
1911 fn test_encoding_options_with_price_guard_roundtrip() {
1912 let enc = EncodingOptions::new(0.01)
1913 .with_price_guard(PriceGuardConfig::default().with_enabled(false));
1914 let dto = QuoteRequest {
1915 orders: vec![Order {
1916 id: "pg-test".to_string(),
1917 token_in: make_address(0x01),
1918 token_out: make_address(0x02),
1919 amount: BigUint::from(1000u64),
1920 side: OrderSide::Sell,
1921 sender: make_address(0xAA),
1922 receiver: None,
1923 }],
1924 options: QuoteOptions::default().with_encoding_options(enc),
1925 };
1926
1927 let core: fynd_core::QuoteRequest = dto.into();
1928 let config = core
1929 .options()
1930 .encoding_options()
1931 .expect("encoding_options should be set")
1932 .price_guard();
1933 assert!(!config.enabled());
1934 }
1935
1936 #[test]
1937 fn test_quote_status_from_core() {
1938 let cases = [
1939 (fynd_core::QuoteStatus::Success, QuoteStatus::Success),
1940 (fynd_core::QuoteStatus::NoRouteFound, QuoteStatus::NoRouteFound),
1941 (fynd_core::QuoteStatus::InsufficientLiquidity, QuoteStatus::InsufficientLiquidity),
1942 (fynd_core::QuoteStatus::Timeout, QuoteStatus::Timeout),
1943 (fynd_core::QuoteStatus::NotReady, QuoteStatus::NotReady),
1944 ];
1945
1946 for (core, expected) in cases {
1947 assert_eq!(QuoteStatus::from(core), expected);
1948 }
1949 }
1950 }
1951}