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 #[serde(default, skip_serializing_if = "Option::is_none")]
173 price_guard: Option<PriceGuardConfig>,
174}
175
176impl QuoteOptions {
177 pub fn with_timeout_ms(mut self, ms: u64) -> Self {
179 self.timeout_ms = Some(ms);
180 self
181 }
182
183 pub fn with_min_responses(mut self, n: usize) -> Self {
185 self.min_responses = Some(n);
186 self
187 }
188
189 pub fn with_max_gas(mut self, gas: BigUint) -> Self {
191 self.max_gas = Some(gas);
192 self
193 }
194
195 pub fn with_encoding_options(mut self, opts: EncodingOptions) -> Self {
197 self.encoding_options = Some(opts);
198 self
199 }
200
201 pub fn timeout_ms(&self) -> Option<u64> {
203 self.timeout_ms
204 }
205
206 pub fn min_responses(&self) -> Option<usize> {
208 self.min_responses
209 }
210
211 pub fn max_gas(&self) -> Option<&BigUint> {
213 self.max_gas.as_ref()
214 }
215
216 pub fn encoding_options(&self) -> Option<&EncodingOptions> {
218 self.encoding_options.as_ref()
219 }
220
221 pub fn with_price_guard(mut self, config: PriceGuardConfig) -> Self {
223 self.price_guard = Some(config);
224 self
225 }
226
227 pub fn price_guard(&self) -> Option<&PriceGuardConfig> {
229 self.price_guard.as_ref()
230 }
231}
232
233#[derive(Debug, Clone, Default, Serialize, Deserialize)]
237#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
238pub struct PriceGuardConfig {
239 #[serde(default, skip_serializing_if = "Option::is_none")]
241 #[cfg_attr(feature = "openapi", schema(example = 300))]
242 lower_tolerance_bps: Option<u32>,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
245 #[cfg_attr(feature = "openapi", schema(example = 10000))]
246 upper_tolerance_bps: Option<u32>,
247 #[serde(default, skip_serializing_if = "Option::is_none")]
249 allow_on_provider_error: Option<bool>,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
252 allow_on_token_price_not_found: Option<bool>,
253 #[serde(default, skip_serializing_if = "Option::is_none")]
255 enabled: Option<bool>,
256}
257
258impl PriceGuardConfig {
259 pub fn with_lower_tolerance_bps(mut self, bps: u32) -> Self {
261 self.lower_tolerance_bps = Some(bps);
262 self
263 }
264
265 pub fn with_upper_tolerance_bps(mut self, bps: u32) -> Self {
267 self.upper_tolerance_bps = Some(bps);
268 self
269 }
270
271 pub fn with_allow_on_provider_error(mut self, allow: bool) -> Self {
273 self.allow_on_provider_error = Some(allow);
274 self
275 }
276
277 pub fn with_allow_on_token_price_not_found(mut self, allow: bool) -> Self {
279 self.allow_on_token_price_not_found = Some(allow);
280 self
281 }
282
283 pub fn with_enabled(mut self, enabled: bool) -> Self {
285 self.enabled = Some(enabled);
286 self
287 }
288
289 pub fn lower_tolerance_bps(&self) -> Option<u32> {
291 self.lower_tolerance_bps
292 }
293
294 pub fn upper_tolerance_bps(&self) -> Option<u32> {
296 self.upper_tolerance_bps
297 }
298
299 pub fn allow_on_provider_error(&self) -> Option<bool> {
301 self.allow_on_provider_error
302 }
303
304 pub fn allow_on_token_price_not_found(&self) -> Option<bool> {
306 self.allow_on_token_price_not_found
307 }
308
309 pub fn enabled(&self) -> Option<bool> {
311 self.enabled
312 }
313}
314
315#[non_exhaustive]
317#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
318#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
319#[serde(rename_all = "snake_case")]
320pub enum UserTransferType {
321 TransferFromPermit2,
323 #[default]
325 TransferFrom,
326 UseVaultsFunds,
328}
329
330#[serde_as]
335#[derive(Debug, Clone, Serialize, Deserialize)]
336#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
337pub struct ClientFeeParams {
338 #[cfg_attr(feature = "openapi", schema(example = 100))]
340 bps: u16,
341 #[cfg_attr(
343 feature = "openapi",
344 schema(value_type = String, example = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
345 )]
346 receiver: Bytes,
347 #[serde_as(as = "DisplayFromStr")]
349 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0"))]
350 max_contribution: BigUint,
351 #[cfg_attr(feature = "openapi", schema(example = 1893456000))]
353 deadline: u64,
354 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0xabcd..."))]
356 signature: Bytes,
357}
358
359impl ClientFeeParams {
360 pub fn new(
362 bps: u16,
363 receiver: Bytes,
364 max_contribution: BigUint,
365 deadline: u64,
366 signature: Bytes,
367 ) -> Self {
368 Self { bps, receiver, max_contribution, deadline, signature }
369 }
370
371 pub fn bps(&self) -> u16 {
373 self.bps
374 }
375
376 pub fn receiver(&self) -> &Bytes {
378 &self.receiver
379 }
380
381 pub fn max_contribution(&self) -> &BigUint {
383 &self.max_contribution
384 }
385
386 pub fn deadline(&self) -> u64 {
388 self.deadline
389 }
390
391 pub fn signature(&self) -> &Bytes {
393 &self.signature
394 }
395}
396
397#[serde_as]
401#[derive(Debug, Clone, Serialize, Deserialize)]
402#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
403pub struct FeeBreakdown {
404 #[serde_as(as = "DisplayFromStr")]
406 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "350000"))]
407 router_fee: BigUint,
408 #[serde_as(as = "DisplayFromStr")]
410 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "2800000"))]
411 client_fee: BigUint,
412 #[serde_as(as = "DisplayFromStr")]
414 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3496850"))]
415 max_slippage: BigUint,
416 #[serde_as(as = "DisplayFromStr")]
419 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3493353150"))]
420 min_amount_received: BigUint,
421}
422
423impl FeeBreakdown {
424 pub fn router_fee(&self) -> &BigUint {
426 &self.router_fee
427 }
428
429 pub fn client_fee(&self) -> &BigUint {
431 &self.client_fee
432 }
433
434 pub fn max_slippage(&self) -> &BigUint {
436 &self.max_slippage
437 }
438
439 pub fn min_amount_received(&self) -> &BigUint {
441 &self.min_amount_received
442 }
443}
444
445#[must_use]
447#[serde_as]
448#[derive(Debug, Clone, Serialize, Deserialize)]
449#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
450pub struct EncodingOptions {
451 #[serde_as(as = "DisplayFromStr")]
452 #[cfg_attr(feature = "openapi", schema(example = "0.001"))]
453 slippage: f64,
454 #[serde(default)]
456 transfer_type: UserTransferType,
457 #[serde(default, skip_serializing_if = "Option::is_none")]
459 permit: Option<PermitSingle>,
460 #[serde(default, skip_serializing_if = "Option::is_none")]
462 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "0xabcd..."))]
463 permit2_signature: Option<Bytes>,
464 #[serde(default, skip_serializing_if = "Option::is_none")]
466 client_fee_params: Option<ClientFeeParams>,
467}
468
469impl EncodingOptions {
470 pub fn new(slippage: f64) -> Self {
472 Self {
473 slippage,
474 transfer_type: UserTransferType::default(),
475 permit: None,
476 permit2_signature: None,
477 client_fee_params: None,
478 }
479 }
480
481 pub fn with_transfer_type(mut self, t: UserTransferType) -> Self {
483 self.transfer_type = t;
484 self
485 }
486
487 pub fn with_permit2(mut self, permit: PermitSingle, sig: Bytes) -> Self {
489 self.permit = Some(permit);
490 self.permit2_signature = Some(sig);
491 self
492 }
493
494 pub fn slippage(&self) -> f64 {
496 self.slippage
497 }
498
499 pub fn transfer_type(&self) -> &UserTransferType {
501 &self.transfer_type
502 }
503
504 pub fn permit(&self) -> Option<&PermitSingle> {
506 self.permit.as_ref()
507 }
508
509 pub fn permit2_signature(&self) -> Option<&Bytes> {
511 self.permit2_signature.as_ref()
512 }
513
514 pub fn with_client_fee_params(mut self, params: ClientFeeParams) -> Self {
516 self.client_fee_params = Some(params);
517 self
518 }
519
520 pub fn client_fee_params(&self) -> Option<&ClientFeeParams> {
522 self.client_fee_params.as_ref()
523 }
524}
525
526#[serde_as]
528#[derive(Debug, Clone, Serialize, Deserialize)]
529#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
530pub struct PermitSingle {
531 details: PermitDetails,
533 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"))]
535 spender: Bytes,
536 #[serde_as(as = "DisplayFromStr")]
538 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "1893456000"))]
539 sig_deadline: BigUint,
540}
541
542impl PermitSingle {
543 pub fn new(details: PermitDetails, spender: Bytes, sig_deadline: BigUint) -> Self {
545 Self { details, spender, sig_deadline }
546 }
547
548 pub fn details(&self) -> &PermitDetails {
550 &self.details
551 }
552
553 pub fn spender(&self) -> &Bytes {
555 &self.spender
556 }
557
558 pub fn sig_deadline(&self) -> &BigUint {
560 &self.sig_deadline
561 }
562}
563
564#[serde_as]
566#[derive(Debug, Clone, Serialize, Deserialize)]
567#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
568pub struct PermitDetails {
569 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"))]
571 token: Bytes,
572 #[serde_as(as = "DisplayFromStr")]
574 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "1000000000000000000"))]
575 amount: BigUint,
576 #[serde_as(as = "DisplayFromStr")]
578 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "1893456000"))]
579 expiration: BigUint,
580 #[serde_as(as = "DisplayFromStr")]
582 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0"))]
583 nonce: BigUint,
584}
585
586impl PermitDetails {
587 pub fn new(token: Bytes, amount: BigUint, expiration: BigUint, nonce: BigUint) -> Self {
589 Self { token, amount, expiration, nonce }
590 }
591
592 pub fn token(&self) -> &Bytes {
594 &self.token
595 }
596
597 pub fn amount(&self) -> &BigUint {
599 &self.amount
600 }
601
602 pub fn expiration(&self) -> &BigUint {
604 &self.expiration
605 }
606
607 pub fn nonce(&self) -> &BigUint {
609 &self.nonce
610 }
611}
612
613#[must_use]
622#[serde_as]
623#[derive(Debug, Clone, Serialize, Deserialize)]
624#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
625pub struct Quote {
626 orders: Vec<OrderQuote>,
628 #[serde_as(as = "DisplayFromStr")]
630 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "150000"))]
631 total_gas_estimate: BigUint,
632 #[cfg_attr(feature = "openapi", schema(example = 12))]
634 solve_time_ms: u64,
635}
636
637impl Quote {
638 pub fn new(orders: Vec<OrderQuote>, total_gas_estimate: BigUint, solve_time_ms: u64) -> Self {
640 Self { orders, total_gas_estimate, solve_time_ms }
641 }
642
643 pub fn orders(&self) -> &[OrderQuote] {
645 &self.orders
646 }
647
648 pub fn into_orders(self) -> Vec<OrderQuote> {
650 self.orders
651 }
652
653 pub fn total_gas_estimate(&self) -> &BigUint {
655 &self.total_gas_estimate
656 }
657
658 pub fn solve_time_ms(&self) -> u64 {
660 self.solve_time_ms
661 }
662}
663
664#[must_use]
668#[serde_as]
669#[derive(Debug, Clone, Serialize, Deserialize)]
670#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
671pub struct Order {
672 #[serde(default = "generate_order_id", skip_deserializing)]
676 id: String,
677 #[cfg_attr(
679 feature = "openapi",
680 schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
681 )]
682 token_in: Address,
683 #[cfg_attr(
685 feature = "openapi",
686 schema(value_type = String, example = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
687 )]
688 token_out: Address,
689 #[serde_as(as = "DisplayFromStr")]
691 #[cfg_attr(
692 feature = "openapi",
693 schema(value_type = String, example = "1000000000000000000")
694 )]
695 amount: BigUint,
696 side: OrderSide,
698 #[cfg_attr(
700 feature = "openapi",
701 schema(value_type = String, example = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
702 )]
703 sender: Address,
704 #[serde(default, skip_serializing_if = "Option::is_none")]
708 #[cfg_attr(
709 feature = "openapi",
710 schema(value_type = Option<String>, example = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
711 )]
712 receiver: Option<Address>,
713}
714
715impl Order {
716 pub fn new(
718 token_in: Address,
719 token_out: Address,
720 amount: BigUint,
721 side: OrderSide,
722 sender: Address,
723 ) -> Self {
724 Self { id: String::new(), token_in, token_out, amount, side, sender, receiver: None }
725 }
726
727 pub fn with_id(mut self, id: impl Into<String>) -> Self {
729 self.id = id.into();
730 self
731 }
732
733 pub fn with_receiver(mut self, receiver: Address) -> Self {
735 self.receiver = Some(receiver);
736 self
737 }
738
739 pub fn id(&self) -> &str {
741 &self.id
742 }
743
744 pub fn token_in(&self) -> &Address {
746 &self.token_in
747 }
748
749 pub fn token_out(&self) -> &Address {
751 &self.token_out
752 }
753
754 pub fn amount(&self) -> &BigUint {
756 &self.amount
757 }
758
759 pub fn side(&self) -> OrderSide {
761 self.side
762 }
763
764 pub fn sender(&self) -> &Address {
766 &self.sender
767 }
768
769 pub fn receiver(&self) -> Option<&Address> {
771 self.receiver.as_ref()
772 }
773}
774
775#[non_exhaustive]
779#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
780#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
781#[serde(rename_all = "snake_case")]
782pub enum OrderSide {
783 Sell,
785}
786
787#[must_use]
792#[serde_as]
793#[derive(Debug, Clone, Serialize, Deserialize)]
794#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
795pub struct OrderQuote {
796 #[cfg_attr(feature = "openapi", schema(example = "f47ac10b-58cc-4372-a567-0e02b2c3d479"))]
798 order_id: String,
799 status: QuoteStatus,
801 #[serde(skip_serializing_if = "Option::is_none")]
803 route: Option<Route>,
804 #[serde_as(as = "DisplayFromStr")]
806 #[cfg_attr(
807 feature = "openapi",
808 schema(value_type = String, example = "1000000000000000000")
809 )]
810 amount_in: BigUint,
811 #[serde_as(as = "DisplayFromStr")]
813 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3500000000"))]
814 amount_out: BigUint,
815 #[serde_as(as = "DisplayFromStr")]
817 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "150000"))]
818 gas_estimate: BigUint,
819 #[serde(skip_serializing_if = "Option::is_none")]
821 price_impact_bps: Option<i32>,
822 #[serde_as(as = "DisplayFromStr")]
825 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3498000000"))]
826 amount_out_net_gas: BigUint,
827 block: BlockInfo,
829 #[serde_as(as = "Option<DisplayFromStr>")]
831 #[serde(skip_serializing_if = "Option::is_none")]
832 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "20000000000"))]
833 gas_price: Option<BigUint>,
834 transaction: Option<Transaction>,
836 #[serde(skip_serializing_if = "Option::is_none")]
838 fee_breakdown: Option<FeeBreakdown>,
839}
840
841impl OrderQuote {
842 pub fn order_id(&self) -> &str {
844 &self.order_id
845 }
846
847 pub fn status(&self) -> QuoteStatus {
849 self.status
850 }
851
852 pub fn route(&self) -> Option<&Route> {
854 self.route.as_ref()
855 }
856
857 pub fn amount_in(&self) -> &BigUint {
859 &self.amount_in
860 }
861
862 pub fn amount_out(&self) -> &BigUint {
864 &self.amount_out
865 }
866
867 pub fn gas_estimate(&self) -> &BigUint {
869 &self.gas_estimate
870 }
871
872 pub fn price_impact_bps(&self) -> Option<i32> {
874 self.price_impact_bps
875 }
876
877 pub fn amount_out_net_gas(&self) -> &BigUint {
879 &self.amount_out_net_gas
880 }
881
882 pub fn block(&self) -> &BlockInfo {
884 &self.block
885 }
886
887 pub fn gas_price(&self) -> Option<&BigUint> {
889 self.gas_price.as_ref()
890 }
891
892 pub fn transaction(&self) -> Option<&Transaction> {
894 self.transaction.as_ref()
895 }
896
897 pub fn fee_breakdown(&self) -> Option<&FeeBreakdown> {
899 self.fee_breakdown.as_ref()
900 }
901}
902
903#[non_exhaustive]
905#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
906#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
907#[serde(rename_all = "snake_case")]
908pub enum QuoteStatus {
909 Success,
911 NoRouteFound,
913 InsufficientLiquidity,
915 Timeout,
917 NotReady,
919 PriceCheckFailed,
921}
922
923#[derive(Debug, Clone, Serialize, Deserialize)]
928#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
929pub struct BlockInfo {
930 #[cfg_attr(feature = "openapi", schema(example = 21000000))]
932 number: u64,
933 #[cfg_attr(
935 feature = "openapi",
936 schema(example = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd")
937 )]
938 hash: String,
939 #[cfg_attr(feature = "openapi", schema(example = 1730000000))]
941 timestamp: u64,
942}
943
944impl BlockInfo {
945 pub fn new(number: u64, hash: String, timestamp: u64) -> Self {
947 Self { number, hash, timestamp }
948 }
949
950 pub fn number(&self) -> u64 {
952 self.number
953 }
954
955 pub fn hash(&self) -> &str {
957 &self.hash
958 }
959
960 pub fn timestamp(&self) -> u64 {
962 self.timestamp
963 }
964}
965
966#[must_use]
975#[derive(Debug, Clone, Serialize, Deserialize)]
976#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
977pub struct Route {
978 swaps: Vec<Swap>,
980}
981
982impl Route {
983 pub fn new(swaps: Vec<Swap>) -> Self {
985 Self { swaps }
986 }
987
988 pub fn swaps(&self) -> &[Swap] {
990 &self.swaps
991 }
992
993 pub fn into_swaps(self) -> Vec<Swap> {
995 self.swaps
996 }
997}
998
999#[serde_as]
1003#[derive(Debug, Clone, Serialize, Deserialize)]
1004#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1005pub struct Swap {
1006 #[cfg_attr(
1008 feature = "openapi",
1009 schema(example = "0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc")
1010 )]
1011 component_id: String,
1012 #[cfg_attr(feature = "openapi", schema(example = "uniswap_v2"))]
1014 protocol: String,
1015 #[cfg_attr(
1017 feature = "openapi",
1018 schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
1019 )]
1020 token_in: Address,
1021 #[cfg_attr(
1023 feature = "openapi",
1024 schema(value_type = String, example = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
1025 )]
1026 token_out: Address,
1027 #[serde_as(as = "DisplayFromStr")]
1029 #[cfg_attr(
1030 feature = "openapi",
1031 schema(value_type = String, example = "1000000000000000000")
1032 )]
1033 amount_in: BigUint,
1034 #[serde_as(as = "DisplayFromStr")]
1036 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3500000000"))]
1037 amount_out: BigUint,
1038 #[serde_as(as = "DisplayFromStr")]
1040 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "150000"))]
1041 gas_estimate: BigUint,
1042 #[serde_as(as = "DisplayFromStr")]
1044 #[cfg_attr(feature = "openapi", schema(example = "0.0"))]
1045 split: f64,
1046}
1047
1048impl Swap {
1049 #[allow(clippy::too_many_arguments)]
1051 pub fn new(
1052 component_id: String,
1053 protocol: String,
1054 token_in: Address,
1055 token_out: Address,
1056 amount_in: BigUint,
1057 amount_out: BigUint,
1058 gas_estimate: BigUint,
1059 split: f64,
1060 ) -> Self {
1061 Self {
1062 component_id,
1063 protocol,
1064 token_in,
1065 token_out,
1066 amount_in,
1067 amount_out,
1068 gas_estimate,
1069 split,
1070 }
1071 }
1072
1073 pub fn component_id(&self) -> &str {
1075 &self.component_id
1076 }
1077
1078 pub fn protocol(&self) -> &str {
1080 &self.protocol
1081 }
1082
1083 pub fn token_in(&self) -> &Address {
1085 &self.token_in
1086 }
1087
1088 pub fn token_out(&self) -> &Address {
1090 &self.token_out
1091 }
1092
1093 pub fn amount_in(&self) -> &BigUint {
1095 &self.amount_in
1096 }
1097
1098 pub fn amount_out(&self) -> &BigUint {
1100 &self.amount_out
1101 }
1102
1103 pub fn gas_estimate(&self) -> &BigUint {
1105 &self.gas_estimate
1106 }
1107
1108 pub fn split(&self) -> f64 {
1110 self.split
1111 }
1112}
1113
1114#[derive(Debug, Clone, Serialize, Deserialize)]
1120#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1121pub struct HealthStatus {
1122 #[cfg_attr(feature = "openapi", schema(example = true))]
1124 healthy: bool,
1125 #[cfg_attr(feature = "openapi", schema(example = 1250))]
1127 last_update_ms: u64,
1128 #[cfg_attr(feature = "openapi", schema(example = 2))]
1130 num_solver_pools: usize,
1131 #[serde(default)]
1137 #[cfg_attr(feature = "openapi", schema(example = true))]
1138 derived_data_ready: bool,
1139 #[serde(default, skip_serializing_if = "Option::is_none")]
1141 #[cfg_attr(feature = "openapi", schema(example = 12000))]
1142 gas_price_age_ms: Option<u64>,
1143}
1144
1145impl HealthStatus {
1146 pub fn new(
1148 healthy: bool,
1149 last_update_ms: u64,
1150 num_solver_pools: usize,
1151 derived_data_ready: bool,
1152 gas_price_age_ms: Option<u64>,
1153 ) -> Self {
1154 Self { healthy, last_update_ms, num_solver_pools, derived_data_ready, gas_price_age_ms }
1155 }
1156
1157 pub fn healthy(&self) -> bool {
1159 self.healthy
1160 }
1161
1162 pub fn last_update_ms(&self) -> u64 {
1164 self.last_update_ms
1165 }
1166
1167 pub fn num_solver_pools(&self) -> usize {
1169 self.num_solver_pools
1170 }
1171
1172 pub fn derived_data_ready(&self) -> bool {
1174 self.derived_data_ready
1175 }
1176
1177 pub fn gas_price_age_ms(&self) -> Option<u64> {
1179 self.gas_price_age_ms
1180 }
1181}
1182
1183#[derive(Debug, Clone, Serialize, Deserialize)]
1185#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1186pub struct InstanceInfo {
1187 #[cfg_attr(feature = "openapi", schema(example = 1))]
1189 chain_id: u64,
1190 #[cfg_attr(
1192 feature = "openapi",
1193 schema(value_type = String, example = "0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35")
1194 )]
1195 router_address: Bytes,
1196 #[cfg_attr(
1198 feature = "openapi",
1199 schema(value_type = String, example = "0x000000000022D473030F116dDEE9F6B43aC78BA3")
1200 )]
1201 permit2_address: Bytes,
1202}
1203
1204impl InstanceInfo {
1205 pub fn new(chain_id: u64, router_address: Bytes, permit2_address: Bytes) -> Self {
1207 Self { chain_id, router_address, permit2_address }
1208 }
1209
1210 pub fn chain_id(&self) -> u64 {
1212 self.chain_id
1213 }
1214
1215 pub fn router_address(&self) -> &Bytes {
1217 &self.router_address
1218 }
1219
1220 pub fn permit2_address(&self) -> &Bytes {
1222 &self.permit2_address
1223 }
1224}
1225
1226#[must_use]
1228#[derive(Debug, Serialize, Deserialize)]
1229#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1230pub struct ErrorResponse {
1231 #[cfg_attr(feature = "openapi", schema(example = "bad request: no orders provided"))]
1232 error: String,
1233 #[cfg_attr(feature = "openapi", schema(example = "BAD_REQUEST"))]
1234 code: String,
1235 #[serde(skip_serializing_if = "Option::is_none")]
1236 details: Option<serde_json::Value>,
1237}
1238
1239impl ErrorResponse {
1240 pub fn new(error: String, code: String) -> Self {
1242 Self { error, code, details: None }
1243 }
1244
1245 pub fn with_details(mut self, details: serde_json::Value) -> Self {
1247 self.details = Some(details);
1248 self
1249 }
1250
1251 pub fn error(&self) -> &str {
1253 &self.error
1254 }
1255
1256 pub fn code(&self) -> &str {
1258 &self.code
1259 }
1260
1261 pub fn details(&self) -> Option<&serde_json::Value> {
1263 self.details.as_ref()
1264 }
1265}
1266
1267#[serde_as]
1273#[derive(Debug, Clone, Serialize, Deserialize)]
1274#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1275pub struct Transaction {
1276 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"))]
1278 to: Bytes,
1279 #[serde_as(as = "DisplayFromStr")]
1281 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0"))]
1282 value: BigUint,
1283 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0x1234567890abcdef"))]
1285 #[serde(serialize_with = "serialize_bytes_hex", deserialize_with = "deserialize_bytes_hex")]
1286 data: Vec<u8>,
1287}
1288
1289impl Transaction {
1290 pub fn new(to: Bytes, value: BigUint, data: Vec<u8>) -> Self {
1292 Self { to, value, data }
1293 }
1294
1295 pub fn to(&self) -> &Bytes {
1297 &self.to
1298 }
1299
1300 pub fn value(&self) -> &BigUint {
1302 &self.value
1303 }
1304
1305 pub fn data(&self) -> &[u8] {
1307 &self.data
1308 }
1309}
1310
1311fn serialize_bytes_hex<S>(bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
1317where
1318 S: serde::Serializer,
1319{
1320 serializer.serialize_str(&format!("0x{}", hex::encode(bytes)))
1321}
1322
1323fn deserialize_bytes_hex<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
1325where
1326 D: serde::Deserializer<'de>,
1327{
1328 let s = String::deserialize(deserializer)?;
1329 let s = s.strip_prefix("0x").unwrap_or(&s);
1330 hex::decode(s).map_err(serde::de::Error::custom)
1331}
1332
1333fn generate_order_id() -> String {
1339 Uuid::new_v4().to_string()
1340}
1341
1342#[cfg(test)]
1351mod wire_format_tests {
1352 use num_bigint::BigUint;
1353
1354 use super::*;
1355
1356 #[test]
1363 fn bytes_deserializes_without_0x_prefix() {
1364 let b: Bytes = serde_json::from_str(r#""deadbeef""#).unwrap();
1365 assert_eq!(b.as_ref(), [0xDE, 0xAD, 0xBE, 0xEF]);
1366 }
1367
1368 #[test]
1375 fn order_serializes_to_full_json() {
1376 let order = Order::new(
1377 Bytes::from([0xAAu8; 20]),
1378 Bytes::from([0xBBu8; 20]),
1379 BigUint::from(1_000_000_000_000_000_000u64),
1380 OrderSide::Sell,
1381 Bytes::from([0xCCu8; 20]),
1382 )
1383 .with_id("abc");
1384
1385 assert_eq!(
1386 serde_json::to_value(&order).unwrap(),
1387 serde_json::json!({
1388 "id": "abc",
1389 "token_in": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1390 "token_out": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1391 "amount": "1000000000000000000",
1392 "side": "sell",
1393 "sender": "0xcccccccccccccccccccccccccccccccccccccccc"
1394 })
1395 );
1396 }
1397
1398 #[test]
1405 fn order_quote_deserializes_from_json() {
1406 let json = r#"{
1407 "order_id": "order-1",
1408 "status": "success",
1409 "amount_in": "1000000000000000000",
1410 "amount_out": "2000000000",
1411 "gas_estimate": "150000",
1412 "amount_out_net_gas": "1999000000",
1413 "price_impact_bps": 5,
1414 "block": { "number": 21000000, "hash": "0xdeadbeef", "timestamp": 1700000000 },
1415 "route": { "swaps": [{
1416 "component_id": "pool-1",
1417 "protocol": "uniswap_v3",
1418 "token_in": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1419 "token_out": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1420 "amount_in": "1000000000000000000",
1421 "amount_out": "2000000000",
1422 "gas_estimate": "150000",
1423 "split": "0"
1424 }]}
1425 }"#;
1426
1427 let quote: OrderQuote = serde_json::from_str(json).unwrap();
1428
1429 assert_eq!(quote.status(), QuoteStatus::Success);
1430 assert_eq!(*quote.amount_in(), BigUint::from(1_000_000_000_000_000_000u64));
1431 assert_eq!(quote.price_impact_bps(), Some(5));
1432 assert_eq!(quote.block().number(), 21_000_000);
1433
1434 let swap = "e.route().unwrap().swaps()[0];
1435 assert_eq!(swap.token_in().as_ref(), [0xAAu8; 20]);
1436 assert_eq!(swap.token_out().as_ref(), [0xBBu8; 20]);
1437 assert_eq!(swap.split(), 0.0);
1438 }
1439
1440 #[test]
1447 fn encoding_options_serializes_to_full_json() {
1448 assert_eq!(
1449 serde_json::to_value(EncodingOptions::new(0.005)).unwrap(),
1450 serde_json::json!({
1451 "slippage": "0.005",
1452 "transfer_type": "transfer_from"
1453 })
1454 );
1455 }
1456
1457 #[test]
1464 fn instance_info_deserializes_and_ignores_unknown_fields() {
1465 let json = r#"{
1466 "chain_id": 1,
1467 "router_address": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1468 "permit2_address": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1469 "future_field": "ignored"
1470 }"#;
1471
1472 let info: InstanceInfo = serde_json::from_str(json).unwrap();
1473 assert_eq!(info.chain_id(), 1);
1474 assert_eq!(info.router_address().as_ref(), [0xAAu8; 20]);
1475 assert_eq!(info.permit2_address().as_ref(), [0xBBu8; 20]);
1476 }
1477}
1478
1479#[cfg(feature = "core")]
1490mod conversions {
1491 use tycho_simulation::tycho_core::Bytes as TychoBytes;
1492
1493 use super::*;
1494
1495 impl From<TychoBytes> for Bytes {
1501 fn from(b: TychoBytes) -> Self {
1502 Self(b.0)
1503 }
1504 }
1505
1506 impl From<Bytes> for TychoBytes {
1507 fn from(b: Bytes) -> Self {
1508 Self(b.0)
1509 }
1510 }
1511
1512 impl Into<fynd_core::QuoteRequest> for QuoteRequest {
1517 fn into(self) -> fynd_core::QuoteRequest {
1518 fynd_core::QuoteRequest::new(
1519 self.orders
1520 .into_iter()
1521 .map(Into::into)
1522 .collect(),
1523 self.options.into(),
1524 )
1525 }
1526 }
1527
1528 impl Into<fynd_core::QuoteOptions> for QuoteOptions {
1529 fn into(self) -> fynd_core::QuoteOptions {
1530 let mut opts = fynd_core::QuoteOptions::default();
1531 if let Some(ms) = self.timeout_ms {
1532 opts = opts.with_timeout_ms(ms);
1533 }
1534 if let Some(n) = self.min_responses {
1535 opts = opts.with_min_responses(n);
1536 }
1537 if let Some(gas) = self.max_gas {
1538 opts = opts.with_max_gas(gas);
1539 }
1540 if let Some(enc) = self.encoding_options {
1541 opts = opts.with_encoding_options(enc.into());
1542 }
1543 if let Some(pg) = self.price_guard {
1544 opts = opts.with_price_guard(pg.into());
1545 }
1546 opts
1547 }
1548 }
1549
1550 impl Into<fynd_core::PriceGuardConfig> for PriceGuardConfig {
1551 fn into(self) -> fynd_core::PriceGuardConfig {
1552 let mut config = fynd_core::PriceGuardConfig::default();
1553 if let Some(bps) = self.lower_tolerance_bps {
1554 config = config.with_lower_tolerance_bps(bps);
1555 }
1556 if let Some(bps) = self.upper_tolerance_bps {
1557 config = config.with_upper_tolerance_bps(bps);
1558 }
1559 if let Some(allow) = self.allow_on_provider_error {
1560 config = config.with_allow_on_provider_error(allow);
1561 }
1562 if let Some(allow) = self.allow_on_token_price_not_found {
1563 config = config.with_allow_on_token_price_not_found(allow);
1564 }
1565 if let Some(enabled) = self.enabled {
1566 config = config.with_enabled(enabled);
1567 }
1568 config
1569 }
1570 }
1571
1572 impl Into<fynd_core::EncodingOptions> for EncodingOptions {
1573 fn into(self) -> fynd_core::EncodingOptions {
1574 let mut opts = fynd_core::EncodingOptions::new(self.slippage)
1575 .with_transfer_type(self.transfer_type.into());
1576 if let (Some(permit), Some(sig)) = (self.permit, self.permit2_signature) {
1577 opts = opts
1578 .with_permit(permit.into())
1579 .with_signature(sig.into());
1580 }
1581 if let Some(fee) = self.client_fee_params {
1582 opts = opts.with_client_fee_params(fee.into());
1583 }
1584 opts
1585 }
1586 }
1587
1588 impl Into<fynd_core::ClientFeeParams> for ClientFeeParams {
1589 fn into(self) -> fynd_core::ClientFeeParams {
1590 fynd_core::ClientFeeParams::new(
1591 self.bps,
1592 self.receiver.into(),
1593 self.max_contribution,
1594 self.deadline,
1595 self.signature.into(),
1596 )
1597 }
1598 }
1599
1600 impl Into<fynd_core::UserTransferType> for UserTransferType {
1601 fn into(self) -> fynd_core::UserTransferType {
1602 match self {
1603 UserTransferType::TransferFromPermit2 => {
1604 fynd_core::UserTransferType::TransferFromPermit2
1605 }
1606 UserTransferType::TransferFrom => fynd_core::UserTransferType::TransferFrom,
1607 UserTransferType::UseVaultsFunds => fynd_core::UserTransferType::UseVaultsFunds,
1608 }
1609 }
1610 }
1611
1612 impl Into<fynd_core::PermitSingle> for PermitSingle {
1613 fn into(self) -> fynd_core::PermitSingle {
1614 fynd_core::PermitSingle::new(
1615 self.details.into(),
1616 self.spender.into(),
1617 self.sig_deadline,
1618 )
1619 }
1620 }
1621
1622 impl Into<fynd_core::PermitDetails> for PermitDetails {
1623 fn into(self) -> fynd_core::PermitDetails {
1624 fynd_core::PermitDetails::new(
1625 self.token.into(),
1626 self.amount,
1627 self.expiration,
1628 self.nonce,
1629 )
1630 }
1631 }
1632
1633 impl Into<fynd_core::Order> for Order {
1634 fn into(self) -> fynd_core::Order {
1635 let mut order = fynd_core::Order::new(
1636 self.token_in.into(),
1637 self.token_out.into(),
1638 self.amount,
1639 self.side.into(),
1640 self.sender.into(),
1641 )
1642 .with_id(self.id);
1643 if let Some(r) = self.receiver {
1644 order = order.with_receiver(r.into());
1645 }
1646 order
1647 }
1648 }
1649
1650 impl Into<fynd_core::OrderSide> for OrderSide {
1651 fn into(self) -> fynd_core::OrderSide {
1652 match self {
1653 OrderSide::Sell => fynd_core::OrderSide::Sell,
1654 }
1655 }
1656 }
1657
1658 impl From<fynd_core::Quote> for Quote {
1663 fn from(core: fynd_core::Quote) -> Self {
1664 let solve_time_ms = core.solve_time_ms();
1665 let total_gas_estimate = core.total_gas_estimate().clone();
1666 Self {
1667 orders: core
1668 .into_orders()
1669 .into_iter()
1670 .map(Into::into)
1671 .collect(),
1672 total_gas_estimate,
1673 solve_time_ms,
1674 }
1675 }
1676 }
1677
1678 impl From<fynd_core::OrderQuote> for OrderQuote {
1679 fn from(core: fynd_core::OrderQuote) -> Self {
1680 let order_id = core.order_id().to_string();
1681 let status = core.status().into();
1682 let amount_in = core.amount_in().clone();
1683 let amount_out = core.amount_out().clone();
1684 let gas_estimate = core.gas_estimate().clone();
1685 let price_impact_bps = core.price_impact_bps();
1686 let amount_out_net_gas = core.amount_out_net_gas().clone();
1687 let block = core.block().clone().into();
1688 let gas_price = core.gas_price().cloned();
1689 let transaction = core
1690 .transaction()
1691 .cloned()
1692 .map(Into::into);
1693 let fee_breakdown = core
1694 .fee_breakdown()
1695 .cloned()
1696 .map(Into::into);
1697 let route = core.into_route().map(Into::into);
1698 Self {
1699 order_id,
1700 status,
1701 route,
1702 amount_in,
1703 amount_out,
1704 gas_estimate,
1705 price_impact_bps,
1706 amount_out_net_gas,
1707 block,
1708 gas_price,
1709 transaction,
1710 fee_breakdown,
1711 }
1712 }
1713 }
1714
1715 impl From<fynd_core::QuoteStatus> for QuoteStatus {
1716 fn from(core: fynd_core::QuoteStatus) -> Self {
1717 match core {
1718 fynd_core::QuoteStatus::Success => Self::Success,
1719 fynd_core::QuoteStatus::NoRouteFound => Self::NoRouteFound,
1720 fynd_core::QuoteStatus::InsufficientLiquidity => Self::InsufficientLiquidity,
1721 fynd_core::QuoteStatus::Timeout => Self::Timeout,
1722 fynd_core::QuoteStatus::NotReady => Self::NotReady,
1723 fynd_core::QuoteStatus::PriceCheckFailed => Self::PriceCheckFailed,
1724 _ => Self::NotReady,
1726 }
1727 }
1728 }
1729
1730 impl From<fynd_core::BlockInfo> for BlockInfo {
1731 fn from(core: fynd_core::BlockInfo) -> Self {
1732 Self {
1733 number: core.number(),
1734 hash: core.hash().to_string(),
1735 timestamp: core.timestamp(),
1736 }
1737 }
1738 }
1739
1740 impl From<fynd_core::Route> for Route {
1741 fn from(core: fynd_core::Route) -> Self {
1742 Self {
1743 swaps: core
1744 .into_swaps()
1745 .into_iter()
1746 .map(Into::into)
1747 .collect(),
1748 }
1749 }
1750 }
1751
1752 impl From<fynd_core::Swap> for Swap {
1753 fn from(core: fynd_core::Swap) -> Self {
1754 Self {
1755 component_id: core.component_id().to_string(),
1756 protocol: core.protocol().to_string(),
1757 token_in: core.token_in().clone().into(),
1758 token_out: core.token_out().clone().into(),
1759 amount_in: core.amount_in().clone(),
1760 amount_out: core.amount_out().clone(),
1761 gas_estimate: core.gas_estimate().clone(),
1762 split: *core.split(),
1763 }
1764 }
1765 }
1766
1767 impl From<fynd_core::Transaction> for Transaction {
1768 fn from(core: fynd_core::Transaction) -> Self {
1769 Self {
1770 to: core.to().clone().into(),
1771 value: core.value().clone(),
1772 data: core.data().to_vec(),
1773 }
1774 }
1775 }
1776
1777 impl From<fynd_core::FeeBreakdown> for FeeBreakdown {
1778 fn from(core: fynd_core::FeeBreakdown) -> Self {
1779 Self {
1780 router_fee: core.router_fee().clone(),
1781 client_fee: core.client_fee().clone(),
1782 max_slippage: core.max_slippage().clone(),
1783 min_amount_received: core.min_amount_received().clone(),
1784 }
1785 }
1786 }
1787
1788 #[cfg(test)]
1789 mod tests {
1790 use num_bigint::BigUint;
1791
1792 use super::*;
1793
1794 fn make_address(byte: u8) -> Address {
1795 Address::from([byte; 20])
1796 }
1797
1798 #[test]
1799 fn test_quote_request_roundtrip() {
1800 let dto = QuoteRequest {
1801 orders: vec![Order {
1802 id: "test-id".to_string(),
1803 token_in: make_address(0x01),
1804 token_out: make_address(0x02),
1805 amount: BigUint::from(1000u64),
1806 side: OrderSide::Sell,
1807 sender: make_address(0xAA),
1808 receiver: None,
1809 }],
1810 options: QuoteOptions {
1811 timeout_ms: Some(5000),
1812 min_responses: None,
1813 max_gas: None,
1814 encoding_options: None,
1815 price_guard: 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_allow_on_provider_error(true)
1901 .with_enabled(false);
1902
1903 let core: fynd_core::PriceGuardConfig = dto.into();
1904 assert_eq!(core.lower_tolerance_bps(), 200);
1905 assert_eq!(core.upper_tolerance_bps(), 5000);
1906 assert!(core.allow_on_provider_error());
1907 assert!(!core.enabled());
1908 }
1909
1910 #[test]
1911 fn test_price_guard_config_defaults_preserved() {
1912 let dto = PriceGuardConfig::default().with_lower_tolerance_bps(100);
1913 let core: fynd_core::PriceGuardConfig = dto.into();
1914
1915 assert_eq!(core.lower_tolerance_bps(), 100);
1916 assert_eq!(core.upper_tolerance_bps(), 10_000);
1918 assert!(!core.allow_on_provider_error());
1919 assert!(!core.enabled());
1920 }
1921
1922 #[test]
1923 fn test_quote_options_with_price_guard_roundtrip() {
1924 let dto = QuoteRequest {
1925 orders: vec![Order {
1926 id: "pg-test".to_string(),
1927 token_in: make_address(0x01),
1928 token_out: make_address(0x02),
1929 amount: BigUint::from(1000u64),
1930 side: OrderSide::Sell,
1931 sender: make_address(0xAA),
1932 receiver: None,
1933 }],
1934 options: QuoteOptions::default()
1935 .with_price_guard(PriceGuardConfig::default().with_enabled(false)),
1936 };
1937
1938 let core: fynd_core::QuoteRequest = dto.into();
1939 let pg = core
1940 .options()
1941 .price_guard()
1942 .expect("price_guard should be set");
1943 assert!(!pg.enabled());
1944 }
1945
1946 #[test]
1947 fn test_quote_status_from_core() {
1948 let cases = [
1949 (fynd_core::QuoteStatus::Success, QuoteStatus::Success),
1950 (fynd_core::QuoteStatus::NoRouteFound, QuoteStatus::NoRouteFound),
1951 (fynd_core::QuoteStatus::InsufficientLiquidity, QuoteStatus::InsufficientLiquidity),
1952 (fynd_core::QuoteStatus::Timeout, QuoteStatus::Timeout),
1953 (fynd_core::QuoteStatus::NotReady, QuoteStatus::NotReady),
1954 ];
1955
1956 for (core, expected) in cases {
1957 assert_eq!(QuoteStatus::from(core), expected);
1958 }
1959 }
1960 }
1961}