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")]
413 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = json!(null)))]
414 swaps_hash: Option<Bytes>,
415}
416
417impl FeeBreakdown {
418 pub fn router_fee(&self) -> &BigUint {
420 &self.router_fee
421 }
422
423 pub fn client_fee(&self) -> &BigUint {
425 &self.client_fee
426 }
427
428 pub fn max_slippage(&self) -> &BigUint {
430 &self.max_slippage
431 }
432
433 pub fn min_amount_received(&self) -> &BigUint {
435 &self.min_amount_received
436 }
437
438 pub fn swaps_hash(&self) -> Option<&Bytes> {
441 self.swaps_hash.as_ref()
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 #[serde(default, skip_serializing_if = "Option::is_none")]
469 price_guard: Option<PriceGuardConfig>,
470}
471
472impl EncodingOptions {
473 pub fn new(slippage: f64) -> Self {
475 Self {
476 slippage,
477 transfer_type: UserTransferType::default(),
478 permit: None,
479 permit2_signature: None,
480 client_fee_params: None,
481 price_guard: None,
482 }
483 }
484
485 pub fn with_transfer_type(mut self, t: UserTransferType) -> Self {
487 self.transfer_type = t;
488 self
489 }
490
491 pub fn with_permit2(mut self, permit: PermitSingle, sig: Bytes) -> Self {
493 self.permit = Some(permit);
494 self.permit2_signature = Some(sig);
495 self
496 }
497
498 pub fn slippage(&self) -> f64 {
500 self.slippage
501 }
502
503 pub fn transfer_type(&self) -> &UserTransferType {
505 &self.transfer_type
506 }
507
508 pub fn permit(&self) -> Option<&PermitSingle> {
510 self.permit.as_ref()
511 }
512
513 pub fn permit2_signature(&self) -> Option<&Bytes> {
515 self.permit2_signature.as_ref()
516 }
517
518 pub fn with_client_fee_params(mut self, params: ClientFeeParams) -> Self {
520 self.client_fee_params = Some(params);
521 self
522 }
523
524 pub fn client_fee_params(&self) -> Option<&ClientFeeParams> {
526 self.client_fee_params.as_ref()
527 }
528
529 pub fn with_price_guard(mut self, config: PriceGuardConfig) -> Self {
531 self.price_guard = Some(config);
532 self
533 }
534
535 pub fn price_guard(&self) -> Option<&PriceGuardConfig> {
537 self.price_guard.as_ref()
538 }
539}
540
541#[serde_as]
543#[derive(Debug, Clone, Serialize, Deserialize)]
544#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
545pub struct PermitSingle {
546 details: PermitDetails,
548 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"))]
550 spender: Bytes,
551 #[serde_as(as = "DisplayFromStr")]
553 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "1893456000"))]
554 sig_deadline: BigUint,
555}
556
557impl PermitSingle {
558 pub fn new(details: PermitDetails, spender: Bytes, sig_deadline: BigUint) -> Self {
560 Self { details, spender, sig_deadline }
561 }
562
563 pub fn details(&self) -> &PermitDetails {
565 &self.details
566 }
567
568 pub fn spender(&self) -> &Bytes {
570 &self.spender
571 }
572
573 pub fn sig_deadline(&self) -> &BigUint {
575 &self.sig_deadline
576 }
577}
578
579#[serde_as]
581#[derive(Debug, Clone, Serialize, Deserialize)]
582#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
583pub struct PermitDetails {
584 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"))]
586 token: Bytes,
587 #[serde_as(as = "DisplayFromStr")]
589 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "1000000000000000000"))]
590 amount: BigUint,
591 #[serde_as(as = "DisplayFromStr")]
593 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "1893456000"))]
594 expiration: BigUint,
595 #[serde_as(as = "DisplayFromStr")]
597 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0"))]
598 nonce: BigUint,
599}
600
601impl PermitDetails {
602 pub fn new(token: Bytes, amount: BigUint, expiration: BigUint, nonce: BigUint) -> Self {
604 Self { token, amount, expiration, nonce }
605 }
606
607 pub fn token(&self) -> &Bytes {
609 &self.token
610 }
611
612 pub fn amount(&self) -> &BigUint {
614 &self.amount
615 }
616
617 pub fn expiration(&self) -> &BigUint {
619 &self.expiration
620 }
621
622 pub fn nonce(&self) -> &BigUint {
624 &self.nonce
625 }
626}
627
628#[must_use]
637#[serde_as]
638#[derive(Debug, Clone, Serialize, Deserialize)]
639#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
640pub struct Quote {
641 orders: Vec<OrderQuote>,
643 #[serde_as(as = "DisplayFromStr")]
645 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "150000"))]
646 total_gas_estimate: BigUint,
647 #[cfg_attr(feature = "openapi", schema(example = 12))]
649 solve_time_ms: u64,
650}
651
652impl Quote {
653 pub fn new(orders: Vec<OrderQuote>, total_gas_estimate: BigUint, solve_time_ms: u64) -> Self {
655 Self { orders, total_gas_estimate, solve_time_ms }
656 }
657
658 pub fn orders(&self) -> &[OrderQuote] {
660 &self.orders
661 }
662
663 pub fn into_orders(self) -> Vec<OrderQuote> {
665 self.orders
666 }
667
668 pub fn total_gas_estimate(&self) -> &BigUint {
670 &self.total_gas_estimate
671 }
672
673 pub fn solve_time_ms(&self) -> u64 {
675 self.solve_time_ms
676 }
677}
678
679#[must_use]
683#[serde_as]
684#[derive(Debug, Clone, Serialize, Deserialize)]
685#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
686pub struct Order {
687 #[serde(default = "generate_order_id", skip_deserializing)]
691 id: String,
692 #[cfg_attr(
694 feature = "openapi",
695 schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
696 )]
697 token_in: Address,
698 #[cfg_attr(
700 feature = "openapi",
701 schema(value_type = String, example = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
702 )]
703 token_out: Address,
704 #[serde_as(as = "DisplayFromStr")]
706 #[cfg_attr(
707 feature = "openapi",
708 schema(value_type = String, example = "1000000000000000000")
709 )]
710 amount: BigUint,
711 side: OrderSide,
713 #[cfg_attr(
715 feature = "openapi",
716 schema(value_type = String, example = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
717 )]
718 sender: Address,
719 #[serde(default, skip_serializing_if = "Option::is_none")]
723 #[cfg_attr(
724 feature = "openapi",
725 schema(value_type = Option<String>, example = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
726 )]
727 receiver: Option<Address>,
728}
729
730impl Order {
731 pub fn new(
733 token_in: Address,
734 token_out: Address,
735 amount: BigUint,
736 side: OrderSide,
737 sender: Address,
738 ) -> Self {
739 Self { id: String::new(), token_in, token_out, amount, side, sender, receiver: None }
740 }
741
742 pub fn with_id(mut self, id: impl Into<String>) -> Self {
744 self.id = id.into();
745 self
746 }
747
748 pub fn with_receiver(mut self, receiver: Address) -> Self {
750 self.receiver = Some(receiver);
751 self
752 }
753
754 pub fn id(&self) -> &str {
756 &self.id
757 }
758
759 pub fn token_in(&self) -> &Address {
761 &self.token_in
762 }
763
764 pub fn token_out(&self) -> &Address {
766 &self.token_out
767 }
768
769 pub fn amount(&self) -> &BigUint {
771 &self.amount
772 }
773
774 pub fn side(&self) -> OrderSide {
776 self.side
777 }
778
779 pub fn sender(&self) -> &Address {
781 &self.sender
782 }
783
784 pub fn receiver(&self) -> Option<&Address> {
786 self.receiver.as_ref()
787 }
788}
789
790#[non_exhaustive]
794#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
795#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
796#[serde(rename_all = "snake_case")]
797pub enum OrderSide {
798 Sell,
800}
801
802#[must_use]
807#[serde_as]
808#[derive(Debug, Clone, Serialize, Deserialize)]
809#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
810pub struct OrderQuote {
811 #[cfg_attr(feature = "openapi", schema(example = "f47ac10b-58cc-4372-a567-0e02b2c3d479"))]
813 order_id: String,
814 status: QuoteStatus,
816 #[serde(skip_serializing_if = "Option::is_none")]
818 route: Option<Route>,
819 #[serde_as(as = "DisplayFromStr")]
821 #[cfg_attr(
822 feature = "openapi",
823 schema(value_type = String, example = "1000000000000000000")
824 )]
825 amount_in: BigUint,
826 #[serde_as(as = "DisplayFromStr")]
828 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3500000000"))]
829 amount_out: BigUint,
830 #[serde_as(as = "DisplayFromStr")]
832 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "150000"))]
833 gas_estimate: BigUint,
834 #[serde(skip_serializing_if = "Option::is_none")]
836 price_impact_bps: Option<i32>,
837 #[serde_as(as = "DisplayFromStr")]
840 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3498000000"))]
841 amount_out_net_gas: BigUint,
842 block: BlockInfo,
844 #[serde_as(as = "Option<DisplayFromStr>")]
846 #[serde(skip_serializing_if = "Option::is_none")]
847 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "20000000000"))]
848 gas_price: Option<BigUint>,
849 transaction: Option<Transaction>,
851 #[serde(skip_serializing_if = "Option::is_none")]
853 fee_breakdown: Option<FeeBreakdown>,
854}
855
856impl OrderQuote {
857 pub fn order_id(&self) -> &str {
859 &self.order_id
860 }
861
862 pub fn status(&self) -> QuoteStatus {
864 self.status
865 }
866
867 pub fn route(&self) -> Option<&Route> {
869 self.route.as_ref()
870 }
871
872 pub fn amount_in(&self) -> &BigUint {
874 &self.amount_in
875 }
876
877 pub fn amount_out(&self) -> &BigUint {
879 &self.amount_out
880 }
881
882 pub fn gas_estimate(&self) -> &BigUint {
884 &self.gas_estimate
885 }
886
887 pub fn price_impact_bps(&self) -> Option<i32> {
889 self.price_impact_bps
890 }
891
892 pub fn amount_out_net_gas(&self) -> &BigUint {
894 &self.amount_out_net_gas
895 }
896
897 pub fn block(&self) -> &BlockInfo {
899 &self.block
900 }
901
902 pub fn gas_price(&self) -> Option<&BigUint> {
904 self.gas_price.as_ref()
905 }
906
907 pub fn transaction(&self) -> Option<&Transaction> {
909 self.transaction.as_ref()
910 }
911
912 pub fn fee_breakdown(&self) -> Option<&FeeBreakdown> {
914 self.fee_breakdown.as_ref()
915 }
916}
917
918#[non_exhaustive]
920#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
921#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
922#[serde(rename_all = "snake_case")]
923pub enum QuoteStatus {
924 Success,
926 NoRouteFound,
928 InsufficientLiquidity,
930 Timeout,
932 NotReady,
934 PriceCheckFailed,
936}
937
938#[derive(Debug, Clone, Serialize, Deserialize)]
943#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
944pub struct BlockInfo {
945 #[cfg_attr(feature = "openapi", schema(example = 21000000))]
947 number: u64,
948 #[cfg_attr(
950 feature = "openapi",
951 schema(example = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd")
952 )]
953 hash: String,
954 #[cfg_attr(feature = "openapi", schema(example = 1730000000))]
956 timestamp: u64,
957}
958
959impl BlockInfo {
960 pub fn new(number: u64, hash: String, timestamp: u64) -> Self {
962 Self { number, hash, timestamp }
963 }
964
965 pub fn number(&self) -> u64 {
967 self.number
968 }
969
970 pub fn hash(&self) -> &str {
972 &self.hash
973 }
974
975 pub fn timestamp(&self) -> u64 {
977 self.timestamp
978 }
979}
980
981#[must_use]
990#[derive(Debug, Clone, Serialize, Deserialize)]
991#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
992pub struct Route {
993 swaps: Vec<Swap>,
995}
996
997impl Route {
998 pub fn new(swaps: Vec<Swap>) -> Self {
1000 Self { swaps }
1001 }
1002
1003 pub fn swaps(&self) -> &[Swap] {
1005 &self.swaps
1006 }
1007
1008 pub fn into_swaps(self) -> Vec<Swap> {
1010 self.swaps
1011 }
1012}
1013
1014#[serde_as]
1018#[derive(Debug, Clone, Serialize, Deserialize)]
1019#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1020pub struct Swap {
1021 #[cfg_attr(
1023 feature = "openapi",
1024 schema(example = "0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc")
1025 )]
1026 component_id: String,
1027 #[cfg_attr(feature = "openapi", schema(example = "uniswap_v2"))]
1029 protocol: String,
1030 #[cfg_attr(
1032 feature = "openapi",
1033 schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
1034 )]
1035 token_in: Address,
1036 #[cfg_attr(
1038 feature = "openapi",
1039 schema(value_type = String, example = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
1040 )]
1041 token_out: Address,
1042 #[serde_as(as = "DisplayFromStr")]
1044 #[cfg_attr(
1045 feature = "openapi",
1046 schema(value_type = String, example = "1000000000000000000")
1047 )]
1048 amount_in: BigUint,
1049 #[serde_as(as = "DisplayFromStr")]
1051 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3500000000"))]
1052 amount_out: BigUint,
1053 #[serde_as(as = "DisplayFromStr")]
1055 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "150000"))]
1056 gas_estimate: BigUint,
1057 #[serde_as(as = "DisplayFromStr")]
1059 #[cfg_attr(feature = "openapi", schema(example = "0.0"))]
1060 split: f64,
1061}
1062
1063impl Swap {
1064 #[allow(clippy::too_many_arguments)]
1066 pub fn new(
1067 component_id: String,
1068 protocol: String,
1069 token_in: Address,
1070 token_out: Address,
1071 amount_in: BigUint,
1072 amount_out: BigUint,
1073 gas_estimate: BigUint,
1074 split: f64,
1075 ) -> Self {
1076 Self {
1077 component_id,
1078 protocol,
1079 token_in,
1080 token_out,
1081 amount_in,
1082 amount_out,
1083 gas_estimate,
1084 split,
1085 }
1086 }
1087
1088 pub fn component_id(&self) -> &str {
1090 &self.component_id
1091 }
1092
1093 pub fn protocol(&self) -> &str {
1095 &self.protocol
1096 }
1097
1098 pub fn token_in(&self) -> &Address {
1100 &self.token_in
1101 }
1102
1103 pub fn token_out(&self) -> &Address {
1105 &self.token_out
1106 }
1107
1108 pub fn amount_in(&self) -> &BigUint {
1110 &self.amount_in
1111 }
1112
1113 pub fn amount_out(&self) -> &BigUint {
1115 &self.amount_out
1116 }
1117
1118 pub fn gas_estimate(&self) -> &BigUint {
1120 &self.gas_estimate
1121 }
1122
1123 pub fn split(&self) -> f64 {
1125 self.split
1126 }
1127}
1128
1129#[derive(Debug, Clone, Serialize, Deserialize)]
1135#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1136pub struct HealthStatus {
1137 #[cfg_attr(feature = "openapi", schema(example = true))]
1139 healthy: bool,
1140 #[cfg_attr(feature = "openapi", schema(example = 1250))]
1142 last_update_ms: u64,
1143 #[cfg_attr(feature = "openapi", schema(example = 2))]
1148 num_solver_pools: usize,
1149 #[serde(default)]
1155 #[cfg_attr(feature = "openapi", schema(example = true))]
1156 derived_data_ready: bool,
1157 #[serde(default, skip_serializing_if = "Option::is_none")]
1159 #[cfg_attr(feature = "openapi", schema(example = 12000))]
1160 gas_price_age_ms: Option<u64>,
1161}
1162
1163impl HealthStatus {
1164 pub fn new(
1166 healthy: bool,
1167 last_update_ms: u64,
1168 num_solver_pools: usize,
1169 derived_data_ready: bool,
1170 gas_price_age_ms: Option<u64>,
1171 ) -> Self {
1172 Self { healthy, last_update_ms, num_solver_pools, derived_data_ready, gas_price_age_ms }
1173 }
1174
1175 pub fn healthy(&self) -> bool {
1177 self.healthy
1178 }
1179
1180 pub fn last_update_ms(&self) -> u64 {
1182 self.last_update_ms
1183 }
1184
1185 pub fn num_solver_pools(&self) -> usize {
1187 self.num_solver_pools
1188 }
1189
1190 pub fn derived_data_ready(&self) -> bool {
1192 self.derived_data_ready
1193 }
1194
1195 pub fn gas_price_age_ms(&self) -> Option<u64> {
1197 self.gas_price_age_ms
1198 }
1199}
1200
1201#[derive(Debug, Clone, Serialize, Deserialize)]
1211#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1212#[non_exhaustive]
1213pub struct InstanceInfo {
1214 #[cfg_attr(feature = "openapi", schema(example = 1))]
1216 chain_id: u64,
1217 #[cfg_attr(
1219 feature = "openapi",
1220 schema(value_type = Option<String>, example = "0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35")
1221 )]
1222 router_address: Option<Bytes>,
1223 #[cfg_attr(
1225 feature = "openapi",
1226 schema(value_type = String, example = "0x000000000022D473030F116dDEE9F6B43aC78BA3")
1227 )]
1228 permit2_address: Bytes,
1229 #[serde(default)]
1233 #[cfg_attr(feature = "openapi", schema(example = "0.89.1"))]
1234 version: String,
1235}
1236
1237impl InstanceInfo {
1238 pub fn builder(
1240 chain_id: u64,
1241 router_address: Option<Bytes>,
1242 permit2_address: Bytes,
1243 ) -> InstanceInfoBuilder {
1244 InstanceInfoBuilder { chain_id, router_address, permit2_address, version: String::new() }
1245 }
1246
1247 pub fn chain_id(&self) -> u64 {
1249 self.chain_id
1250 }
1251
1252 pub fn router_address(&self) -> Option<&Bytes> {
1254 self.router_address.as_ref()
1255 }
1256
1257 pub fn permit2_address(&self) -> &Bytes {
1259 &self.permit2_address
1260 }
1261
1262 pub fn version(&self) -> &str {
1264 &self.version
1265 }
1266}
1267
1268#[derive(Debug, Clone)]
1270pub struct InstanceInfoBuilder {
1271 chain_id: u64,
1272 router_address: Option<Bytes>,
1273 permit2_address: Bytes,
1274 version: String,
1275}
1276
1277impl InstanceInfoBuilder {
1278 pub fn version(mut self, version: impl Into<String>) -> Self {
1280 self.version = version.into();
1281 self
1282 }
1283
1284 pub fn build(self) -> InstanceInfo {
1286 InstanceInfo {
1287 chain_id: self.chain_id,
1288 router_address: self.router_address,
1289 permit2_address: self.permit2_address,
1290 version: self.version,
1291 }
1292 }
1293}
1294
1295#[must_use]
1297#[derive(Debug, Serialize, Deserialize)]
1298#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1299pub struct ErrorResponse {
1300 #[cfg_attr(feature = "openapi", schema(example = "bad request: no orders provided"))]
1301 error: String,
1302 #[cfg_attr(feature = "openapi", schema(example = "BAD_REQUEST"))]
1303 code: String,
1304 #[serde(skip_serializing_if = "Option::is_none")]
1305 details: Option<serde_json::Value>,
1306}
1307
1308impl ErrorResponse {
1309 pub fn new(error: String, code: String) -> Self {
1311 Self { error, code, details: None }
1312 }
1313
1314 pub fn with_details(mut self, details: serde_json::Value) -> Self {
1316 self.details = Some(details);
1317 self
1318 }
1319
1320 pub fn error(&self) -> &str {
1322 &self.error
1323 }
1324
1325 pub fn code(&self) -> &str {
1327 &self.code
1328 }
1329
1330 pub fn details(&self) -> Option<&serde_json::Value> {
1332 self.details.as_ref()
1333 }
1334}
1335
1336#[serde_as]
1342#[derive(Debug, Clone, Serialize, Deserialize)]
1343#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1344pub struct Transaction {
1345 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"))]
1347 to: Bytes,
1348 #[serde_as(as = "DisplayFromStr")]
1350 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0"))]
1351 value: BigUint,
1352 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0x1234567890abcdef"))]
1354 #[serde(serialize_with = "serialize_bytes_hex", deserialize_with = "deserialize_bytes_hex")]
1355 data: Vec<u8>,
1356 #[serde(default, skip_serializing_if = "Option::is_none")]
1359 #[cfg_attr(feature = "openapi", schema(example = json!(null)))]
1360 client_fee_signature_offset: Option<usize>,
1361}
1362
1363impl Transaction {
1364 pub fn new(to: Bytes, value: BigUint, data: Vec<u8>) -> Self {
1366 Self { to, value, data, client_fee_signature_offset: None }
1367 }
1368
1369 pub fn to(&self) -> &Bytes {
1371 &self.to
1372 }
1373
1374 pub fn value(&self) -> &BigUint {
1376 &self.value
1377 }
1378
1379 pub fn data(&self) -> &[u8] {
1381 &self.data
1382 }
1383
1384 pub fn client_fee_signature_offset(&self) -> Option<usize> {
1386 self.client_fee_signature_offset
1387 }
1388}
1389
1390fn serialize_bytes_hex<S>(bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
1396where
1397 S: serde::Serializer,
1398{
1399 serializer.serialize_str(&format!("0x{}", hex::encode(bytes)))
1400}
1401
1402fn deserialize_bytes_hex<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
1404where
1405 D: serde::Deserializer<'de>,
1406{
1407 let s = String::deserialize(deserializer)?;
1408 let s = s.strip_prefix("0x").unwrap_or(&s);
1409 hex::decode(s).map_err(serde::de::Error::custom)
1410}
1411
1412fn generate_order_id() -> String {
1418 Uuid::new_v4().to_string()
1419}
1420
1421#[cfg(test)]
1430mod wire_format_tests {
1431 use num_bigint::BigUint;
1432
1433 use super::*;
1434
1435 #[test]
1442 fn bytes_deserializes_without_0x_prefix() {
1443 let b: Bytes = serde_json::from_str(r#""deadbeef""#).unwrap();
1444 assert_eq!(b.as_ref(), [0xDE, 0xAD, 0xBE, 0xEF]);
1445 }
1446
1447 #[test]
1454 fn order_serializes_to_full_json() {
1455 let order = Order::new(
1456 Bytes::from([0xAAu8; 20]),
1457 Bytes::from([0xBBu8; 20]),
1458 BigUint::from(1_000_000_000_000_000_000u64),
1459 OrderSide::Sell,
1460 Bytes::from([0xCCu8; 20]),
1461 )
1462 .with_id("abc");
1463
1464 assert_eq!(
1465 serde_json::to_value(&order).unwrap(),
1466 serde_json::json!({
1467 "id": "abc",
1468 "token_in": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1469 "token_out": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1470 "amount": "1000000000000000000",
1471 "side": "sell",
1472 "sender": "0xcccccccccccccccccccccccccccccccccccccccc"
1473 })
1474 );
1475 }
1476
1477 #[test]
1484 fn order_quote_deserializes_from_json() {
1485 let json = r#"{
1486 "order_id": "order-1",
1487 "status": "success",
1488 "amount_in": "1000000000000000000",
1489 "amount_out": "2000000000",
1490 "gas_estimate": "150000",
1491 "amount_out_net_gas": "1999000000",
1492 "price_impact_bps": 5,
1493 "block": { "number": 21000000, "hash": "0xdeadbeef", "timestamp": 1700000000 },
1494 "route": { "swaps": [{
1495 "component_id": "component-1",
1496 "protocol": "uniswap_v3",
1497 "token_in": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1498 "token_out": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1499 "amount_in": "1000000000000000000",
1500 "amount_out": "2000000000",
1501 "gas_estimate": "150000",
1502 "split": "0"
1503 }]}
1504 }"#;
1505
1506 let quote: OrderQuote = serde_json::from_str(json).unwrap();
1507
1508 assert_eq!(quote.status(), QuoteStatus::Success);
1509 assert_eq!(*quote.amount_in(), BigUint::from(1_000_000_000_000_000_000u64));
1510 assert_eq!(quote.price_impact_bps(), Some(5));
1511 assert_eq!(quote.block().number(), 21_000_000);
1512
1513 let swap = "e.route().unwrap().swaps()[0];
1514 assert_eq!(swap.token_in().as_ref(), [0xAAu8; 20]);
1515 assert_eq!(swap.token_out().as_ref(), [0xBBu8; 20]);
1516 assert_eq!(swap.split(), 0.0);
1517 }
1518
1519 #[test]
1526 fn encoding_options_serializes_to_full_json() {
1527 assert_eq!(
1528 serde_json::to_value(EncodingOptions::new(0.005)).unwrap(),
1529 serde_json::json!({
1530 "slippage": "0.005",
1531 "transfer_type": "transfer_from"
1532 })
1533 );
1534 }
1535
1536 #[test]
1543 fn instance_info_deserializes_and_ignores_unknown_fields() {
1544 let json = r#"{
1545 "version": "1.2.3",
1546 "chain_id": 1,
1547 "router_address": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1548 "permit2_address": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1549 "future_field": "ignored"
1550 }"#;
1551
1552 let info: InstanceInfo = serde_json::from_str(json).unwrap();
1553 assert_eq!(info.version(), "1.2.3");
1554 assert_eq!(info.chain_id(), 1);
1555 assert_eq!(info.router_address().unwrap().as_ref(), [0xAAu8; 20]);
1556 assert_eq!(info.permit2_address().as_ref(), [0xBBu8; 20]);
1557 }
1558
1559 #[test]
1560 fn instance_info_builder_sets_fields() {
1561 let info =
1562 InstanceInfo::builder(1, Some(Bytes::from([0xAAu8; 20])), Bytes::from([0xBBu8; 20]))
1563 .version("0.1.0")
1564 .build();
1565
1566 assert_eq!(info.version(), "0.1.0");
1567 assert_eq!(info.chain_id(), 1);
1568 assert_eq!(info.router_address().unwrap().as_ref(), [0xAAu8; 20]);
1569 assert_eq!(info.permit2_address().as_ref(), [0xBBu8; 20]);
1570 }
1571
1572 #[test]
1573 fn instance_info_deserializes_without_version() {
1574 let json = r#"{
1576 "chain_id": 1,
1577 "router_address": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1578 "permit2_address": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
1579 }"#;
1580
1581 let info: InstanceInfo = serde_json::from_str(json).unwrap();
1582 assert_eq!(info.version(), "");
1583 assert_eq!(info.chain_id(), 1);
1584 }
1585}
1586
1587#[cfg(feature = "core")]
1598mod conversions {
1599 use tycho_simulation::tycho_core::Bytes as TychoBytes;
1600
1601 use super::*;
1602
1603 impl From<TychoBytes> for Bytes {
1609 fn from(b: TychoBytes) -> Self {
1610 Self(b.0)
1611 }
1612 }
1613
1614 impl From<Bytes> for TychoBytes {
1615 fn from(b: Bytes) -> Self {
1616 Self(b.0)
1617 }
1618 }
1619
1620 impl Into<fynd_core::QuoteRequest> for QuoteRequest {
1625 fn into(self) -> fynd_core::QuoteRequest {
1626 fynd_core::QuoteRequest::new(
1627 self.orders
1628 .into_iter()
1629 .map(Into::into)
1630 .collect(),
1631 self.options.into(),
1632 )
1633 }
1634 }
1635
1636 impl Into<fynd_core::QuoteOptions> for QuoteOptions {
1637 fn into(self) -> fynd_core::QuoteOptions {
1638 let mut opts = fynd_core::QuoteOptions::default();
1639 if let Some(ms) = self.timeout_ms {
1640 opts = opts.with_timeout_ms(ms);
1641 }
1642 if let Some(n) = self.min_responses {
1643 opts = opts.with_min_responses(n);
1644 }
1645 if let Some(gas) = self.max_gas {
1646 opts = opts.with_max_gas(gas);
1647 }
1648 if let Some(enc) = self.encoding_options {
1649 opts = opts.with_encoding_options(enc.into());
1650 }
1651 opts
1652 }
1653 }
1654
1655 impl Into<fynd_core::PriceGuardConfig> for PriceGuardConfig {
1656 fn into(self) -> fynd_core::PriceGuardConfig {
1657 let mut config = fynd_core::PriceGuardConfig::default();
1658 if let Some(bps) = self.lower_tolerance_bps {
1659 config = config.with_lower_tolerance_bps(bps);
1660 }
1661 if let Some(bps) = self.upper_tolerance_bps {
1662 config = config.with_upper_tolerance_bps(bps);
1663 }
1664 if let Some(fail) = self.fail_on_provider_error {
1665 config = config.with_fail_on_provider_error(fail);
1666 }
1667 if let Some(fail) = self.fail_on_token_price_not_found {
1668 config = config.with_fail_on_token_price_not_found(fail);
1669 }
1670 if let Some(enabled) = self.enabled {
1671 config = config.with_enabled(enabled);
1672 }
1673 config
1674 }
1675 }
1676
1677 impl Into<fynd_core::EncodingOptions> for EncodingOptions {
1678 fn into(self) -> fynd_core::EncodingOptions {
1679 let mut opts = fynd_core::EncodingOptions::new(self.slippage)
1680 .with_transfer_type(self.transfer_type.into());
1681 if let (Some(permit), Some(sig)) = (self.permit, self.permit2_signature) {
1682 opts = opts
1683 .with_permit(permit.into())
1684 .with_signature(sig.into());
1685 }
1686 if let Some(fee) = self.client_fee_params {
1687 opts = opts.with_client_fee_params(fee.into());
1688 }
1689 if let Some(pg) = self.price_guard {
1690 opts = opts.with_price_guard(pg.into());
1691 }
1692 opts
1693 }
1694 }
1695
1696 impl Into<fynd_core::ClientFeeParams> for ClientFeeParams {
1697 fn into(self) -> fynd_core::ClientFeeParams {
1698 fynd_core::ClientFeeParams::new(
1699 self.bps,
1700 self.receiver.into(),
1701 self.max_contribution,
1702 self.deadline,
1703 self.signature.into(),
1704 )
1705 }
1706 }
1707
1708 impl Into<fynd_core::UserTransferType> for UserTransferType {
1709 fn into(self) -> fynd_core::UserTransferType {
1710 match self {
1711 UserTransferType::TransferFromPermit2 => {
1712 fynd_core::UserTransferType::TransferFromPermit2
1713 }
1714 UserTransferType::TransferFrom => fynd_core::UserTransferType::TransferFrom,
1715 UserTransferType::UseVaultsFunds => fynd_core::UserTransferType::UseVaultsFunds,
1716 }
1717 }
1718 }
1719
1720 impl Into<fynd_core::PermitSingle> for PermitSingle {
1721 fn into(self) -> fynd_core::PermitSingle {
1722 fynd_core::PermitSingle::new(
1723 self.details.into(),
1724 self.spender.into(),
1725 self.sig_deadline,
1726 )
1727 }
1728 }
1729
1730 impl Into<fynd_core::PermitDetails> for PermitDetails {
1731 fn into(self) -> fynd_core::PermitDetails {
1732 fynd_core::PermitDetails::new(
1733 self.token.into(),
1734 self.amount,
1735 self.expiration,
1736 self.nonce,
1737 )
1738 }
1739 }
1740
1741 impl Into<fynd_core::Order> for Order {
1742 fn into(self) -> fynd_core::Order {
1743 let mut order = fynd_core::Order::new(
1744 self.token_in.into(),
1745 self.token_out.into(),
1746 self.amount,
1747 self.side.into(),
1748 self.sender.into(),
1749 )
1750 .with_id(self.id);
1751 if let Some(r) = self.receiver {
1752 order = order.with_receiver(r.into());
1753 }
1754 order
1755 }
1756 }
1757
1758 impl Into<fynd_core::OrderSide> for OrderSide {
1759 fn into(self) -> fynd_core::OrderSide {
1760 match self {
1761 OrderSide::Sell => fynd_core::OrderSide::Sell,
1762 }
1763 }
1764 }
1765
1766 impl From<fynd_core::Quote> for Quote {
1771 fn from(core: fynd_core::Quote) -> Self {
1772 let solve_time_ms = core.solve_time_ms();
1773 let total_gas_estimate = core.total_gas_estimate().clone();
1774 Self {
1775 orders: core
1776 .into_orders()
1777 .into_iter()
1778 .map(Into::into)
1779 .collect(),
1780 total_gas_estimate,
1781 solve_time_ms,
1782 }
1783 }
1784 }
1785
1786 impl From<fynd_core::OrderQuote> for OrderQuote {
1787 fn from(core: fynd_core::OrderQuote) -> Self {
1792 let order_id = core.order_id().to_string();
1793 let status = core.status().into();
1794 let amount_in = core.amount_in().clone();
1795 let amount_out = core.amount_out().clone();
1796 let gas_estimate = core.gas_estimate().clone();
1797 let price_impact_bps = core.price_impact_bps();
1798 let amount_out_net_gas = core.amount_out_net_gas().clone();
1799 let block = core.block().clone().into();
1800 let gas_price = core.gas_price().cloned();
1801 let transaction = core
1802 .transaction()
1803 .cloned()
1804 .map(Into::into);
1805 let fee_breakdown = core
1806 .fee_breakdown()
1807 .cloned()
1808 .map(Into::into);
1809 let route = core.into_route().map(Into::into);
1810 Self {
1811 order_id,
1812 status,
1813 route,
1814 amount_in,
1815 amount_out,
1816 gas_estimate,
1817 price_impact_bps,
1818 amount_out_net_gas,
1819 block,
1820 gas_price,
1821 transaction,
1822 fee_breakdown,
1823 }
1824 }
1825 }
1826
1827 impl From<fynd_core::QuoteStatus> for QuoteStatus {
1828 fn from(core: fynd_core::QuoteStatus) -> Self {
1829 match core {
1830 fynd_core::QuoteStatus::Success => Self::Success,
1831 fynd_core::QuoteStatus::NoRouteFound => Self::NoRouteFound,
1832 fynd_core::QuoteStatus::InsufficientLiquidity => Self::InsufficientLiquidity,
1833 fynd_core::QuoteStatus::Timeout => Self::Timeout,
1834 fynd_core::QuoteStatus::NotReady => Self::NotReady,
1835 fynd_core::QuoteStatus::PriceCheckFailed => Self::PriceCheckFailed,
1836 _ => Self::NotReady,
1838 }
1839 }
1840 }
1841
1842 impl From<fynd_core::BlockInfo> for BlockInfo {
1843 fn from(core: fynd_core::BlockInfo) -> Self {
1844 Self {
1845 number: core.number(),
1846 hash: core.hash().to_string(),
1847 timestamp: core.timestamp(),
1848 }
1849 }
1850 }
1851
1852 impl From<fynd_core::Route> for Route {
1853 fn from(core: fynd_core::Route) -> Self {
1854 Self {
1855 swaps: core
1856 .into_swaps()
1857 .into_iter()
1858 .map(Into::into)
1859 .collect(),
1860 }
1861 }
1862 }
1863
1864 impl From<fynd_core::Swap> for Swap {
1865 fn from(core: fynd_core::Swap) -> Self {
1866 Self {
1867 component_id: core.component_id().to_string(),
1868 protocol: core.protocol().to_string(),
1869 token_in: core.token_in().clone().into(),
1870 token_out: core.token_out().clone().into(),
1871 amount_in: core.amount_in().clone(),
1872 amount_out: core.amount_out().clone(),
1873 gas_estimate: core.gas_estimate().clone(),
1874 split: *core.split(),
1875 }
1876 }
1877 }
1878
1879 impl From<fynd_core::Transaction> for Transaction {
1880 fn from(core: fynd_core::Transaction) -> Self {
1881 Self {
1882 to: core.to().clone().into(),
1883 value: core.value().clone(),
1884 data: core.data().to_vec(),
1885 client_fee_signature_offset: core.client_fee_signature_offset(),
1886 }
1887 }
1888 }
1889
1890 impl From<fynd_core::FeeBreakdown> for FeeBreakdown {
1891 fn from(core: fynd_core::FeeBreakdown) -> Self {
1892 let swaps_hash = core
1893 .swaps_hash()
1894 .map(|h| Bytes(bytes::Bytes::copy_from_slice(h.as_ref())));
1895 Self {
1896 router_fee: core.router_fee().clone(),
1897 client_fee: core.client_fee().clone(),
1898 max_slippage: core.max_slippage().clone(),
1899 min_amount_received: core.min_amount_received().clone(),
1900 swaps_hash,
1901 }
1902 }
1903 }
1904
1905 #[cfg(test)]
1906 mod tests {
1907 use num_bigint::BigUint;
1908
1909 use super::*;
1910
1911 fn make_address(byte: u8) -> Address {
1912 Address::from([byte; 20])
1913 }
1914
1915 #[test]
1916 fn test_quote_request_roundtrip() {
1917 let dto = QuoteRequest {
1918 orders: vec![Order {
1919 id: "test-id".to_string(),
1920 token_in: make_address(0x01),
1921 token_out: make_address(0x02),
1922 amount: BigUint::from(1000u64),
1923 side: OrderSide::Sell,
1924 sender: make_address(0xAA),
1925 receiver: None,
1926 }],
1927 options: QuoteOptions {
1928 timeout_ms: Some(5000),
1929 min_responses: None,
1930 max_gas: None,
1931 encoding_options: None,
1932 },
1933 };
1934
1935 let core: fynd_core::QuoteRequest = dto.clone().into();
1936 assert_eq!(core.orders().len(), 1);
1937 assert_eq!(core.orders()[0].id(), "test-id");
1938 assert_eq!(core.options().timeout_ms(), Some(5000));
1939 }
1940
1941 #[test]
1942 fn test_quote_from_core() {
1943 let core: fynd_core::Quote = serde_json::from_str(
1944 r#"{"orders":[],"total_gas_estimate":"100000","solve_time_ms":50}"#,
1945 )
1946 .unwrap();
1947
1948 let dto = Quote::from(core);
1949 assert_eq!(dto.total_gas_estimate, BigUint::from(100_000u64));
1950 assert_eq!(dto.solve_time_ms, 50);
1951 }
1952
1953 #[test]
1954 fn test_order_side_into_core() {
1955 let core: fynd_core::OrderSide = OrderSide::Sell.into();
1956 assert_eq!(core, fynd_core::OrderSide::Sell);
1957 }
1958
1959 #[test]
1960 fn test_client_fee_params_into_core() {
1961 let dto = ClientFeeParams::new(
1962 200,
1963 Bytes::from(make_address(0xBB).as_ref()),
1964 BigUint::from(1_000_000u64),
1965 1_893_456_000u64,
1966 Bytes::from(vec![0xABu8; 65]),
1967 );
1968 let core: fynd_core::ClientFeeParams = dto.into();
1969 assert_eq!(core.bps(), 200);
1970 assert_eq!(*core.max_contribution(), BigUint::from(1_000_000u64));
1971 assert_eq!(core.deadline(), 1_893_456_000u64);
1972 assert_eq!(core.signature().len(), 65);
1973 }
1974
1975 #[test]
1976 fn test_encoding_options_with_client_fee_into_core() {
1977 let fee = ClientFeeParams::new(
1978 100,
1979 Bytes::from(make_address(0xCC).as_ref()),
1980 BigUint::from(500u64),
1981 9_999u64,
1982 Bytes::from(vec![0xDEu8; 65]),
1983 );
1984 let dto = EncodingOptions::new(0.005).with_client_fee_params(fee);
1985 let core: fynd_core::EncodingOptions = dto.into();
1986
1987 assert!(core.client_fee_params().is_some());
1988 let core_fee = core.client_fee_params().unwrap();
1989 assert_eq!(core_fee.bps(), 100);
1990 assert_eq!(*core_fee.max_contribution(), BigUint::from(500u64));
1991 }
1992
1993 #[test]
1994 fn test_client_fee_params_serde_roundtrip() {
1995 let fee = ClientFeeParams::new(
1996 150,
1997 Bytes::from(make_address(0xDD).as_ref()),
1998 BigUint::from(999_999u64),
1999 1_700_000_000u64,
2000 Bytes::from(vec![0xFFu8; 65]),
2001 );
2002 let json = serde_json::to_string(&fee).unwrap();
2003 assert!(json.contains(r#""max_contribution":"999999""#));
2004 assert!(json.contains(r#""deadline":1700000000"#));
2005
2006 let deserialized: ClientFeeParams = serde_json::from_str(&json).unwrap();
2007 assert_eq!(deserialized.bps(), 150);
2008 assert_eq!(*deserialized.max_contribution(), BigUint::from(999_999u64));
2009 }
2010
2011 #[test]
2012 fn test_price_guard_config_into_core() {
2013 let dto = PriceGuardConfig::default()
2014 .with_lower_tolerance_bps(200)
2015 .with_upper_tolerance_bps(5000)
2016 .with_fail_on_provider_error(false)
2017 .with_enabled(false);
2018
2019 let config: fynd_core::PriceGuardConfig = dto.into();
2020 assert_eq!(config.lower_tolerance_bps(), 200);
2021 assert_eq!(config.upper_tolerance_bps(), 5000);
2022 assert!(!config.fail_on_provider_error());
2023 assert!(!config.enabled());
2024 }
2025
2026 #[test]
2027 fn test_encoding_options_with_price_guard_roundtrip() {
2028 let enc = EncodingOptions::new(0.01)
2029 .with_price_guard(PriceGuardConfig::default().with_enabled(false));
2030 let dto = QuoteRequest {
2031 orders: vec![Order {
2032 id: "pg-test".to_string(),
2033 token_in: make_address(0x01),
2034 token_out: make_address(0x02),
2035 amount: BigUint::from(1000u64),
2036 side: OrderSide::Sell,
2037 sender: make_address(0xAA),
2038 receiver: None,
2039 }],
2040 options: QuoteOptions::default().with_encoding_options(enc),
2041 };
2042
2043 let core: fynd_core::QuoteRequest = dto.into();
2044 let config = core
2045 .options()
2046 .encoding_options()
2047 .expect("encoding_options should be set")
2048 .price_guard();
2049 assert!(!config.enabled());
2050 }
2051
2052 #[test]
2053 fn test_quote_status_from_core() {
2054 let cases = [
2055 (fynd_core::QuoteStatus::Success, QuoteStatus::Success),
2056 (fynd_core::QuoteStatus::NoRouteFound, QuoteStatus::NoRouteFound),
2057 (fynd_core::QuoteStatus::InsufficientLiquidity, QuoteStatus::InsufficientLiquidity),
2058 (fynd_core::QuoteStatus::Timeout, QuoteStatus::Timeout),
2059 (fynd_core::QuoteStatus::NotReady, QuoteStatus::NotReady),
2060 ];
2061
2062 for (core, expected) in cases {
2063 assert_eq!(QuoteStatus::from(core), expected);
2064 }
2065 }
2066 }
2067}