1use num_bigint::BigUint;
11use serde::{Deserialize, Serialize};
12use serde_with::{serde_as, DisplayFromStr};
13use tycho_simulation::{tycho_common::models::Address, tycho_core::Bytes};
14use uuid::Uuid;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
22#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
23pub struct QuoteRequest {
24 orders: Vec<Order>,
26 #[serde(default)]
28 options: QuoteOptions,
29}
30
31impl QuoteRequest {
32 pub fn new(orders: Vec<Order>) -> Self {
34 Self { orders, options: QuoteOptions::default() }
35 }
36
37 pub fn with_options(mut self, options: QuoteOptions) -> Self {
39 self.options = options;
40 self
41 }
42
43 pub fn orders(&self) -> &[Order] {
45 &self.orders
46 }
47
48 pub fn options(&self) -> &QuoteOptions {
50 &self.options
51 }
52}
53
54#[serde_as]
56#[derive(Debug, Clone, Default, Serialize, Deserialize)]
57#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
58pub struct QuoteOptions {
59 #[cfg_attr(feature = "openapi", schema(example = 2000))]
61 timeout_ms: Option<u64>,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
68 min_responses: Option<usize>,
69 #[serde_as(as = "Option<DisplayFromStr>")]
71 #[serde(default, skip_serializing_if = "Option::is_none")]
72 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "500000"))]
73 max_gas: Option<BigUint>,
74 encoding_options: Option<EncodingOptions>,
76}
77
78impl QuoteOptions {
79 pub fn with_timeout_ms(mut self, ms: u64) -> Self {
81 self.timeout_ms = Some(ms);
82 self
83 }
84
85 pub fn with_min_responses(mut self, n: usize) -> Self {
87 self.min_responses = Some(n);
88 self
89 }
90
91 pub fn with_max_gas(mut self, gas: BigUint) -> Self {
93 self.max_gas = Some(gas);
94 self
95 }
96
97 pub fn with_encoding_options(mut self, opts: EncodingOptions) -> Self {
99 self.encoding_options = Some(opts);
100 self
101 }
102
103 pub fn timeout_ms(&self) -> Option<u64> {
105 self.timeout_ms
106 }
107
108 pub fn min_responses(&self) -> Option<usize> {
110 self.min_responses
111 }
112
113 pub fn max_gas(&self) -> Option<&BigUint> {
115 self.max_gas.as_ref()
116 }
117
118 pub fn encoding_options(&self) -> Option<&EncodingOptions> {
120 self.encoding_options.as_ref()
121 }
122}
123
124#[non_exhaustive]
126#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
127#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
128#[serde(rename_all = "snake_case")]
129pub enum UserTransferType {
130 TransferFromPermit2,
132 #[default]
134 TransferFrom,
135 UseVaultsFunds,
137}
138
139#[serde_as]
144#[derive(Debug, Clone, Serialize, Deserialize)]
145#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
146pub struct ClientFeeParams {
147 #[cfg_attr(feature = "openapi", schema(example = 100))]
149 bps: u16,
150 #[cfg_attr(
152 feature = "openapi",
153 schema(value_type = String, example = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
154 )]
155 receiver: Bytes,
156 #[serde_as(as = "DisplayFromStr")]
158 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0"))]
159 max_contribution: BigUint,
160 #[cfg_attr(feature = "openapi", schema(example = 1893456000))]
162 deadline: u64,
163 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0xabcd..."))]
165 signature: Bytes,
166}
167
168impl ClientFeeParams {
169 pub fn new(
171 bps: u16,
172 receiver: Bytes,
173 max_contribution: BigUint,
174 deadline: u64,
175 signature: Bytes,
176 ) -> Self {
177 Self { bps, receiver, max_contribution, deadline, signature }
178 }
179
180 pub fn bps(&self) -> u16 {
182 self.bps
183 }
184
185 pub fn receiver(&self) -> &Bytes {
187 &self.receiver
188 }
189
190 pub fn max_contribution(&self) -> &BigUint {
192 &self.max_contribution
193 }
194
195 pub fn deadline(&self) -> u64 {
197 self.deadline
198 }
199
200 pub fn signature(&self) -> &Bytes {
202 &self.signature
203 }
204}
205
206#[serde_as]
210#[derive(Debug, Clone, Serialize, Deserialize)]
211#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
212pub struct FeeBreakdown {
213 #[serde_as(as = "DisplayFromStr")]
215 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "350000"))]
216 router_fee: BigUint,
217 #[serde_as(as = "DisplayFromStr")]
219 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "2800000"))]
220 client_fee: BigUint,
221 #[serde_as(as = "DisplayFromStr")]
223 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3496850"))]
224 max_slippage: BigUint,
225 #[serde_as(as = "DisplayFromStr")]
228 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3493353150"))]
229 min_amount_received: BigUint,
230}
231
232impl FeeBreakdown {
233 pub fn router_fee(&self) -> &BigUint {
235 &self.router_fee
236 }
237
238 pub fn client_fee(&self) -> &BigUint {
240 &self.client_fee
241 }
242
243 pub fn max_slippage(&self) -> &BigUint {
245 &self.max_slippage
246 }
247
248 pub fn min_amount_received(&self) -> &BigUint {
250 &self.min_amount_received
251 }
252}
253
254#[serde_as]
256#[derive(Debug, Clone, Serialize, Deserialize)]
257#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
258pub struct EncodingOptions {
259 #[serde_as(as = "DisplayFromStr")]
260 #[cfg_attr(feature = "openapi", schema(example = "0.001"))]
261 slippage: f64,
262 #[serde(default)]
264 transfer_type: UserTransferType,
265 #[serde(default, skip_serializing_if = "Option::is_none")]
267 permit: Option<PermitSingle>,
268 #[serde(default, skip_serializing_if = "Option::is_none")]
270 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "0xabcd..."))]
271 permit2_signature: Option<Bytes>,
272 #[serde(default, skip_serializing_if = "Option::is_none")]
274 client_fee_params: Option<ClientFeeParams>,
275}
276
277impl EncodingOptions {
278 pub fn new(slippage: f64) -> Self {
280 Self {
281 slippage,
282 transfer_type: UserTransferType::default(),
283 permit: None,
284 permit2_signature: None,
285 client_fee_params: None,
286 }
287 }
288
289 pub fn with_transfer_type(mut self, t: UserTransferType) -> Self {
291 self.transfer_type = t;
292 self
293 }
294
295 pub fn with_permit2(mut self, permit: PermitSingle, sig: Bytes) -> Self {
297 self.permit = Some(permit);
298 self.permit2_signature = Some(sig);
299 self
300 }
301
302 pub fn slippage(&self) -> f64 {
304 self.slippage
305 }
306
307 pub fn transfer_type(&self) -> &UserTransferType {
309 &self.transfer_type
310 }
311
312 pub fn permit(&self) -> Option<&PermitSingle> {
314 self.permit.as_ref()
315 }
316
317 pub fn permit2_signature(&self) -> Option<&Bytes> {
319 self.permit2_signature.as_ref()
320 }
321
322 pub fn with_client_fee_params(mut self, params: ClientFeeParams) -> Self {
324 self.client_fee_params = Some(params);
325 self
326 }
327
328 pub fn client_fee_params(&self) -> Option<&ClientFeeParams> {
330 self.client_fee_params.as_ref()
331 }
332}
333
334#[serde_as]
336#[derive(Debug, Clone, Serialize, Deserialize)]
337#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
338pub struct PermitSingle {
339 details: PermitDetails,
341 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"))]
343 spender: Bytes,
344 #[serde_as(as = "DisplayFromStr")]
346 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "1893456000"))]
347 sig_deadline: BigUint,
348}
349
350impl PermitSingle {
351 pub fn new(details: PermitDetails, spender: Bytes, sig_deadline: BigUint) -> Self {
353 Self { details, spender, sig_deadline }
354 }
355
356 pub fn details(&self) -> &PermitDetails {
358 &self.details
359 }
360
361 pub fn spender(&self) -> &Bytes {
363 &self.spender
364 }
365
366 pub fn sig_deadline(&self) -> &BigUint {
368 &self.sig_deadline
369 }
370}
371
372#[serde_as]
374#[derive(Debug, Clone, Serialize, Deserialize)]
375#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
376pub struct PermitDetails {
377 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"))]
379 token: Bytes,
380 #[serde_as(as = "DisplayFromStr")]
382 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "1000000000000000000"))]
383 amount: BigUint,
384 #[serde_as(as = "DisplayFromStr")]
386 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "1893456000"))]
387 expiration: BigUint,
388 #[serde_as(as = "DisplayFromStr")]
390 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0"))]
391 nonce: BigUint,
392}
393
394impl PermitDetails {
395 pub fn new(token: Bytes, amount: BigUint, expiration: BigUint, nonce: BigUint) -> Self {
397 Self { token, amount, expiration, nonce }
398 }
399
400 pub fn token(&self) -> &Bytes {
402 &self.token
403 }
404
405 pub fn amount(&self) -> &BigUint {
407 &self.amount
408 }
409
410 pub fn expiration(&self) -> &BigUint {
412 &self.expiration
413 }
414
415 pub fn nonce(&self) -> &BigUint {
417 &self.nonce
418 }
419}
420
421#[must_use]
430#[serde_as]
431#[derive(Debug, Clone, Serialize, Deserialize)]
432#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
433pub struct Quote {
434 orders: Vec<OrderQuote>,
436 #[serde_as(as = "DisplayFromStr")]
438 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "150000"))]
439 total_gas_estimate: BigUint,
440 #[cfg_attr(feature = "openapi", schema(example = 12))]
442 solve_time_ms: u64,
443}
444
445impl Quote {
446 pub fn new(orders: Vec<OrderQuote>, total_gas_estimate: BigUint, solve_time_ms: u64) -> Self {
448 Self { orders, total_gas_estimate, solve_time_ms }
449 }
450
451 pub fn orders(&self) -> &[OrderQuote] {
453 &self.orders
454 }
455
456 pub fn into_orders(self) -> Vec<OrderQuote> {
458 self.orders
459 }
460
461 pub fn total_gas_estimate(&self) -> &BigUint {
463 &self.total_gas_estimate
464 }
465
466 pub fn solve_time_ms(&self) -> u64 {
468 self.solve_time_ms
469 }
470}
471
472#[serde_as]
476#[derive(Debug, Clone, Serialize, Deserialize)]
477#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
478pub struct Order {
479 #[serde(default = "generate_order_id", skip_deserializing)]
483 id: String,
484 #[cfg_attr(
486 feature = "openapi",
487 schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
488 )]
489 token_in: Address,
490 #[cfg_attr(
492 feature = "openapi",
493 schema(value_type = String, example = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
494 )]
495 token_out: Address,
496 #[serde_as(as = "DisplayFromStr")]
498 #[cfg_attr(
499 feature = "openapi",
500 schema(value_type = String, example = "1000000000000000000")
501 )]
502 amount: BigUint,
503 side: OrderSide,
505 #[cfg_attr(
507 feature = "openapi",
508 schema(value_type = String, example = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
509 )]
510 sender: Address,
511 #[serde(default, skip_serializing_if = "Option::is_none")]
515 #[cfg_attr(
516 feature = "openapi",
517 schema(value_type = Option<String>, example = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
518 )]
519 receiver: Option<Address>,
520}
521
522impl Order {
523 pub fn new(
525 token_in: Address,
526 token_out: Address,
527 amount: BigUint,
528 side: OrderSide,
529 sender: Address,
530 ) -> Self {
531 Self { id: String::new(), token_in, token_out, amount, side, sender, receiver: None }
532 }
533
534 pub fn with_id(mut self, id: impl Into<String>) -> Self {
536 self.id = id.into();
537 self
538 }
539
540 pub fn with_receiver(mut self, receiver: Address) -> Self {
542 self.receiver = Some(receiver);
543 self
544 }
545
546 pub fn id(&self) -> &str {
548 &self.id
549 }
550
551 pub fn token_in(&self) -> &Address {
553 &self.token_in
554 }
555
556 pub fn token_out(&self) -> &Address {
558 &self.token_out
559 }
560
561 pub fn amount(&self) -> &BigUint {
563 &self.amount
564 }
565
566 pub fn side(&self) -> OrderSide {
568 self.side
569 }
570
571 pub fn sender(&self) -> &Address {
573 &self.sender
574 }
575
576 pub fn receiver(&self) -> Option<&Address> {
578 self.receiver.as_ref()
579 }
580}
581
582#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
586#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
587#[serde(rename_all = "snake_case")]
588pub enum OrderSide {
589 Sell,
591}
592
593#[must_use]
598#[serde_as]
599#[derive(Debug, Clone, Serialize, Deserialize)]
600#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
601pub struct OrderQuote {
602 #[cfg_attr(feature = "openapi", schema(example = "f47ac10b-58cc-4372-a567-0e02b2c3d479"))]
604 order_id: String,
605 status: QuoteStatus,
607 #[serde(skip_serializing_if = "Option::is_none")]
609 route: Option<Route>,
610 #[serde_as(as = "DisplayFromStr")]
612 #[cfg_attr(
613 feature = "openapi",
614 schema(value_type = String, example = "1000000000000000000")
615 )]
616 amount_in: BigUint,
617 #[serde_as(as = "DisplayFromStr")]
619 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3500000000"))]
620 amount_out: BigUint,
621 #[serde_as(as = "DisplayFromStr")]
623 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "150000"))]
624 gas_estimate: BigUint,
625 #[serde(skip_serializing_if = "Option::is_none")]
627 price_impact_bps: Option<i32>,
628 #[serde_as(as = "DisplayFromStr")]
631 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3498000000"))]
632 amount_out_net_gas: BigUint,
633 block: BlockInfo,
635 #[serde_as(as = "Option<DisplayFromStr>")]
637 #[serde(skip_serializing_if = "Option::is_none")]
638 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "20000000000"))]
639 gas_price: Option<BigUint>,
640 transaction: Option<Transaction>,
642 #[serde(skip_serializing_if = "Option::is_none")]
644 fee_breakdown: Option<FeeBreakdown>,
645}
646
647impl OrderQuote {
648 pub fn order_id(&self) -> &str {
650 &self.order_id
651 }
652
653 pub fn status(&self) -> QuoteStatus {
655 self.status
656 }
657
658 pub fn route(&self) -> Option<&Route> {
660 self.route.as_ref()
661 }
662
663 pub fn amount_in(&self) -> &BigUint {
665 &self.amount_in
666 }
667
668 pub fn amount_out(&self) -> &BigUint {
670 &self.amount_out
671 }
672
673 pub fn gas_estimate(&self) -> &BigUint {
675 &self.gas_estimate
676 }
677
678 pub fn price_impact_bps(&self) -> Option<i32> {
680 self.price_impact_bps
681 }
682
683 pub fn amount_out_net_gas(&self) -> &BigUint {
685 &self.amount_out_net_gas
686 }
687
688 pub fn block(&self) -> &BlockInfo {
690 &self.block
691 }
692
693 pub fn gas_price(&self) -> Option<&BigUint> {
695 self.gas_price.as_ref()
696 }
697
698 pub fn transaction(&self) -> Option<&Transaction> {
700 self.transaction.as_ref()
701 }
702
703 pub fn fee_breakdown(&self) -> Option<&FeeBreakdown> {
705 self.fee_breakdown.as_ref()
706 }
707}
708
709#[non_exhaustive]
711#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
712#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
713#[serde(rename_all = "snake_case")]
714pub enum QuoteStatus {
715 Success,
717 NoRouteFound,
719 InsufficientLiquidity,
721 Timeout,
723 NotReady,
725}
726
727#[derive(Debug, Clone, Serialize, Deserialize)]
732#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
733pub struct BlockInfo {
734 #[cfg_attr(feature = "openapi", schema(example = 21000000))]
736 number: u64,
737 #[cfg_attr(
739 feature = "openapi",
740 schema(example = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd")
741 )]
742 hash: String,
743 #[cfg_attr(feature = "openapi", schema(example = 1730000000))]
745 timestamp: u64,
746}
747
748impl BlockInfo {
749 pub fn new(number: u64, hash: String, timestamp: u64) -> Self {
751 Self { number, hash, timestamp }
752 }
753
754 pub fn number(&self) -> u64 {
756 self.number
757 }
758
759 pub fn hash(&self) -> &str {
761 &self.hash
762 }
763
764 pub fn timestamp(&self) -> u64 {
766 self.timestamp
767 }
768}
769
770#[must_use]
779#[derive(Debug, Clone, Serialize, Deserialize)]
780#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
781pub struct Route {
782 swaps: Vec<Swap>,
784}
785
786impl Route {
787 pub fn new(swaps: Vec<Swap>) -> Self {
789 Self { swaps }
790 }
791
792 pub fn swaps(&self) -> &[Swap] {
794 &self.swaps
795 }
796
797 pub fn into_swaps(self) -> Vec<Swap> {
799 self.swaps
800 }
801}
802
803#[serde_as]
807#[derive(Debug, Clone, Serialize, Deserialize)]
808#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
809pub struct Swap {
810 #[cfg_attr(
812 feature = "openapi",
813 schema(example = "0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc")
814 )]
815 component_id: String,
816 #[cfg_attr(feature = "openapi", schema(example = "uniswap_v2"))]
818 protocol: String,
819 #[cfg_attr(
821 feature = "openapi",
822 schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
823 )]
824 token_in: Address,
825 #[cfg_attr(
827 feature = "openapi",
828 schema(value_type = String, example = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
829 )]
830 token_out: Address,
831 #[serde_as(as = "DisplayFromStr")]
833 #[cfg_attr(
834 feature = "openapi",
835 schema(value_type = String, example = "1000000000000000000")
836 )]
837 amount_in: BigUint,
838 #[serde_as(as = "DisplayFromStr")]
840 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "3500000000"))]
841 amount_out: BigUint,
842 #[serde_as(as = "DisplayFromStr")]
844 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "150000"))]
845 gas_estimate: BigUint,
846 #[serde_as(as = "DisplayFromStr")]
848 #[cfg_attr(feature = "openapi", schema(example = "0.0"))]
849 split: f64,
850}
851
852impl Swap {
853 #[allow(clippy::too_many_arguments)]
855 pub fn new(
856 component_id: String,
857 protocol: String,
858 token_in: Address,
859 token_out: Address,
860 amount_in: BigUint,
861 amount_out: BigUint,
862 gas_estimate: BigUint,
863 split: f64,
864 ) -> Self {
865 Self {
866 component_id,
867 protocol,
868 token_in,
869 token_out,
870 amount_in,
871 amount_out,
872 gas_estimate,
873 split,
874 }
875 }
876
877 pub fn component_id(&self) -> &str {
879 &self.component_id
880 }
881
882 pub fn protocol(&self) -> &str {
884 &self.protocol
885 }
886
887 pub fn token_in(&self) -> &Address {
889 &self.token_in
890 }
891
892 pub fn token_out(&self) -> &Address {
894 &self.token_out
895 }
896
897 pub fn amount_in(&self) -> &BigUint {
899 &self.amount_in
900 }
901
902 pub fn amount_out(&self) -> &BigUint {
904 &self.amount_out
905 }
906
907 pub fn gas_estimate(&self) -> &BigUint {
909 &self.gas_estimate
910 }
911
912 pub fn split(&self) -> f64 {
914 self.split
915 }
916}
917
918#[derive(Debug, Clone, Serialize, Deserialize)]
924#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
925pub struct HealthStatus {
926 #[cfg_attr(feature = "openapi", schema(example = true))]
928 healthy: bool,
929 #[cfg_attr(feature = "openapi", schema(example = 1250))]
931 last_update_ms: u64,
932 #[cfg_attr(feature = "openapi", schema(example = 2))]
934 num_solver_pools: usize,
935 #[serde(default)]
941 #[cfg_attr(feature = "openapi", schema(example = true))]
942 derived_data_ready: bool,
943 #[serde(default, skip_serializing_if = "Option::is_none")]
945 #[cfg_attr(feature = "openapi", schema(example = 12000))]
946 gas_price_age_ms: Option<u64>,
947}
948
949impl HealthStatus {
950 pub fn new(
952 healthy: bool,
953 last_update_ms: u64,
954 num_solver_pools: usize,
955 derived_data_ready: bool,
956 gas_price_age_ms: Option<u64>,
957 ) -> Self {
958 Self { healthy, last_update_ms, num_solver_pools, derived_data_ready, gas_price_age_ms }
959 }
960
961 pub fn healthy(&self) -> bool {
963 self.healthy
964 }
965
966 pub fn last_update_ms(&self) -> u64 {
968 self.last_update_ms
969 }
970
971 pub fn num_solver_pools(&self) -> usize {
973 self.num_solver_pools
974 }
975
976 pub fn derived_data_ready(&self) -> bool {
978 self.derived_data_ready
979 }
980
981 pub fn gas_price_age_ms(&self) -> Option<u64> {
983 self.gas_price_age_ms
984 }
985}
986
987#[derive(Debug, Clone, Serialize, Deserialize)]
989#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
990pub struct InstanceInfo {
991 #[cfg_attr(feature = "openapi", schema(example = 1))]
993 chain_id: u64,
994 #[cfg_attr(
996 feature = "openapi",
997 schema(value_type = String, example = "0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35")
998 )]
999 router_address: Bytes,
1000 #[cfg_attr(
1002 feature = "openapi",
1003 schema(value_type = String, example = "0x000000000022D473030F116dDEE9F6B43aC78BA3")
1004 )]
1005 permit2_address: Bytes,
1006}
1007
1008impl InstanceInfo {
1009 pub fn new(chain_id: u64, router_address: Bytes, permit2_address: Bytes) -> Self {
1011 Self { chain_id, router_address, permit2_address }
1012 }
1013
1014 pub fn chain_id(&self) -> u64 {
1016 self.chain_id
1017 }
1018
1019 pub fn router_address(&self) -> &Bytes {
1021 &self.router_address
1022 }
1023
1024 pub fn permit2_address(&self) -> &Bytes {
1026 &self.permit2_address
1027 }
1028}
1029
1030#[derive(Debug, Serialize, Deserialize)]
1032#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1033pub struct ErrorResponse {
1034 #[cfg_attr(feature = "openapi", schema(example = "bad request: no orders provided"))]
1035 error: String,
1036 #[cfg_attr(feature = "openapi", schema(example = "BAD_REQUEST"))]
1037 code: String,
1038 #[serde(skip_serializing_if = "Option::is_none")]
1039 details: Option<serde_json::Value>,
1040}
1041
1042impl ErrorResponse {
1043 pub fn new(error: String, code: String) -> Self {
1045 Self { error, code, details: None }
1046 }
1047
1048 pub fn with_details(mut self, details: serde_json::Value) -> Self {
1050 self.details = Some(details);
1051 self
1052 }
1053
1054 pub fn error(&self) -> &str {
1056 &self.error
1057 }
1058
1059 pub fn code(&self) -> &str {
1061 &self.code
1062 }
1063
1064 pub fn details(&self) -> Option<&serde_json::Value> {
1066 self.details.as_ref()
1067 }
1068}
1069
1070#[serde_as]
1076#[derive(Debug, Clone, Serialize, Deserialize)]
1077#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1078pub struct Transaction {
1079 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"))]
1081 to: Bytes,
1082 #[serde_as(as = "DisplayFromStr")]
1084 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0"))]
1085 value: BigUint,
1086 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "0x1234567890abcdef"))]
1088 #[serde(serialize_with = "serialize_bytes_hex", deserialize_with = "deserialize_bytes_hex")]
1089 data: Vec<u8>,
1090}
1091
1092impl Transaction {
1093 pub fn new(to: Bytes, value: BigUint, data: Vec<u8>) -> Self {
1095 Self { to, value, data }
1096 }
1097
1098 pub fn to(&self) -> &Bytes {
1100 &self.to
1101 }
1102
1103 pub fn value(&self) -> &BigUint {
1105 &self.value
1106 }
1107
1108 pub fn data(&self) -> &[u8] {
1110 &self.data
1111 }
1112}
1113
1114fn serialize_bytes_hex<S>(bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
1120where
1121 S: serde::Serializer,
1122{
1123 serializer.serialize_str(&format!("0x{}", hex::encode(bytes)))
1124}
1125
1126fn deserialize_bytes_hex<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
1128where
1129 D: serde::Deserializer<'de>,
1130{
1131 let s = String::deserialize(deserializer)?;
1132 let s = s.strip_prefix("0x").unwrap_or(&s);
1133 hex::decode(s).map_err(serde::de::Error::custom)
1134}
1135
1136fn generate_order_id() -> String {
1142 Uuid::new_v4().to_string()
1143}
1144
1145#[cfg(feature = "core")]
1156mod conversions {
1157 use super::*;
1158
1159 impl Into<fynd_core::QuoteRequest> for QuoteRequest {
1164 fn into(self) -> fynd_core::QuoteRequest {
1165 fynd_core::QuoteRequest::new(
1166 self.orders
1167 .into_iter()
1168 .map(Into::into)
1169 .collect(),
1170 self.options.into(),
1171 )
1172 }
1173 }
1174
1175 impl Into<fynd_core::QuoteOptions> for QuoteOptions {
1176 fn into(self) -> fynd_core::QuoteOptions {
1177 let mut opts = fynd_core::QuoteOptions::default();
1178 if let Some(ms) = self.timeout_ms {
1179 opts = opts.with_timeout_ms(ms);
1180 }
1181 if let Some(n) = self.min_responses {
1182 opts = opts.with_min_responses(n);
1183 }
1184 if let Some(gas) = self.max_gas {
1185 opts = opts.with_max_gas(gas);
1186 }
1187 if let Some(enc) = self.encoding_options {
1188 opts = opts.with_encoding_options(enc.into());
1189 }
1190 opts
1191 }
1192 }
1193
1194 impl Into<fynd_core::EncodingOptions> for EncodingOptions {
1195 fn into(self) -> fynd_core::EncodingOptions {
1196 let mut opts = fynd_core::EncodingOptions::new(self.slippage)
1197 .with_transfer_type(self.transfer_type.into());
1198 if let (Some(permit), Some(sig)) = (self.permit, self.permit2_signature) {
1199 opts = opts
1200 .with_permit(permit.into())
1201 .with_signature(sig);
1202 }
1203 if let Some(fee) = self.client_fee_params {
1204 opts = opts.with_client_fee_params(fee.into());
1205 }
1206 opts
1207 }
1208 }
1209
1210 impl Into<fynd_core::ClientFeeParams> for ClientFeeParams {
1211 fn into(self) -> fynd_core::ClientFeeParams {
1212 fynd_core::ClientFeeParams::new(
1213 self.bps,
1214 self.receiver,
1215 self.max_contribution,
1216 self.deadline,
1217 self.signature,
1218 )
1219 }
1220 }
1221
1222 impl Into<fynd_core::UserTransferType> for UserTransferType {
1223 fn into(self) -> fynd_core::UserTransferType {
1224 match self {
1225 UserTransferType::TransferFromPermit2 => {
1226 fynd_core::UserTransferType::TransferFromPermit2
1227 }
1228 UserTransferType::TransferFrom => fynd_core::UserTransferType::TransferFrom,
1229 UserTransferType::UseVaultsFunds => fynd_core::UserTransferType::UseVaultsFunds,
1230 }
1231 }
1232 }
1233
1234 impl Into<fynd_core::PermitSingle> for PermitSingle {
1235 fn into(self) -> fynd_core::PermitSingle {
1236 fynd_core::PermitSingle::new(self.details.into(), self.spender, self.sig_deadline)
1237 }
1238 }
1239
1240 impl Into<fynd_core::PermitDetails> for PermitDetails {
1241 fn into(self) -> fynd_core::PermitDetails {
1242 fynd_core::PermitDetails::new(self.token, self.amount, self.expiration, self.nonce)
1243 }
1244 }
1245
1246 impl Into<fynd_core::Order> for Order {
1247 fn into(self) -> fynd_core::Order {
1248 let mut order = fynd_core::Order::new(
1249 self.token_in,
1250 self.token_out,
1251 self.amount,
1252 self.side.into(),
1253 self.sender,
1254 )
1255 .with_id(self.id);
1256 if let Some(r) = self.receiver {
1257 order = order.with_receiver(r);
1258 }
1259 order
1260 }
1261 }
1262
1263 impl Into<fynd_core::OrderSide> for OrderSide {
1264 fn into(self) -> fynd_core::OrderSide {
1265 match self {
1266 OrderSide::Sell => fynd_core::OrderSide::Sell,
1267 }
1268 }
1269 }
1270
1271 impl From<fynd_core::Quote> for Quote {
1276 fn from(core: fynd_core::Quote) -> Self {
1277 let solve_time_ms = core.solve_time_ms();
1278 let total_gas_estimate = core.total_gas_estimate().clone();
1279 Self {
1280 orders: core
1281 .into_orders()
1282 .into_iter()
1283 .map(Into::into)
1284 .collect(),
1285 total_gas_estimate,
1286 solve_time_ms,
1287 }
1288 }
1289 }
1290
1291 impl From<fynd_core::OrderQuote> for OrderQuote {
1292 fn from(core: fynd_core::OrderQuote) -> Self {
1293 let order_id = core.order_id().to_string();
1294 let status = core.status().into();
1295 let amount_in = core.amount_in().clone();
1296 let amount_out = core.amount_out().clone();
1297 let gas_estimate = core.gas_estimate().clone();
1298 let price_impact_bps = core.price_impact_bps();
1299 let amount_out_net_gas = core.amount_out_net_gas().clone();
1300 let block = core.block().clone().into();
1301 let gas_price = core.gas_price().cloned();
1302 let transaction = core
1303 .transaction()
1304 .cloned()
1305 .map(Into::into);
1306 let fee_breakdown = core
1307 .fee_breakdown()
1308 .cloned()
1309 .map(Into::into);
1310 let route = core.into_route().map(Into::into);
1311 Self {
1312 order_id,
1313 status,
1314 route,
1315 amount_in,
1316 amount_out,
1317 gas_estimate,
1318 price_impact_bps,
1319 amount_out_net_gas,
1320 block,
1321 gas_price,
1322 transaction,
1323 fee_breakdown,
1324 }
1325 }
1326 }
1327
1328 impl From<fynd_core::QuoteStatus> for QuoteStatus {
1329 fn from(core: fynd_core::QuoteStatus) -> Self {
1330 match core {
1331 fynd_core::QuoteStatus::Success => Self::Success,
1332 fynd_core::QuoteStatus::NoRouteFound => Self::NoRouteFound,
1333 fynd_core::QuoteStatus::InsufficientLiquidity => Self::InsufficientLiquidity,
1334 fynd_core::QuoteStatus::Timeout => Self::Timeout,
1335 fynd_core::QuoteStatus::NotReady => Self::NotReady,
1336 _ => Self::NotReady,
1338 }
1339 }
1340 }
1341
1342 impl From<fynd_core::BlockInfo> for BlockInfo {
1343 fn from(core: fynd_core::BlockInfo) -> Self {
1344 Self {
1345 number: core.number(),
1346 hash: core.hash().to_string(),
1347 timestamp: core.timestamp(),
1348 }
1349 }
1350 }
1351
1352 impl From<fynd_core::Route> for Route {
1353 fn from(core: fynd_core::Route) -> Self {
1354 Self {
1355 swaps: core
1356 .into_swaps()
1357 .into_iter()
1358 .map(Into::into)
1359 .collect(),
1360 }
1361 }
1362 }
1363
1364 impl From<fynd_core::Swap> for Swap {
1365 fn from(core: fynd_core::Swap) -> Self {
1366 Self {
1367 component_id: core.component_id().to_string(),
1368 protocol: core.protocol().to_string(),
1369 token_in: core.token_in().clone(),
1370 token_out: core.token_out().clone(),
1371 amount_in: core.amount_in().clone(),
1372 amount_out: core.amount_out().clone(),
1373 gas_estimate: core.gas_estimate().clone(),
1374 split: *core.split(),
1375 }
1376 }
1377 }
1378
1379 impl From<fynd_core::Transaction> for Transaction {
1380 fn from(core: fynd_core::Transaction) -> Self {
1381 Self { to: core.to().clone(), value: core.value().clone(), data: core.data().to_vec() }
1382 }
1383 }
1384
1385 impl From<fynd_core::FeeBreakdown> for FeeBreakdown {
1386 fn from(core: fynd_core::FeeBreakdown) -> Self {
1387 Self {
1388 router_fee: core.router_fee().clone(),
1389 client_fee: core.client_fee().clone(),
1390 max_slippage: core.max_slippage().clone(),
1391 min_amount_received: core.min_amount_received().clone(),
1392 }
1393 }
1394 }
1395
1396 #[cfg(test)]
1397 mod tests {
1398 use num_bigint::BigUint;
1399 use tycho_simulation::tycho_common::models::Address;
1400
1401 use super::*;
1402
1403 fn make_address(byte: u8) -> Address {
1404 Address::from([byte; 20])
1405 }
1406
1407 #[test]
1408 fn test_quote_request_roundtrip() {
1409 let dto = QuoteRequest {
1410 orders: vec![Order {
1411 id: "test-id".to_string(),
1412 token_in: make_address(0x01),
1413 token_out: make_address(0x02),
1414 amount: BigUint::from(1000u64),
1415 side: OrderSide::Sell,
1416 sender: make_address(0xAA),
1417 receiver: None,
1418 }],
1419 options: QuoteOptions {
1420 timeout_ms: Some(5000),
1421 min_responses: None,
1422 max_gas: None,
1423 encoding_options: None,
1424 },
1425 };
1426
1427 let core: fynd_core::QuoteRequest = dto.clone().into();
1428 assert_eq!(core.orders().len(), 1);
1429 assert_eq!(core.orders()[0].id(), "test-id");
1430 assert_eq!(core.options().timeout_ms(), Some(5000));
1431 }
1432
1433 #[test]
1434 fn test_quote_from_core() {
1435 let core: fynd_core::Quote = serde_json::from_str(
1436 r#"{"orders":[],"total_gas_estimate":"100000","solve_time_ms":50}"#,
1437 )
1438 .unwrap();
1439
1440 let dto = Quote::from(core);
1441 assert_eq!(dto.total_gas_estimate, BigUint::from(100_000u64));
1442 assert_eq!(dto.solve_time_ms, 50);
1443 }
1444
1445 #[test]
1446 fn test_order_side_into_core() {
1447 let core: fynd_core::OrderSide = OrderSide::Sell.into();
1448 assert_eq!(core, fynd_core::OrderSide::Sell);
1449 }
1450
1451 #[test]
1452 fn test_client_fee_params_into_core() {
1453 use tycho_simulation::tycho_core::Bytes as TychoBytes;
1454
1455 let dto = ClientFeeParams::new(
1456 200,
1457 TychoBytes::from(make_address(0xBB).as_ref()),
1458 BigUint::from(1_000_000u64),
1459 1_893_456_000u64,
1460 TychoBytes::from(vec![0xAB; 65]),
1461 );
1462 let core: fynd_core::ClientFeeParams = dto.into();
1463 assert_eq!(core.bps(), 200);
1464 assert_eq!(*core.max_contribution(), BigUint::from(1_000_000u64));
1465 assert_eq!(core.deadline(), 1_893_456_000u64);
1466 assert_eq!(core.signature().len(), 65);
1467 }
1468
1469 #[test]
1470 fn test_encoding_options_with_client_fee_into_core() {
1471 use tycho_simulation::tycho_core::Bytes as TychoBytes;
1472
1473 let fee = ClientFeeParams::new(
1474 100,
1475 TychoBytes::from(make_address(0xCC).as_ref()),
1476 BigUint::from(500u64),
1477 9_999u64,
1478 TychoBytes::from(vec![0xDE; 65]),
1479 );
1480 let dto = EncodingOptions::new(0.005).with_client_fee_params(fee);
1481 let core: fynd_core::EncodingOptions = dto.into();
1482
1483 assert!(core.client_fee_params().is_some());
1484 let core_fee = core.client_fee_params().unwrap();
1485 assert_eq!(core_fee.bps(), 100);
1486 assert_eq!(*core_fee.max_contribution(), BigUint::from(500u64));
1487 }
1488
1489 #[test]
1490 fn test_client_fee_params_serde_roundtrip() {
1491 use tycho_simulation::tycho_core::Bytes as TychoBytes;
1492
1493 let fee = ClientFeeParams::new(
1494 150,
1495 TychoBytes::from(make_address(0xDD).as_ref()),
1496 BigUint::from(999_999u64),
1497 1_700_000_000u64,
1498 TychoBytes::from(vec![0xFF; 65]),
1499 );
1500 let json = serde_json::to_string(&fee).unwrap();
1501 assert!(json.contains(r#""max_contribution":"999999""#));
1502 assert!(json.contains(r#""deadline":1700000000"#));
1503
1504 let deserialized: ClientFeeParams = serde_json::from_str(&json).unwrap();
1505 assert_eq!(deserialized.bps(), 150);
1506 assert_eq!(*deserialized.max_contribution(), BigUint::from(999_999u64));
1507 }
1508
1509 #[test]
1510 fn test_quote_status_from_core() {
1511 let cases = [
1512 (fynd_core::QuoteStatus::Success, QuoteStatus::Success),
1513 (fynd_core::QuoteStatus::NoRouteFound, QuoteStatus::NoRouteFound),
1514 (fynd_core::QuoteStatus::InsufficientLiquidity, QuoteStatus::InsufficientLiquidity),
1515 (fynd_core::QuoteStatus::Timeout, QuoteStatus::Timeout),
1516 (fynd_core::QuoteStatus::NotReady, QuoteStatus::NotReady),
1517 ];
1518
1519 for (core, expected) in cases {
1520 assert_eq!(QuoteStatus::from(core), expected);
1521 }
1522 }
1523 }
1524}