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]
153#[derive(Debug, Clone, Default, Serialize, Deserialize)]
154#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
155pub struct RouteFilter {
156 #[serde(default, skip_serializing_if = "Vec::is_empty")]
158 exclude_pools: Vec<String>,
159 #[serde(default, skip_serializing_if = "Vec::is_empty")]
162 #[cfg_attr(feature = "openapi", schema(example = json!(["uniswap_v2"])))]
163 exclude_protocols: Vec<String>,
164 #[serde(default, skip_serializing_if = "Vec::is_empty")]
167 #[cfg_attr(
168 feature = "openapi",
169 schema(
170 value_type = Vec<String>,
171 example = json!(["0xdAC17F958D2ee523a2206206994597C13D831ec7"])
172 )
173 )]
174 exclude_tokens: Vec<Address>,
175}
176
177impl RouteFilter {
178 pub fn with_excluded_pools(mut self, pools: impl IntoIterator<Item = String>) -> Self {
180 self.exclude_pools.extend(pools);
181 self
182 }
183
184 pub fn with_excluded_protocols(mut self, protocols: impl IntoIterator<Item = String>) -> Self {
186 self.exclude_protocols.extend(protocols);
187 self
188 }
189
190 pub fn with_excluded_tokens(mut self, tokens: impl IntoIterator<Item = Address>) -> Self {
192 self.exclude_tokens.extend(tokens);
193 self
194 }
195
196 pub fn excluded_pools(&self) -> &[String] {
198 &self.exclude_pools
199 }
200
201 pub fn excluded_protocols(&self) -> &[String] {
203 &self.exclude_protocols
204 }
205
206 pub fn excluded_tokens(&self) -> &[Address] {
208 &self.exclude_tokens
209 }
210}
211
212#[must_use]
214#[serde_as]
215#[derive(Debug, Clone, Default, Serialize, Deserialize)]
216#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
217pub struct QuoteOptions {
218 #[cfg_attr(feature = "openapi", schema(example = 2000))]
220 timeout_ms: Option<u64>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
227 min_responses: Option<usize>,
228 #[serde_as(as = "Option<DisplayFromStr>")]
230 #[serde(default, skip_serializing_if = "Option::is_none")]
231 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "500000"))]
232 max_gas: Option<BigUint>,
233 encoding_options: Option<EncodingOptions>,
235 #[serde(default, skip_serializing_if = "Option::is_none")]
237 route_filter: Option<RouteFilter>,
238}
239
240impl QuoteOptions {
241 pub fn with_timeout_ms(mut self, ms: u64) -> Self {
243 self.timeout_ms = Some(ms);
244 self
245 }
246
247 pub fn with_min_responses(mut self, n: usize) -> Self {
249 self.min_responses = Some(n);
250 self
251 }
252
253 pub fn with_max_gas(mut self, gas: BigUint) -> Self {
255 self.max_gas = Some(gas);
256 self
257 }
258
259 pub fn with_encoding_options(mut self, opts: EncodingOptions) -> Self {
261 self.encoding_options = Some(opts);
262 self
263 }
264
265 pub fn with_route_filter(mut self, filter: RouteFilter) -> Self {
267 self.route_filter = Some(filter);
268 self
269 }
270
271 pub fn timeout_ms(&self) -> Option<u64> {
273 self.timeout_ms
274 }
275
276 pub fn min_responses(&self) -> Option<usize> {
278 self.min_responses
279 }
280
281 pub fn max_gas(&self) -> Option<&BigUint> {
283 self.max_gas.as_ref()
284 }
285
286 pub fn encoding_options(&self) -> Option<&EncodingOptions> {
288 self.encoding_options.as_ref()
289 }
290
291 pub fn route_filter(&self) -> Option<&RouteFilter> {
293 self.route_filter.as_ref()
294 }
295}
296
297#[derive(Debug, Clone, Default, Serialize, Deserialize)]
301#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
302pub struct PriceGuardConfig {
303 #[serde(default, skip_serializing_if = "Option::is_none")]
305 #[cfg_attr(feature = "openapi", schema(example = 300))]
306 lower_tolerance_bps: Option<u32>,
307 #[serde(default, skip_serializing_if = "Option::is_none")]
309 #[cfg_attr(feature = "openapi", schema(example = 10000))]
310 upper_tolerance_bps: Option<u32>,
311 #[serde(default, skip_serializing_if = "Option::is_none")]
313 fail_on_provider_error: Option<bool>,
314 #[serde(default, skip_serializing_if = "Option::is_none")]
316 fail_on_token_price_not_found: Option<bool>,
317 #[serde(default, skip_serializing_if = "Option::is_none")]
319 enabled: Option<bool>,
320}
321
322impl PriceGuardConfig {
323 pub fn with_lower_tolerance_bps(mut self, bps: u32) -> Self {
325 self.lower_tolerance_bps = Some(bps);
326 self
327 }
328
329 pub fn with_upper_tolerance_bps(mut self, bps: u32) -> Self {
331 self.upper_tolerance_bps = Some(bps);
332 self
333 }
334
335 pub fn with_fail_on_provider_error(mut self, fail: bool) -> Self {
337 self.fail_on_provider_error = Some(fail);
338 self
339 }
340
341 pub fn with_fail_on_token_price_not_found(mut self, fail: bool) -> Self {
343 self.fail_on_token_price_not_found = Some(fail);
344 self
345 }
346
347 pub fn with_enabled(mut self, enabled: bool) -> Self {
349 self.enabled = Some(enabled);
350 self
351 }
352
353 pub fn lower_tolerance_bps(&self) -> Option<u32> {
355 self.lower_tolerance_bps
356 }
357
358 pub fn upper_tolerance_bps(&self) -> Option<u32> {
360 self.upper_tolerance_bps
361 }
362
363 pub fn fail_on_provider_error(&self) -> Option<bool> {
365 self.fail_on_provider_error
366 }
367
368 pub fn fail_on_token_price_not_found(&self) -> Option<bool> {
370 self.fail_on_token_price_not_found
371 }
372
373 pub fn enabled(&self) -> Option<bool> {
375 self.enabled
376 }
377}
378
379#[non_exhaustive]
381#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
382#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
383#[serde(rename_all = "snake_case")]
384pub enum UserTransferType {
385 TransferFromPermit2,
387 #[default]
389 TransferFrom,
390 UseVaultsFunds,
392}
393
394#[serde_as]
399#[derive(Debug, Clone, Serialize, Deserialize)]
400#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
401pub struct ClientFeeParams {
402 #[cfg_attr(feature = "openapi", schema(example = 100))]
404 bps: u16,
405 #[cfg_attr(
407 feature = "openapi",
408 schema(value_type = String, example = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
409 )]
410 receiver: Bytes,
411 #[serde_as(as = "DisplayFromStr")]
413 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0"))]
414 max_contribution: BigUint,
415 #[cfg_attr(feature = "openapi", schema(example = 1893456000))]
417 deadline: u64,
418 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0xabcd..."))]
420 signature: Bytes,
421}
422
423impl ClientFeeParams {
424 pub fn new(
426 bps: u16,
427 receiver: Bytes,
428 max_contribution: BigUint,
429 deadline: u64,
430 signature: Bytes,
431 ) -> Self {
432 Self { bps, receiver, max_contribution, deadline, signature }
433 }
434
435 pub fn bps(&self) -> u16 {
437 self.bps
438 }
439
440 pub fn receiver(&self) -> &Bytes {
442 &self.receiver
443 }
444
445 pub fn max_contribution(&self) -> &BigUint {
447 &self.max_contribution
448 }
449
450 pub fn deadline(&self) -> u64 {
452 self.deadline
453 }
454
455 pub fn signature(&self) -> &Bytes {
457 &self.signature
458 }
459}
460
461#[serde_as]
465#[derive(Debug, Clone, Serialize, Deserialize)]
466#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
467pub struct FeeBreakdown {
468 #[serde_as(as = "DisplayFromStr")]
470 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "350000"))]
471 router_fee: BigUint,
472 #[serde_as(as = "DisplayFromStr")]
474 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "2800000"))]
475 client_fee: BigUint,
476 #[serde_as(as = "DisplayFromStr")]
478 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3496850"))]
479 max_slippage: BigUint,
480 #[serde_as(as = "DisplayFromStr")]
483 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3493353150"))]
484 min_amount_received: BigUint,
485 #[serde(default, skip_serializing_if = "Option::is_none")]
491 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = json!(null)))]
492 swaps_hash: Option<Bytes>,
493}
494
495impl FeeBreakdown {
496 pub fn router_fee(&self) -> &BigUint {
498 &self.router_fee
499 }
500
501 pub fn client_fee(&self) -> &BigUint {
503 &self.client_fee
504 }
505
506 pub fn max_slippage(&self) -> &BigUint {
508 &self.max_slippage
509 }
510
511 pub fn min_amount_received(&self) -> &BigUint {
513 &self.min_amount_received
514 }
515
516 pub fn swaps_hash(&self) -> Option<&Bytes> {
519 self.swaps_hash.as_ref()
520 }
521}
522
523#[must_use]
525#[serde_as]
526#[derive(Debug, Clone, Serialize, Deserialize)]
527#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
528pub struct EncodingOptions {
529 #[serde_as(as = "DisplayFromStr")]
530 #[cfg_attr(feature = "openapi", schema(example = "0.001"))]
531 slippage: f64,
532 #[serde(default)]
534 transfer_type: UserTransferType,
535 #[serde(default, skip_serializing_if = "Option::is_none")]
537 permit: Option<PermitSingle>,
538 #[serde(default, skip_serializing_if = "Option::is_none")]
540 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "0xabcd..."))]
541 permit2_signature: Option<Bytes>,
542 #[serde(default, skip_serializing_if = "Option::is_none")]
544 client_fee_params: Option<ClientFeeParams>,
545 #[serde(default, skip_serializing_if = "Option::is_none")]
547 price_guard: Option<PriceGuardConfig>,
548 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
550 #[cfg_attr(feature = "openapi", schema(example = false))]
551 simulate: bool,
552}
553
554impl EncodingOptions {
555 pub fn new(slippage: f64) -> Self {
557 Self {
558 slippage,
559 transfer_type: UserTransferType::default(),
560 permit: None,
561 permit2_signature: None,
562 client_fee_params: None,
563 price_guard: None,
564 simulate: false,
565 }
566 }
567
568 pub fn with_transfer_type(mut self, t: UserTransferType) -> Self {
570 self.transfer_type = t;
571 self
572 }
573
574 pub fn with_permit2(mut self, permit: PermitSingle, sig: Bytes) -> Self {
576 self.permit = Some(permit);
577 self.permit2_signature = Some(sig);
578 self
579 }
580
581 pub fn slippage(&self) -> f64 {
583 self.slippage
584 }
585
586 pub fn transfer_type(&self) -> &UserTransferType {
588 &self.transfer_type
589 }
590
591 pub fn permit(&self) -> Option<&PermitSingle> {
593 self.permit.as_ref()
594 }
595
596 pub fn permit2_signature(&self) -> Option<&Bytes> {
598 self.permit2_signature.as_ref()
599 }
600
601 pub fn with_client_fee_params(mut self, params: ClientFeeParams) -> Self {
603 self.client_fee_params = Some(params);
604 self
605 }
606
607 pub fn client_fee_params(&self) -> Option<&ClientFeeParams> {
609 self.client_fee_params.as_ref()
610 }
611
612 pub fn with_price_guard(mut self, config: PriceGuardConfig) -> Self {
614 self.price_guard = Some(config);
615 self
616 }
617
618 pub fn price_guard(&self) -> Option<&PriceGuardConfig> {
620 self.price_guard.as_ref()
621 }
622
623 pub fn with_simulation(mut self) -> Self {
625 self.simulate = true;
626 self
627 }
628
629 pub fn simulate(&self) -> bool {
631 self.simulate
632 }
633}
634
635#[serde_as]
637#[derive(Debug, Clone, Serialize, Deserialize)]
638#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
639pub struct PermitSingle {
640 details: PermitDetails,
642 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"))]
644 spender: Bytes,
645 #[serde_as(as = "DisplayFromStr")]
647 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "1893456000"))]
648 sig_deadline: BigUint,
649}
650
651impl PermitSingle {
652 pub fn new(details: PermitDetails, spender: Bytes, sig_deadline: BigUint) -> Self {
654 Self { details, spender, sig_deadline }
655 }
656
657 pub fn details(&self) -> &PermitDetails {
659 &self.details
660 }
661
662 pub fn spender(&self) -> &Bytes {
664 &self.spender
665 }
666
667 pub fn sig_deadline(&self) -> &BigUint {
669 &self.sig_deadline
670 }
671}
672
673#[serde_as]
675#[derive(Debug, Clone, Serialize, Deserialize)]
676#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
677pub struct PermitDetails {
678 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"))]
680 token: Bytes,
681 #[serde_as(as = "DisplayFromStr")]
683 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "1000000000000000000"))]
684 amount: BigUint,
685 #[serde_as(as = "DisplayFromStr")]
687 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "1893456000"))]
688 expiration: BigUint,
689 #[serde_as(as = "DisplayFromStr")]
691 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0"))]
692 nonce: BigUint,
693}
694
695impl PermitDetails {
696 pub fn new(token: Bytes, amount: BigUint, expiration: BigUint, nonce: BigUint) -> Self {
698 Self { token, amount, expiration, nonce }
699 }
700
701 pub fn token(&self) -> &Bytes {
703 &self.token
704 }
705
706 pub fn amount(&self) -> &BigUint {
708 &self.amount
709 }
710
711 pub fn expiration(&self) -> &BigUint {
713 &self.expiration
714 }
715
716 pub fn nonce(&self) -> &BigUint {
718 &self.nonce
719 }
720}
721
722#[must_use]
731#[serde_as]
732#[derive(Debug, Clone, Serialize, Deserialize)]
733#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
734pub struct Quote {
735 orders: Vec<OrderQuote>,
737 #[serde_as(as = "DisplayFromStr")]
739 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "150000"))]
740 total_gas_estimate: BigUint,
741 #[cfg_attr(feature = "openapi", schema(example = 12))]
743 solve_time_ms: u64,
744}
745
746impl Quote {
747 pub fn new(orders: Vec<OrderQuote>, total_gas_estimate: BigUint, solve_time_ms: u64) -> Self {
749 Self { orders, total_gas_estimate, solve_time_ms }
750 }
751
752 pub fn orders(&self) -> &[OrderQuote] {
754 &self.orders
755 }
756
757 pub fn into_orders(self) -> Vec<OrderQuote> {
759 self.orders
760 }
761
762 pub fn total_gas_estimate(&self) -> &BigUint {
764 &self.total_gas_estimate
765 }
766
767 pub fn solve_time_ms(&self) -> u64 {
769 self.solve_time_ms
770 }
771}
772
773#[must_use]
777#[serde_as]
778#[derive(Debug, Clone, Serialize, Deserialize)]
779#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
780pub struct Order {
781 #[serde(default = "generate_order_id", skip_deserializing)]
785 id: String,
786 #[cfg_attr(
788 feature = "openapi",
789 schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
790 )]
791 token_in: Address,
792 #[cfg_attr(
794 feature = "openapi",
795 schema(value_type = String, example = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
796 )]
797 token_out: Address,
798 #[serde_as(as = "DisplayFromStr")]
800 #[cfg_attr(
801 feature = "openapi",
802 schema(value_type = String, example = "1000000000000000000")
803 )]
804 amount: BigUint,
805 side: OrderSide,
807 #[cfg_attr(
809 feature = "openapi",
810 schema(value_type = String, example = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
811 )]
812 sender: Address,
813 #[serde(default, skip_serializing_if = "Option::is_none")]
817 #[cfg_attr(
818 feature = "openapi",
819 schema(value_type = Option<String>, example = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
820 )]
821 receiver: Option<Address>,
822}
823
824impl Order {
825 pub fn new(
827 token_in: Address,
828 token_out: Address,
829 amount: BigUint,
830 side: OrderSide,
831 sender: Address,
832 ) -> Self {
833 Self { id: String::new(), token_in, token_out, amount, side, sender, receiver: None }
834 }
835
836 pub fn with_id(mut self, id: impl Into<String>) -> Self {
838 self.id = id.into();
839 self
840 }
841
842 pub fn with_receiver(mut self, receiver: Address) -> Self {
844 self.receiver = Some(receiver);
845 self
846 }
847
848 pub fn id(&self) -> &str {
850 &self.id
851 }
852
853 pub fn token_in(&self) -> &Address {
855 &self.token_in
856 }
857
858 pub fn token_out(&self) -> &Address {
860 &self.token_out
861 }
862
863 pub fn amount(&self) -> &BigUint {
865 &self.amount
866 }
867
868 pub fn side(&self) -> OrderSide {
870 self.side
871 }
872
873 pub fn sender(&self) -> &Address {
875 &self.sender
876 }
877
878 pub fn receiver(&self) -> Option<&Address> {
880 self.receiver.as_ref()
881 }
882}
883
884#[non_exhaustive]
888#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
889#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
890#[serde(rename_all = "snake_case")]
891pub enum OrderSide {
892 Sell,
894}
895
896#[must_use]
901#[serde_as]
902#[derive(Debug, Clone, Serialize, Deserialize)]
903#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
904pub struct OrderQuote {
905 #[cfg_attr(feature = "openapi", schema(example = "f47ac10b-58cc-4372-a567-0e02b2c3d479"))]
907 order_id: String,
908 status: QuoteStatus,
910 #[serde(skip_serializing_if = "Option::is_none")]
912 route: Option<Route>,
913 #[serde_as(as = "DisplayFromStr")]
915 #[cfg_attr(
916 feature = "openapi",
917 schema(value_type = String, example = "1000000000000000000")
918 )]
919 amount_in: BigUint,
920 #[serde_as(as = "DisplayFromStr")]
922 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3500000000"))]
923 amount_out: BigUint,
924 #[serde_as(as = "DisplayFromStr")]
926 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "150000"))]
927 gas_estimate: BigUint,
928 #[serde(skip_serializing_if = "Option::is_none")]
930 price_impact_bps: Option<i32>,
931 #[serde_as(as = "DisplayFromStr")]
934 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3498000000"))]
935 amount_out_net_gas: BigUint,
936 block: BlockInfo,
938 #[serde_as(as = "Option<DisplayFromStr>")]
940 #[serde(skip_serializing_if = "Option::is_none")]
941 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "20000000000"))]
942 gas_price: Option<BigUint>,
943 transaction: Option<Transaction>,
945 #[serde(skip_serializing_if = "Option::is_none")]
947 fee_breakdown: Option<FeeBreakdown>,
948 #[serde(skip_serializing_if = "Option::is_none")]
950 simulation_result: Option<SimulationResult>,
951 #[serde(skip_serializing_if = "Option::is_none")]
955 #[cfg_attr(feature = "openapi", schema(example = "bellman_ford"))]
956 algorithm: Option<String>,
957}
958
959impl OrderQuote {
960 pub fn order_id(&self) -> &str {
962 &self.order_id
963 }
964
965 pub fn status(&self) -> QuoteStatus {
967 self.status
968 }
969
970 pub fn route(&self) -> Option<&Route> {
972 self.route.as_ref()
973 }
974
975 pub fn amount_in(&self) -> &BigUint {
977 &self.amount_in
978 }
979
980 pub fn amount_out(&self) -> &BigUint {
982 &self.amount_out
983 }
984
985 pub fn gas_estimate(&self) -> &BigUint {
987 &self.gas_estimate
988 }
989
990 pub fn price_impact_bps(&self) -> Option<i32> {
992 self.price_impact_bps
993 }
994
995 pub fn amount_out_net_gas(&self) -> &BigUint {
997 &self.amount_out_net_gas
998 }
999
1000 pub fn algorithm(&self) -> Option<&str> {
1002 self.algorithm.as_deref()
1003 }
1004
1005 pub fn block(&self) -> &BlockInfo {
1007 &self.block
1008 }
1009
1010 pub fn gas_price(&self) -> Option<&BigUint> {
1012 self.gas_price.as_ref()
1013 }
1014
1015 pub fn transaction(&self) -> Option<&Transaction> {
1017 self.transaction.as_ref()
1018 }
1019
1020 pub fn fee_breakdown(&self) -> Option<&FeeBreakdown> {
1022 self.fee_breakdown.as_ref()
1023 }
1024
1025 pub fn simulation_result(&self) -> Option<&SimulationResult> {
1027 self.simulation_result.as_ref()
1028 }
1029}
1030
1031#[serde_as]
1033#[derive(Debug, Clone, Serialize, Deserialize)]
1034#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1035#[serde(tag = "status", rename_all = "snake_case")]
1036pub enum SimulationResult {
1037 Success {
1039 #[serde_as(as = "DisplayFromStr")]
1041 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3500000000"))]
1042 amount_out: BigUint,
1043 #[cfg_attr(feature = "openapi", schema(example = 150000))]
1045 gas_used: u64,
1046 },
1047 Failure {
1049 #[cfg_attr(
1051 feature = "openapi",
1052 schema(example = "execution reverted: insufficient output")
1053 )]
1054 reason: String,
1055 },
1056}
1057
1058#[non_exhaustive]
1060#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1061#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1062#[serde(rename_all = "snake_case")]
1063pub enum QuoteStatus {
1064 Success,
1066 NoRouteFound,
1068 InsufficientLiquidity,
1070 Timeout,
1072 NotReady,
1074 PriceCheckFailed,
1076 EncodingFailed,
1079}
1080
1081#[derive(Debug, Clone, Serialize, Deserialize)]
1083#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1084pub struct BlockInfo {
1085 #[cfg_attr(feature = "openapi", schema(example = 21000000))]
1087 number: u64,
1088 #[cfg_attr(
1090 feature = "openapi",
1091 schema(example = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd")
1092 )]
1093 hash: String,
1094 #[cfg_attr(feature = "openapi", schema(example = 1730000000))]
1096 timestamp: u64,
1097}
1098
1099impl BlockInfo {
1100 pub fn new(number: u64, hash: String, timestamp: u64) -> Self {
1102 Self { number, hash, timestamp }
1103 }
1104
1105 pub fn number(&self) -> u64 {
1107 self.number
1108 }
1109
1110 pub fn hash(&self) -> &str {
1112 &self.hash
1113 }
1114
1115 pub fn timestamp(&self) -> u64 {
1117 self.timestamp
1118 }
1119}
1120
1121#[must_use]
1130#[derive(Debug, Clone, Serialize, Deserialize)]
1131#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1132pub struct Route {
1133 swaps: Vec<Swap>,
1135}
1136
1137impl Route {
1138 pub fn new(swaps: Vec<Swap>) -> Self {
1140 Self { swaps }
1141 }
1142
1143 pub fn swaps(&self) -> &[Swap] {
1145 &self.swaps
1146 }
1147
1148 pub fn into_swaps(self) -> Vec<Swap> {
1150 self.swaps
1151 }
1152}
1153
1154#[serde_as]
1158#[derive(Debug, Clone, Serialize, Deserialize)]
1159#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1160pub struct Swap {
1161 #[cfg_attr(
1163 feature = "openapi",
1164 schema(example = "0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc")
1165 )]
1166 component_id: String,
1167 #[cfg_attr(feature = "openapi", schema(example = "uniswap_v2"))]
1169 protocol: String,
1170 #[cfg_attr(
1172 feature = "openapi",
1173 schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
1174 )]
1175 token_in: Address,
1176 #[cfg_attr(
1178 feature = "openapi",
1179 schema(value_type = String, example = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
1180 )]
1181 token_out: Address,
1182 #[serde_as(as = "DisplayFromStr")]
1184 #[cfg_attr(
1185 feature = "openapi",
1186 schema(value_type = String, example = "1000000000000000000")
1187 )]
1188 amount_in: BigUint,
1189 #[serde_as(as = "DisplayFromStr")]
1191 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3500000000"))]
1192 amount_out: BigUint,
1193 #[serde_as(as = "DisplayFromStr")]
1195 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "150000"))]
1196 gas_estimate: BigUint,
1197 #[serde_as(as = "DisplayFromStr")]
1199 #[cfg_attr(feature = "openapi", schema(example = "0.0"))]
1200 split: f64,
1201}
1202
1203impl Swap {
1204 #[allow(clippy::too_many_arguments)]
1206 pub fn new(
1207 component_id: String,
1208 protocol: String,
1209 token_in: Address,
1210 token_out: Address,
1211 amount_in: BigUint,
1212 amount_out: BigUint,
1213 gas_estimate: BigUint,
1214 split: f64,
1215 ) -> Self {
1216 Self {
1217 component_id,
1218 protocol,
1219 token_in,
1220 token_out,
1221 amount_in,
1222 amount_out,
1223 gas_estimate,
1224 split,
1225 }
1226 }
1227
1228 pub fn component_id(&self) -> &str {
1230 &self.component_id
1231 }
1232
1233 pub fn protocol(&self) -> &str {
1235 &self.protocol
1236 }
1237
1238 pub fn token_in(&self) -> &Address {
1240 &self.token_in
1241 }
1242
1243 pub fn token_out(&self) -> &Address {
1245 &self.token_out
1246 }
1247
1248 pub fn amount_in(&self) -> &BigUint {
1250 &self.amount_in
1251 }
1252
1253 pub fn amount_out(&self) -> &BigUint {
1255 &self.amount_out
1256 }
1257
1258 pub fn gas_estimate(&self) -> &BigUint {
1260 &self.gas_estimate
1261 }
1262
1263 pub fn split(&self) -> f64 {
1265 self.split
1266 }
1267}
1268
1269#[derive(Debug, Clone, Serialize, Deserialize)]
1275#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1276pub struct HealthStatus {
1277 #[cfg_attr(feature = "openapi", schema(example = true))]
1279 healthy: bool,
1280 #[cfg_attr(feature = "openapi", schema(example = 1250))]
1282 last_update_ms: u64,
1283 #[cfg_attr(feature = "openapi", schema(example = 2))]
1288 num_solver_pools: usize,
1289 #[serde(default)]
1295 #[cfg_attr(feature = "openapi", schema(example = true))]
1296 derived_data_ready: bool,
1297 #[serde(default, skip_serializing_if = "Option::is_none")]
1299 #[cfg_attr(feature = "openapi", schema(example = 12000))]
1300 gas_price_age_ms: Option<u64>,
1301}
1302
1303impl HealthStatus {
1304 pub fn new(
1306 healthy: bool,
1307 last_update_ms: u64,
1308 num_solver_pools: usize,
1309 derived_data_ready: bool,
1310 gas_price_age_ms: Option<u64>,
1311 ) -> Self {
1312 Self { healthy, last_update_ms, num_solver_pools, derived_data_ready, gas_price_age_ms }
1313 }
1314
1315 pub fn healthy(&self) -> bool {
1317 self.healthy
1318 }
1319
1320 pub fn last_update_ms(&self) -> u64 {
1322 self.last_update_ms
1323 }
1324
1325 pub fn num_solver_pools(&self) -> usize {
1327 self.num_solver_pools
1328 }
1329
1330 pub fn derived_data_ready(&self) -> bool {
1332 self.derived_data_ready
1333 }
1334
1335 pub fn gas_price_age_ms(&self) -> Option<u64> {
1337 self.gas_price_age_ms
1338 }
1339}
1340
1341#[derive(Debug, Clone, Serialize, Deserialize)]
1351#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1352#[non_exhaustive]
1353pub struct InstanceInfo {
1354 #[cfg_attr(feature = "openapi", schema(example = 1))]
1356 chain_id: u64,
1357 #[cfg_attr(
1359 feature = "openapi",
1360 schema(value_type = Option<String>, example = "0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35")
1361 )]
1362 router_address: Option<Bytes>,
1363 #[cfg_attr(
1365 feature = "openapi",
1366 schema(value_type = String, example = "0x000000000022D473030F116dDEE9F6B43aC78BA3")
1367 )]
1368 permit2_address: Bytes,
1369 #[serde(default)]
1373 #[cfg_attr(feature = "openapi", schema(example = "0.89.1"))]
1374 version: String,
1375}
1376
1377impl InstanceInfo {
1378 pub fn builder(
1380 chain_id: u64,
1381 router_address: Option<Bytes>,
1382 permit2_address: Bytes,
1383 ) -> InstanceInfoBuilder {
1384 InstanceInfoBuilder { chain_id, router_address, permit2_address, version: String::new() }
1385 }
1386
1387 pub fn chain_id(&self) -> u64 {
1389 self.chain_id
1390 }
1391
1392 pub fn router_address(&self) -> Option<&Bytes> {
1394 self.router_address.as_ref()
1395 }
1396
1397 pub fn permit2_address(&self) -> &Bytes {
1399 &self.permit2_address
1400 }
1401
1402 pub fn version(&self) -> &str {
1404 &self.version
1405 }
1406}
1407
1408#[derive(Debug, Clone)]
1410pub struct InstanceInfoBuilder {
1411 chain_id: u64,
1412 router_address: Option<Bytes>,
1413 permit2_address: Bytes,
1414 version: String,
1415}
1416
1417impl InstanceInfoBuilder {
1418 pub fn version(mut self, version: impl Into<String>) -> Self {
1420 self.version = version.into();
1421 self
1422 }
1423
1424 pub fn build(self) -> InstanceInfo {
1426 InstanceInfo {
1427 chain_id: self.chain_id,
1428 router_address: self.router_address,
1429 permit2_address: self.permit2_address,
1430 version: self.version,
1431 }
1432 }
1433}
1434
1435#[must_use]
1437#[derive(Debug, Serialize, Deserialize)]
1438#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1439pub struct ErrorResponse {
1440 #[cfg_attr(feature = "openapi", schema(example = "bad request: no orders provided"))]
1441 error: String,
1442 #[cfg_attr(feature = "openapi", schema(example = "BAD_REQUEST"))]
1443 code: String,
1444 #[serde(skip_serializing_if = "Option::is_none")]
1445 details: Option<serde_json::Value>,
1446}
1447
1448impl ErrorResponse {
1449 pub fn new(error: String, code: String) -> Self {
1451 Self { error, code, details: None }
1452 }
1453
1454 pub fn with_details(mut self, details: serde_json::Value) -> Self {
1456 self.details = Some(details);
1457 self
1458 }
1459
1460 pub fn error(&self) -> &str {
1462 &self.error
1463 }
1464
1465 pub fn code(&self) -> &str {
1467 &self.code
1468 }
1469
1470 pub fn details(&self) -> Option<&serde_json::Value> {
1472 self.details.as_ref()
1473 }
1474}
1475
1476#[serde_as]
1482#[derive(Debug, Clone, Serialize, Deserialize)]
1483#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1484pub struct Transaction {
1485 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"))]
1487 to: Bytes,
1488 #[serde_as(as = "DisplayFromStr")]
1490 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0"))]
1491 value: BigUint,
1492 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0x1234567890abcdef"))]
1494 #[serde(serialize_with = "serialize_bytes_hex", deserialize_with = "deserialize_bytes_hex")]
1495 data: Vec<u8>,
1496 #[serde(default, skip_serializing_if = "Option::is_none")]
1499 #[cfg_attr(feature = "openapi", schema(example = json!(null)))]
1500 client_fee_signature_offset: Option<usize>,
1501}
1502
1503impl Transaction {
1504 pub fn new(to: Bytes, value: BigUint, data: Vec<u8>) -> Self {
1506 Self { to, value, data, client_fee_signature_offset: None }
1507 }
1508
1509 pub fn to(&self) -> &Bytes {
1511 &self.to
1512 }
1513
1514 pub fn value(&self) -> &BigUint {
1516 &self.value
1517 }
1518
1519 pub fn data(&self) -> &[u8] {
1521 &self.data
1522 }
1523
1524 pub fn client_fee_signature_offset(&self) -> Option<usize> {
1526 self.client_fee_signature_offset
1527 }
1528}
1529
1530fn serialize_bytes_hex<S>(bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
1536where
1537 S: serde::Serializer,
1538{
1539 serializer.serialize_str(&format!("0x{}", hex::encode(bytes)))
1540}
1541
1542fn deserialize_bytes_hex<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
1544where
1545 D: serde::Deserializer<'de>,
1546{
1547 let s = String::deserialize(deserializer)?;
1548 let s = s.strip_prefix("0x").unwrap_or(&s);
1549 hex::decode(s).map_err(serde::de::Error::custom)
1550}
1551
1552fn generate_order_id() -> String {
1558 Uuid::new_v4().to_string()
1559}
1560
1561#[cfg(test)]
1570mod wire_format_tests {
1571 use num_bigint::BigUint;
1572
1573 use super::*;
1574
1575 #[test]
1582 fn bytes_deserializes_without_0x_prefix() {
1583 let b: Bytes = serde_json::from_str(r#""deadbeef""#).unwrap();
1584 assert_eq!(b.as_ref(), [0xDE, 0xAD, 0xBE, 0xEF]);
1585 }
1586
1587 mod route_filter {
1589 use super::*;
1590
1591 #[test]
1592 fn test_route_filter_deserializes_from_request_json() {
1593 let json = r#"{
1594 "timeout_ms": 2000,
1595 "route_filter": {
1596 "exclude_pools": ["0xabc"],
1597 "exclude_protocols": ["uniswap_v2"],
1598 "exclude_tokens": ["0xdAC17F958D2ee523a2206206994597C13D831ec7"]
1599 }
1600 }"#;
1601
1602 let options: QuoteOptions = serde_json::from_str(json).unwrap();
1603 let filter = options.route_filter().unwrap();
1604
1605 assert_eq!(filter.excluded_pools(), ["0xabc".to_string()]);
1606 assert_eq!(filter.excluded_protocols(), ["uniswap_v2".to_string()]);
1607 assert_eq!(
1608 filter.excluded_tokens(),
1609 [Bytes::from(hex::decode("dAC17F958D2ee523a2206206994597C13D831ec7").unwrap())]
1610 );
1611 }
1612 }
1613
1614 #[test]
1621 fn order_serializes_to_full_json() {
1622 let order = Order::new(
1623 Bytes::from([0xAAu8; 20]),
1624 Bytes::from([0xBBu8; 20]),
1625 BigUint::from(1_000_000_000_000_000_000u64),
1626 OrderSide::Sell,
1627 Bytes::from([0xCCu8; 20]),
1628 )
1629 .with_id("abc");
1630
1631 assert_eq!(
1632 serde_json::to_value(&order).unwrap(),
1633 serde_json::json!({
1634 "id": "abc",
1635 "token_in": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1636 "token_out": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1637 "amount": "1000000000000000000",
1638 "side": "sell",
1639 "sender": "0xcccccccccccccccccccccccccccccccccccccccc"
1640 })
1641 );
1642 }
1643
1644 #[test]
1651 fn order_quote_deserializes_from_json() {
1652 let json = r#"{
1653 "order_id": "order-1",
1654 "status": "success",
1655 "amount_in": "1000000000000000000",
1656 "amount_out": "2000000000",
1657 "gas_estimate": "150000",
1658 "amount_out_net_gas": "1999000000",
1659 "price_impact_bps": 5,
1660 "block": { "number": 21000000, "hash": "0xdeadbeef", "timestamp": 1700000000 },
1661 "route": { "swaps": [{
1662 "component_id": "component-1",
1663 "protocol": "uniswap_v3",
1664 "token_in": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1665 "token_out": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1666 "amount_in": "1000000000000000000",
1667 "amount_out": "2000000000",
1668 "gas_estimate": "150000",
1669 "split": "0"
1670 }]}
1671 }"#;
1672
1673 let quote: OrderQuote = serde_json::from_str(json).unwrap();
1674
1675 assert_eq!(quote.status(), QuoteStatus::Success);
1676 assert_eq!(*quote.amount_in(), BigUint::from(1_000_000_000_000_000_000u64));
1677 assert_eq!(quote.price_impact_bps(), Some(5));
1678 assert_eq!(quote.block().number(), 21_000_000);
1679
1680 let swap = "e.route().unwrap().swaps()[0];
1681 assert_eq!(swap.token_in().as_ref(), [0xAAu8; 20]);
1682 assert_eq!(swap.token_out().as_ref(), [0xBBu8; 20]);
1683 assert_eq!(swap.split(), 0.0);
1684 }
1685
1686 #[test]
1693 fn encoding_options_serializes_to_full_json() {
1694 assert_eq!(
1695 serde_json::to_value(EncodingOptions::new(0.005)).unwrap(),
1696 serde_json::json!({
1697 "slippage": "0.005",
1698 "transfer_type": "transfer_from"
1699 })
1700 );
1701 }
1702
1703 #[test]
1710 fn instance_info_deserializes_and_ignores_unknown_fields() {
1711 let json = r#"{
1712 "version": "1.2.3",
1713 "chain_id": 1,
1714 "router_address": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1715 "permit2_address": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1716 "future_field": "ignored"
1717 }"#;
1718
1719 let info: InstanceInfo = serde_json::from_str(json).unwrap();
1720 assert_eq!(info.version(), "1.2.3");
1721 assert_eq!(info.chain_id(), 1);
1722 assert_eq!(info.router_address().unwrap().as_ref(), [0xAAu8; 20]);
1723 assert_eq!(info.permit2_address().as_ref(), [0xBBu8; 20]);
1724 }
1725
1726 #[test]
1727 fn instance_info_builder_sets_fields() {
1728 let info =
1729 InstanceInfo::builder(1, Some(Bytes::from([0xAAu8; 20])), Bytes::from([0xBBu8; 20]))
1730 .version("0.1.0")
1731 .build();
1732
1733 assert_eq!(info.version(), "0.1.0");
1734 assert_eq!(info.chain_id(), 1);
1735 assert_eq!(info.router_address().unwrap().as_ref(), [0xAAu8; 20]);
1736 assert_eq!(info.permit2_address().as_ref(), [0xBBu8; 20]);
1737 }
1738
1739 #[test]
1740 fn instance_info_deserializes_without_version() {
1741 let json = r#"{
1743 "chain_id": 1,
1744 "router_address": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1745 "permit2_address": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
1746 }"#;
1747
1748 let info: InstanceInfo = serde_json::from_str(json).unwrap();
1749 assert_eq!(info.version(), "");
1750 assert_eq!(info.chain_id(), 1);
1751 }
1752}
1753
1754#[cfg(feature = "core")]
1765mod conversions {
1766 use tycho_simulation::tycho_core::Bytes as TychoBytes;
1767
1768 use super::*;
1769
1770 impl From<TychoBytes> for Bytes {
1776 fn from(b: TychoBytes) -> Self {
1777 Self(b.0)
1778 }
1779 }
1780
1781 impl From<Bytes> for TychoBytes {
1782 fn from(b: Bytes) -> Self {
1783 Self(b.0)
1784 }
1785 }
1786
1787 impl Into<fynd_core::QuoteRequest> for QuoteRequest {
1792 fn into(self) -> fynd_core::QuoteRequest {
1793 fynd_core::QuoteRequest::new(
1794 self.orders
1795 .into_iter()
1796 .map(Into::into)
1797 .collect(),
1798 self.options.into(),
1799 )
1800 }
1801 }
1802
1803 impl Into<fynd_core::QuoteOptions> for QuoteOptions {
1804 fn into(self) -> fynd_core::QuoteOptions {
1805 let mut opts = fynd_core::QuoteOptions::default();
1806 if let Some(ms) = self.timeout_ms {
1807 opts = opts.with_timeout_ms(ms);
1808 }
1809 if let Some(n) = self.min_responses {
1810 opts = opts.with_min_responses(n);
1811 }
1812 if let Some(gas) = self.max_gas {
1813 opts = opts.with_max_gas(gas);
1814 }
1815 if let Some(enc) = self.encoding_options {
1816 opts = opts.with_encoding_options(enc.into());
1817 }
1818 if let Some(filter) = self.route_filter {
1819 opts = opts.with_route_filter(filter.into());
1820 }
1821 opts
1822 }
1823 }
1824
1825 impl Into<fynd_core::RouteExclusionFilter> for RouteFilter {
1826 fn into(self) -> fynd_core::RouteExclusionFilter {
1827 fynd_core::RouteExclusionFilter::default()
1828 .with_excluded_pools(self.exclude_pools)
1829 .with_excluded_protocols(self.exclude_protocols)
1830 .with_excluded_tokens(
1831 self.exclude_tokens
1832 .into_iter()
1833 .map(Into::into),
1834 )
1835 }
1836 }
1837
1838 impl Into<fynd_core::PriceGuardConfig> for PriceGuardConfig {
1839 fn into(self) -> fynd_core::PriceGuardConfig {
1840 let mut config = fynd_core::PriceGuardConfig::default();
1841 if let Some(bps) = self.lower_tolerance_bps {
1842 config = config.with_lower_tolerance_bps(bps);
1843 }
1844 if let Some(bps) = self.upper_tolerance_bps {
1845 config = config.with_upper_tolerance_bps(bps);
1846 }
1847 if let Some(fail) = self.fail_on_provider_error {
1848 config = config.with_fail_on_provider_error(fail);
1849 }
1850 if let Some(fail) = self.fail_on_token_price_not_found {
1851 config = config.with_fail_on_token_price_not_found(fail);
1852 }
1853 if let Some(enabled) = self.enabled {
1854 config = config.with_enabled(enabled);
1855 }
1856 config
1857 }
1858 }
1859
1860 impl Into<fynd_core::EncodingOptions> for EncodingOptions {
1861 fn into(self) -> fynd_core::EncodingOptions {
1862 let mut opts = fynd_core::EncodingOptions::new(self.slippage)
1863 .with_transfer_type(self.transfer_type.into());
1864 if let (Some(permit), Some(sig)) = (self.permit, self.permit2_signature) {
1865 opts = opts
1866 .with_permit(permit.into())
1867 .with_signature(sig.into());
1868 }
1869 if let Some(fee) = self.client_fee_params {
1870 opts = opts.with_client_fee_params(fee.into());
1871 }
1872 if let Some(pg) = self.price_guard {
1873 opts = opts.with_price_guard(pg.into());
1874 }
1875 if self.simulate {
1876 opts = opts.with_simulation();
1877 }
1878 opts
1879 }
1880 }
1881
1882 impl Into<fynd_core::ClientFeeParams> for ClientFeeParams {
1883 fn into(self) -> fynd_core::ClientFeeParams {
1884 fynd_core::ClientFeeParams::new(
1885 self.bps,
1886 self.receiver.into(),
1887 self.max_contribution,
1888 self.deadline,
1889 self.signature.into(),
1890 )
1891 }
1892 }
1893
1894 impl Into<fynd_core::UserTransferType> for UserTransferType {
1895 fn into(self) -> fynd_core::UserTransferType {
1896 match self {
1897 UserTransferType::TransferFromPermit2 => {
1898 fynd_core::UserTransferType::TransferFromPermit2
1899 }
1900 UserTransferType::TransferFrom => fynd_core::UserTransferType::TransferFrom,
1901 UserTransferType::UseVaultsFunds => fynd_core::UserTransferType::UseVaultsFunds,
1902 }
1903 }
1904 }
1905
1906 impl Into<fynd_core::PermitSingle> for PermitSingle {
1907 fn into(self) -> fynd_core::PermitSingle {
1908 fynd_core::PermitSingle::new(
1909 self.details.into(),
1910 self.spender.into(),
1911 self.sig_deadline,
1912 )
1913 }
1914 }
1915
1916 impl Into<fynd_core::PermitDetails> for PermitDetails {
1917 fn into(self) -> fynd_core::PermitDetails {
1918 fynd_core::PermitDetails::new(
1919 self.token.into(),
1920 self.amount,
1921 self.expiration,
1922 self.nonce,
1923 )
1924 }
1925 }
1926
1927 impl Into<fynd_core::Order> for Order {
1928 fn into(self) -> fynd_core::Order {
1929 let mut order = fynd_core::Order::new(
1930 self.token_in.into(),
1931 self.token_out.into(),
1932 self.amount,
1933 self.side.into(),
1934 self.sender.into(),
1935 )
1936 .with_id(self.id);
1937 if let Some(r) = self.receiver {
1938 order = order.with_receiver(r.into());
1939 }
1940 order
1941 }
1942 }
1943
1944 impl Into<fynd_core::OrderSide> for OrderSide {
1945 fn into(self) -> fynd_core::OrderSide {
1946 match self {
1947 OrderSide::Sell => fynd_core::OrderSide::Sell,
1948 }
1949 }
1950 }
1951
1952 impl From<fynd_core::Quote> for Quote {
1957 fn from(core: fynd_core::Quote) -> Self {
1958 let solve_time_ms = core.solve_time_ms();
1959 let total_gas_estimate = core.total_gas_estimate().clone();
1960 Self {
1961 orders: core
1962 .into_orders()
1963 .into_iter()
1964 .map(Into::into)
1965 .collect(),
1966 total_gas_estimate,
1967 solve_time_ms,
1968 }
1969 }
1970 }
1971
1972 impl From<fynd_core::OrderQuote> for OrderQuote {
1973 fn from(core: fynd_core::OrderQuote) -> Self {
1978 let order_id = core.order_id().to_string();
1979 let status = core.status().into();
1980 let amount_in = core.amount_in().clone();
1981 let amount_out = core.amount_out().clone();
1982 let gas_estimate = core.gas_estimate().clone();
1983 let price_impact_bps = core.price_impact_bps();
1984 let amount_out_net_gas = core.amount_out_net_gas().clone();
1985 let block = core.block().clone().into();
1986 let gas_price = core.gas_price().cloned();
1987 let transaction = core
1988 .transaction()
1989 .cloned()
1990 .map(Into::into);
1991 let fee_breakdown = core
1992 .fee_breakdown()
1993 .cloned()
1994 .map(Into::into);
1995 let simulation_result = core
1996 .simulation_result()
1997 .cloned()
1998 .map(Into::into);
1999 let algorithm = (!core.algorithm().is_empty()).then(|| core.algorithm().to_string());
2000 let route = core.into_route().map(Into::into);
2001 Self {
2002 order_id,
2003 status,
2004 route,
2005 amount_in,
2006 amount_out,
2007 gas_estimate,
2008 price_impact_bps,
2009 amount_out_net_gas,
2010 block,
2011 gas_price,
2012 transaction,
2013 fee_breakdown,
2014 simulation_result,
2015 algorithm,
2016 }
2017 }
2018 }
2019
2020 impl From<fynd_core::QuoteStatus> for QuoteStatus {
2021 fn from(core: fynd_core::QuoteStatus) -> Self {
2022 match core {
2023 fynd_core::QuoteStatus::Success => Self::Success,
2024 fynd_core::QuoteStatus::NoRouteFound => Self::NoRouteFound,
2025 fynd_core::QuoteStatus::InsufficientLiquidity => Self::InsufficientLiquidity,
2026 fynd_core::QuoteStatus::Timeout => Self::Timeout,
2027 fynd_core::QuoteStatus::NotReady => Self::NotReady,
2028 fynd_core::QuoteStatus::PriceCheckFailed => Self::PriceCheckFailed,
2029 fynd_core::QuoteStatus::EncodingFailed => Self::EncodingFailed,
2030 _ => Self::NotReady,
2032 }
2033 }
2034 }
2035
2036 impl From<fynd_core::BlockInfo> for BlockInfo {
2037 fn from(core: fynd_core::BlockInfo) -> Self {
2038 Self {
2039 number: core.number(),
2040 hash: core.hash().to_string(),
2041 timestamp: core.timestamp(),
2042 }
2043 }
2044 }
2045
2046 impl From<fynd_core::Route> for Route {
2047 fn from(core: fynd_core::Route) -> Self {
2048 Self {
2049 swaps: core
2050 .into_swaps()
2051 .into_iter()
2052 .map(Into::into)
2053 .collect(),
2054 }
2055 }
2056 }
2057
2058 impl From<fynd_core::Swap> for Swap {
2059 fn from(core: fynd_core::Swap) -> Self {
2060 Self {
2061 component_id: core.component_id().to_string(),
2062 protocol: core.protocol().to_string(),
2063 token_in: core.token_in().clone().into(),
2064 token_out: core.token_out().clone().into(),
2065 amount_in: core.amount_in().clone(),
2066 amount_out: core.amount_out().clone(),
2067 gas_estimate: core.gas_estimate().clone(),
2068 split: *core.split(),
2069 }
2070 }
2071 }
2072
2073 impl From<fynd_core::Transaction> for Transaction {
2074 fn from(core: fynd_core::Transaction) -> Self {
2075 Self {
2076 to: core.to().clone().into(),
2077 value: core.value().clone(),
2078 data: core.data().to_vec(),
2079 client_fee_signature_offset: core.client_fee_signature_offset(),
2080 }
2081 }
2082 }
2083
2084 impl From<fynd_core::FeeBreakdown> for FeeBreakdown {
2085 fn from(core: fynd_core::FeeBreakdown) -> Self {
2086 let swaps_hash = core
2087 .swaps_hash()
2088 .map(|h| Bytes(bytes::Bytes::copy_from_slice(h.as_ref())));
2089 Self {
2090 router_fee: core.router_fee().clone(),
2091 client_fee: core.client_fee().clone(),
2092 max_slippage: core.max_slippage().clone(),
2093 min_amount_received: core.min_amount_received().clone(),
2094 swaps_hash,
2095 }
2096 }
2097 }
2098
2099 impl From<fynd_core::SimulationResult> for SimulationResult {
2100 fn from(core: fynd_core::SimulationResult) -> Self {
2101 match core {
2102 fynd_core::SimulationResult::Success { amount_out, gas_used } => {
2103 Self::Success { amount_out, gas_used }
2104 }
2105 fynd_core::SimulationResult::Failure { reason } => Self::Failure { reason },
2106 }
2107 }
2108 }
2109
2110 #[cfg(test)]
2111 mod tests {
2112 use num_bigint::BigUint;
2113
2114 use super::*;
2115
2116 fn make_address(byte: u8) -> Address {
2117 Address::from([byte; 20])
2118 }
2119
2120 #[test]
2121 fn test_quote_request_roundtrip() {
2122 let dto = QuoteRequest {
2123 orders: vec![Order {
2124 id: "test-id".to_string(),
2125 token_in: make_address(0x01),
2126 token_out: make_address(0x02),
2127 amount: BigUint::from(1000u64),
2128 side: OrderSide::Sell,
2129 sender: make_address(0xAA),
2130 receiver: None,
2131 }],
2132 options: QuoteOptions {
2133 timeout_ms: Some(5000),
2134 min_responses: None,
2135 max_gas: None,
2136 encoding_options: None,
2137 route_filter: None,
2138 },
2139 };
2140
2141 let core: fynd_core::QuoteRequest = dto.clone().into();
2142 assert_eq!(core.orders().len(), 1);
2143 assert_eq!(core.orders()[0].id(), "test-id");
2144 assert_eq!(core.options().timeout_ms(), Some(5000));
2145 }
2146
2147 #[test]
2148 fn test_quote_from_core() {
2149 let core: fynd_core::Quote = serde_json::from_str(
2150 r#"{"orders":[],"total_gas_estimate":"100000","solve_time_ms":50}"#,
2151 )
2152 .unwrap();
2153
2154 let dto = Quote::from(core);
2155 assert_eq!(dto.total_gas_estimate, BigUint::from(100_000u64));
2156 assert_eq!(dto.solve_time_ms, 50);
2157 }
2158
2159 #[test]
2161 fn test_route_filter_into_core() {
2162 let usdt = make_address(0xDA);
2163 let dto = QuoteOptions::default().with_route_filter(
2164 RouteFilter::default()
2165 .with_excluded_pools(["pool-1".to_string()])
2166 .with_excluded_protocols(["uniswap_v2".to_string()])
2167 .with_excluded_tokens([usdt.clone()]),
2168 );
2169
2170 let core: fynd_core::QuoteOptions = dto.into();
2171
2172 let expected = fynd_core::RouteExclusionFilter::default()
2173 .with_excluded_pools(["pool-1".to_string()])
2174 .with_excluded_protocols(["uniswap_v2".to_string()])
2175 .with_excluded_tokens([TychoBytes::from(usdt)]);
2176 assert_eq!(core.route_filter(), &expected);
2177 }
2178
2179 #[test]
2180 fn test_order_side_into_core() {
2181 let core: fynd_core::OrderSide = OrderSide::Sell.into();
2182 assert_eq!(core, fynd_core::OrderSide::Sell);
2183 }
2184
2185 #[test]
2186 fn test_client_fee_params_into_core() {
2187 let dto = ClientFeeParams::new(
2188 200,
2189 Bytes::from(make_address(0xBB).as_ref()),
2190 BigUint::from(1_000_000u64),
2191 1_893_456_000u64,
2192 Bytes::from(vec![0xABu8; 65]),
2193 );
2194 let core: fynd_core::ClientFeeParams = dto.into();
2195 assert_eq!(core.bps(), 200);
2196 assert_eq!(*core.max_contribution(), BigUint::from(1_000_000u64));
2197 assert_eq!(core.deadline(), 1_893_456_000u64);
2198 assert_eq!(core.signature().len(), 65);
2199 }
2200
2201 #[test]
2202 fn test_encoding_options_with_client_fee_into_core() {
2203 let fee = ClientFeeParams::new(
2204 100,
2205 Bytes::from(make_address(0xCC).as_ref()),
2206 BigUint::from(500u64),
2207 9_999u64,
2208 Bytes::from(vec![0xDEu8; 65]),
2209 );
2210 let dto = EncodingOptions::new(0.005).with_client_fee_params(fee);
2211 let core: fynd_core::EncodingOptions = dto.into();
2212
2213 assert!(core.client_fee_params().is_some());
2214 let core_fee = core.client_fee_params().unwrap();
2215 assert_eq!(core_fee.bps(), 100);
2216 assert_eq!(*core_fee.max_contribution(), BigUint::from(500u64));
2217 }
2218
2219 #[test]
2222 fn test_encoding_options_into_core_never_disables_slippage_taking() {
2223 let json = serde_json::json!({
2224 "slippage": "0.005",
2225 "disable_slippage_taking": true
2226 });
2227 let dto: EncodingOptions = serde_json::from_value(json).expect("unknown field ignored");
2228
2229 let core: fynd_core::EncodingOptions = dto.into();
2230
2231 assert!(!core.disable_slippage_taking());
2232 }
2233
2234 #[test]
2235 fn test_client_fee_params_serde_roundtrip() {
2236 let fee = ClientFeeParams::new(
2237 150,
2238 Bytes::from(make_address(0xDD).as_ref()),
2239 BigUint::from(999_999u64),
2240 1_700_000_000u64,
2241 Bytes::from(vec![0xFFu8; 65]),
2242 );
2243 let json = serde_json::to_string(&fee).unwrap();
2244 assert!(json.contains(r#""max_contribution":"999999""#));
2245 assert!(json.contains(r#""deadline":1700000000"#));
2246
2247 let deserialized: ClientFeeParams = serde_json::from_str(&json).unwrap();
2248 assert_eq!(deserialized.bps(), 150);
2249 assert_eq!(*deserialized.max_contribution(), BigUint::from(999_999u64));
2250 }
2251
2252 #[test]
2253 fn test_price_guard_config_into_core() {
2254 let dto = PriceGuardConfig::default()
2255 .with_lower_tolerance_bps(200)
2256 .with_upper_tolerance_bps(5000)
2257 .with_fail_on_provider_error(false)
2258 .with_enabled(false);
2259
2260 let config: fynd_core::PriceGuardConfig = dto.into();
2261 assert_eq!(config.lower_tolerance_bps(), 200);
2262 assert_eq!(config.upper_tolerance_bps(), 5000);
2263 assert!(!config.fail_on_provider_error());
2264 assert!(!config.enabled());
2265 }
2266
2267 #[test]
2268 fn test_encoding_options_with_price_guard_roundtrip() {
2269 let enc = EncodingOptions::new(0.01)
2270 .with_price_guard(PriceGuardConfig::default().with_enabled(false));
2271 let dto = QuoteRequest {
2272 orders: vec![Order {
2273 id: "pg-test".to_string(),
2274 token_in: make_address(0x01),
2275 token_out: make_address(0x02),
2276 amount: BigUint::from(1000u64),
2277 side: OrderSide::Sell,
2278 sender: make_address(0xAA),
2279 receiver: None,
2280 }],
2281 options: QuoteOptions::default().with_encoding_options(enc),
2282 };
2283
2284 let core: fynd_core::QuoteRequest = dto.into();
2285 let config = core
2286 .options()
2287 .encoding_options()
2288 .expect("encoding_options should be set")
2289 .price_guard();
2290 assert!(!config.enabled());
2291 }
2292
2293 #[test]
2294 fn test_encoding_options_omits_disabled_simulation() {
2295 let json = serde_json::to_string(&EncodingOptions::new(0.01)).unwrap();
2296 assert!(!json.contains("simulate"));
2297 }
2298
2299 #[test]
2300 fn test_simulation_result_success_serde_roundtrip() {
2301 let result = SimulationResult::Success {
2302 amount_out: BigUint::from(3_500_000_000_u64),
2303 gas_used: 150_000,
2304 };
2305 let json = serde_json::to_string(&result).unwrap();
2306 assert_eq!(json, r#"{"status":"success","amount_out":"3500000000","gas_used":150000}"#);
2307 let decoded: SimulationResult = serde_json::from_str(&json).unwrap();
2308 assert!(
2309 matches!(decoded, SimulationResult::Success { amount_out, gas_used } if amount_out == BigUint::from(3_500_000_000_u64) && gas_used == 150_000)
2310 );
2311 }
2312
2313 #[test]
2314 fn test_simulation_result_failure_serde_roundtrip() {
2315 let result = SimulationResult::Failure { reason: "execution reverted".to_string() };
2316 let json = serde_json::to_string(&result).unwrap();
2317 assert_eq!(json, r#"{"status":"failure","reason":"execution reverted"}"#);
2318 let decoded: SimulationResult = serde_json::from_str(&json).unwrap();
2319 assert!(
2320 matches!(decoded, SimulationResult::Failure { reason } if reason == "execution reverted")
2321 );
2322 }
2323
2324 #[test]
2325 fn test_quote_status_from_core() {
2326 let cases = [
2327 (fynd_core::QuoteStatus::Success, QuoteStatus::Success),
2328 (fynd_core::QuoteStatus::NoRouteFound, QuoteStatus::NoRouteFound),
2329 (fynd_core::QuoteStatus::InsufficientLiquidity, QuoteStatus::InsufficientLiquidity),
2330 (fynd_core::QuoteStatus::Timeout, QuoteStatus::Timeout),
2331 (fynd_core::QuoteStatus::NotReady, QuoteStatus::NotReady),
2332 (fynd_core::QuoteStatus::EncodingFailed, QuoteStatus::EncodingFailed),
2333 ];
2334
2335 for (core, expected) in cases {
2336 assert_eq!(QuoteStatus::from(core), expected);
2337 }
2338 }
2339 }
2340}