tycho_common/simulation/swap.rs
1use std::{collections::HashMap, fmt, fmt::Debug, sync::Arc};
2
3use itertools::Itertools;
4use num_bigint::BigUint;
5
6use crate::{
7 dto::ProtocolStateDelta,
8 models::{protocol::ProtocolComponent, token::Token},
9 simulation::{
10 errors::{SimulationError, TransitionError},
11 indicatively_priced::IndicativelyPriced,
12 protocol_sim::{Balances, Price, ProtocolSim},
13 },
14 Bytes,
15};
16
17/// Result type for swap simulation operations that may fail with a `SimulationError`.
18pub type SimulationResult<T> = Result<T, SimulationError>;
19
20/// Type alias for token addresses, represented as raw bytes.
21pub type TokenAddress = Bytes;
22
23/// Macro that generates parameter structs with embedded blockchain context.
24///
25/// This macro creates structs that automatically include a `Context` field and provides
26/// methods for managing blockchain context (block number and timestamp). All parameter
27/// structs used in swap simulations should be created with this macro to ensure consistent
28/// context handling.
29///
30/// # Generated Methods
31/// - `with_context(context: Context) -> Self` - Sets the blockchain context
32/// - `context() -> &Context` - Gets a reference to the current context
33///
34/// # Example
35/// ```rust
36/// params_with_context! {
37/// pub struct MyParams {
38/// token: TokenAddress,
39/// amount: BigUint,
40/// }
41/// }
42/// ```
43macro_rules! params_with_context {
44 (
45 $(#[$meta:meta])*
46 $vis:vis struct $name:ident $(<$($gen:tt),*>)? {
47 $($field:ident: $ty:ty),* $(,)?
48 }
49 ) => {
50 $(#[$meta])*
51 #[derive(Debug, Clone)]
52 $vis struct $name $(<$($gen),*>)? {
53 context: Context,
54 $($field: $ty,)*
55 }
56
57 impl $(<$($gen),*>)? $name $(<$($gen),*>)? {
58
59 pub fn with_context(mut self, context: Context) -> Self {
60 self.context = context;
61 self
62 }
63
64 pub fn context(&self) -> &Context {
65 &self.context
66 }
67 }
68 };
69}
70
71/// Blockchain context information used in swap simulations.
72///
73/// Contains optional future block information that can be used for time-sensitive
74/// simulations or to simulate swaps at specific future blockchain states.
75#[derive(Debug, Clone)]
76pub struct Context {}
77
78impl Default for Context {
79 /// Creates a new `Context` with no future block information.
80 fn default() -> Self {
81 Self {}
82 }
83}
84
85params_with_context! {
86/// Parameters for requesting a swap quote from a pool.
87///
88/// Contains the tokens to swap between, the input amount, and whether the
89/// simulation should return a modified state to reflect the swap execution.
90pub struct QuoteParams<'a>{
91 token_in: &'a TokenAddress,
92 token_out: &'a TokenAddress,
93 amount: QuoteAmount,
94 return_new_state: bool,
95 }
96}
97
98#[derive(Debug, Clone)]
99pub enum QuoteAmount {
100 FixedIn(BigUint),
101 FixedOut(BigUint),
102}
103
104impl<'a> QuoteParams<'a> {
105 /// Creates new fixed input parameters with default settings (no state modification).
106 ///
107 /// # Arguments
108 /// * `token_in` - The token to sell
109 /// * `token_out` - The token to buy
110 /// * `amount` - The amount of input token to sell
111 pub fn fixed_in(
112 token_in: &'a TokenAddress,
113 token_out: &'a TokenAddress,
114 amount: BigUint,
115 ) -> SimulationResult<Self> {
116 if token_out == token_in {
117 return Err(SimulationError::InvalidInput(
118 "Quote tokens have to differ!".to_string(),
119 None,
120 ))
121 }
122 Ok(Self {
123 context: Context::default(),
124 token_in,
125 token_out,
126 amount: QuoteAmount::FixedIn(amount),
127 return_new_state: false,
128 })
129 }
130
131 /// Creates new fixed output parameters with default settings (no state modification).
132 ///
133 /// # Arguments
134 /// * `token_in` - The token to sell
135 /// * `token_out` - The token to buy
136 /// * `amount` - The amount of output token to buy
137 pub fn fixed_out(
138 token_in: &'a TokenAddress,
139 token_out: &'a TokenAddress,
140 amount: BigUint,
141 ) -> SimulationResult<Self> {
142 if token_out == token_in {
143 return Err(SimulationError::InvalidInput(
144 "Quote tokens have to differ!".to_string(),
145 None,
146 ))
147 }
148
149 Ok(Self {
150 context: Context::default(),
151 token_in,
152 token_out,
153 amount: QuoteAmount::FixedOut(amount),
154 return_new_state: false,
155 })
156 }
157
158 pub fn amount(&self) -> &QuoteAmount {
159 &self.amount
160 }
161
162 /// Configures the quote to modify the state during simulation.
163 ///
164 /// When enabled, the quote simulation will return an updated state
165 /// as if the swap was actually executed.
166 pub fn with_new_state(mut self) -> Self {
167 self.return_new_state = true;
168 self
169 }
170
171 /// Returns the input token address.
172 pub fn token_in(&self) -> &TokenAddress {
173 self.token_in
174 }
175
176 /// Returns the output token address.
177 pub fn token_out(&self) -> &TokenAddress {
178 self.token_out
179 }
180
181 /// Returns whether the simulation should return the post-swap state.
182 pub fn should_return_new_state(&self) -> bool {
183 self.return_new_state
184 }
185}
186
187params_with_context! {
188/// Parameters for querying swap limits from a pool.
189///
190/// Used to determine the minimum and maximum amounts that can be traded
191/// between two tokens in the pool.
192pub struct LimitsParams<'a> {
193 token_in: &'a TokenAddress,
194 token_out: &'a TokenAddress,
195}
196}
197
198impl<'a> LimitsParams<'a> {
199 /// Creates new parameters for querying swap limits.
200 ///
201 /// # Arguments
202 /// * `token_in` - The input token address
203 /// * `token_out` - The output token address
204 pub fn new(token_in: &'a TokenAddress, token_out: &'a TokenAddress) -> Self {
205 Self { context: Context::default(), token_in, token_out }
206 }
207
208 pub fn token_in(&self) -> &TokenAddress {
209 self.token_in
210 }
211
212 pub fn token_out(&self) -> &TokenAddress {
213 self.token_out
214 }
215}
216
217params_with_context! {
218/// Parameters for querying the spot price between two tokens.
219///
220/// Used to get the current marginal price for infinitesimally small trades
221/// between two tokens in a pool.
222pub struct MarginalPriceParams<'a> {
223 token_in: &'a TokenAddress,
224 token_out: &'a TokenAddress,
225}
226}
227
228impl<'a> MarginalPriceParams<'a> {
229 /// Creates new parameters for querying marginal price.
230 ///
231 /// # Arguments
232 /// * `token_in` - The input token address
233 /// * `token_out` - The output token address
234 pub fn new(token_in: &'a TokenAddress, token_out: &'a TokenAddress) -> Self {
235 Self { context: Context::default(), token_in, token_out }
236 }
237 pub fn token_in(&self) -> &TokenAddress {
238 self.token_in
239 }
240
241 pub fn token_out(&self) -> &TokenAddress {
242 self.token_out
243 }
244}
245
246/// Represents a swap fee as a fraction.
247///
248/// The fee is typically expressed as a decimal (e.g., 0.003 for 0.3%).
249pub struct SwapFee {
250 fee: f64,
251}
252
253impl SwapFee {
254 /// Creates a new swap fee.
255 ///
256 /// # Arguments
257 /// * `fee` - The fee as a decimal fraction (e.g., 0.003 for 0.3%)
258 pub fn new(fee: f64) -> Self {
259 Self { fee }
260 }
261
262 /// Returns the fee as a decimal fraction.
263 pub fn fee(&self) -> f64 {
264 self.fee
265 }
266}
267
268/// Represents the marginal price at which the next infinitesimal trade would execute.
269///
270/// This is the instantaneous price for very small trades at the current pool state,
271/// often different from the effective price of larger trades due to slippage.
272pub struct MarginalPrice {
273 price: f64,
274}
275
276impl MarginalPrice {
277 /// Creates a new marginal price.
278 ///
279 /// # Arguments
280 /// * `price` - The marginal price as token_out/token_in ratio
281 pub fn new(price: f64) -> Self {
282 Self { price }
283 }
284
285 /// Returns the marginal price value.
286 pub fn price(&self) -> f64 {
287 self.price
288 }
289}
290
291/// Result of a swap quote calculation.
292///
293/// Contains the expected output amount, gas cost, and optionally the new pool state
294/// if the quote was requested with state modification enabled.
295pub struct Quote {
296 amount_out: BigUint,
297 gas: BigUint,
298 new_state: Option<Arc<dyn SwapQuoter>>,
299}
300
301impl Quote {
302 /// Creates a new quote result.
303 ///
304 /// # Arguments
305 /// * `amount_out` - The amount of output tokens that would be received
306 /// * `gas` - The estimated gas cost for executing this swap, excluding token transfers cost
307 /// * `new_state` - The new pool state after the swap (if state modification was requested)
308 pub fn new(amount_out: BigUint, gas: BigUint, new_state: Option<Arc<dyn SwapQuoter>>) -> Self {
309 Self { amount_out, gas, new_state }
310 }
311
312 /// Returns the amount of output tokens.
313 pub fn amount_out(&self) -> &BigUint {
314 &self.amount_out
315 }
316
317 /// Returns the estimated swap gas cost excluding including token transfer cost.
318 pub fn gas(&self) -> &BigUint {
319 &self.gas
320 }
321
322 /// Returns the new pool state after the swap, if available.
323 pub fn new_state(&self) -> Option<Arc<dyn SwapQuoter>> {
324 self.new_state.clone()
325 }
326}
327
328/// Represents a numeric range with lower and upper bounds.
329///
330/// Used for specifying trading limits and constraints.
331pub struct Range {
332 lower: BigUint,
333 upper: BigUint,
334}
335
336impl Range {
337 /// Creates a new range with validation.
338 ///
339 /// # Arguments
340 /// * `lower` - The lower bound (must be <= upper)
341 /// * `upper` - The upper bound (must be >= lower)
342 ///
343 /// # Errors
344 /// Returns `SimulationError::InvalidInput` if lower > upper.
345 pub fn new(lower: BigUint, upper: BigUint) -> SimulationResult<Self> {
346 if lower > upper {
347 return Err(SimulationError::InvalidInput(
348 "Invalid range! Argument lower > upper".to_string(),
349 None,
350 ))
351 }
352 Ok(Self { lower, upper })
353 }
354
355 /// Returns the lower bound.
356 pub fn lower(&self) -> &BigUint {
357 &self.lower
358 }
359
360 /// Returns the upper bound.
361 pub fn upper(&self) -> &BigUint {
362 &self.upper
363 }
364}
365
366/// Defines the trading limits for input and output amounts in a swap.
367///
368/// Specifies the minimum and maximum amounts that can be traded through a pool
369/// for both input and output tokens.
370pub struct SwapLimits {
371 range_in: Range,
372 range_out: Range,
373}
374
375impl SwapLimits {
376 /// Creates new swap limits.
377 ///
378 /// # Arguments
379 /// * `range_in` - The valid range for input token amounts
380 /// * `range_out` - The valid range for output token amounts
381 pub fn new(range_in: Range, range_out: Range) -> Self {
382 Self { range_in, range_out }
383 }
384
385 /// Returns the input amount limits.
386 pub fn range_in(&self) -> &Range {
387 &self.range_in
388 }
389
390 /// Returns the output amount limits.
391 pub fn range_out(&self) -> &Range {
392 &self.range_out
393 }
394}
395
396params_with_context! {
397/// Parameters for applying protocol state transitions.
398///
399/// Contains the state delta and associated data needed to transition
400/// a pool's state in response to blockchain events.
401pub struct TransitionParams<'a> {
402 delta: ProtocolStateDelta,
403 tokens: &'a HashMap<Bytes, Token>,
404 balances: &'a Balances,
405 }
406}
407
408impl<'a> TransitionParams<'a> {
409 /// Creates new parameters for state transition.
410 ///
411 /// # Arguments
412 /// * `delta` - The protocol state change to apply
413 /// * `tokens` - Map of token addresses to token metadata
414 /// * `balances` - Current token balances in the system
415 pub fn new(
416 delta: ProtocolStateDelta,
417 tokens: &'a HashMap<Bytes, Token>,
418 balances: &'a Balances,
419 ) -> Self {
420 Self { context: Context::default(), delta, tokens, balances }
421 }
422
423 pub fn delta(&self) -> &ProtocolStateDelta {
424 &self.delta
425 }
426
427 pub fn tokens(&self) -> &HashMap<Bytes, Token> {
428 self.tokens
429 }
430
431 pub fn balances(&self) -> &Balances {
432 self.balances
433 }
434}
435
436/// Result of applying a state transition to a pool.
437///
438/// Currently, a placeholder struct that may be extended in the future
439/// to contain transition metadata or validation results.
440pub struct Transition {}
441
442impl Default for Transition {
443 /// Creates a new transition result.
444 fn default() -> Self {
445 Self {}
446 }
447}
448
449/// Defines constraints for advanced quote calculations.
450///
451/// These constraints allow sophisticated trading strategies by limiting swaps
452/// based on price thresholds or targeting specific pool states.
453#[derive(Debug, Clone, PartialEq)]
454pub enum SwapConstraint {
455 /// This mode will calculate the maximum trade that this pool can execute while respecting a
456 /// trade limit price.
457 #[non_exhaustive]
458 TradeLimitPrice {
459 /// The minimum acceptable price for the resulting trade, as a [Price] struct. The
460 /// resulting amount_out / amount_in must be >= trade_limit_price
461 limit: Price,
462 /// The tolerance as a fraction to be applied on top of (increasing) the trade
463 /// limit price, raising the acceptance threshold. This is used to loosen the acceptance
464 /// criteria for implementations of this method, but will never allow violating the trade
465 /// limit price itself.
466 tolerance: f64,
467 /// The minimum amount of token_in that must be used for this trade.
468 min_amount_in: Option<BigUint>,
469 /// The maximum amount of token_in that can be used for this trade.
470 max_amount_in: Option<BigUint>,
471 },
472
473 /// This mode will calculate the amount of token_in required to move the pool's marginal price
474 /// down to a target price, and the amount of token_out received.
475 ///
476 /// # Edge Cases and Limitations
477 ///
478 /// Computing the exact amount to move a pool's marginal price to a target has several
479 /// challenges:
480 /// - The definition of marginal price varies between protocols. It is usually not an attribute
481 /// of the pool but a consequence of its liquidity distribution and current state.
482 /// - For protocols with concentrated liquidity, the marginal price is discrete, meaning we
483 /// can't always find an exact trade amount to reach the target price.
484 /// - Not all protocols support analytical solutions for this problem, requiring numerical
485 /// methods.
486 #[non_exhaustive]
487 PoolTargetPrice {
488 /// The marginal price we want the pool to be after the trade, as a [Price] struct. The
489 /// pool's price will move down to this level as token_in is sold into it
490 target: Price,
491 /// The tolerance as a fraction of the resulting pool marginal price. After trading, the
492 /// pool's price will decrease to the interval `[target, target * (1 +
493 /// tolerance)]`.
494 tolerance: f64,
495 /// The lower bound for searching algorithms.
496 min_amount_in: Option<BigUint>,
497 /// The upper bound for searching algorithms.
498 max_amount_in: Option<BigUint>,
499 },
500}
501
502impl SwapConstraint {
503 /// Creates a trade limit price constraint.
504 ///
505 /// This constraint finds the maximum trade size while respecting a minimum price
506 /// threshold. See `SwapConstraint::TradeLimitPrice` for details.
507 ///
508 /// # Arguments
509 /// * `limit` - The minimum acceptable price for the trade
510 /// * `tolerance` - Additional tolerance as a fraction to loosen the constraint
511 pub fn trade_limit_price(limit: Price, tolerance: f64) -> Self {
512 SwapConstraint::TradeLimitPrice {
513 limit,
514 tolerance,
515 min_amount_in: None,
516 max_amount_in: None,
517 }
518 }
519
520 /// Creates a pool target price constraint.
521 ///
522 /// This constraint calculates the trade needed to move the pool's price to a
523 /// target level. See `SwapConstraint::PoolTargetPrice` for details.`
524 ///
525 /// # Arguments
526 /// * `target` - The desired final marginal price of the pool
527 /// * `tolerance` - Acceptable variance from the target price as a fraction
528 pub fn pool_target_price(target: Price, tolerance: f64) -> Self {
529 SwapConstraint::PoolTargetPrice {
530 target,
531 tolerance,
532 min_amount_in: None,
533 max_amount_in: None,
534 }
535 }
536
537 /// Adds a lower bound to the constraint's search range.
538 ///
539 /// # Arguments
540 /// * `lower` - The minimum amount_in to consider
541 ///
542 /// # Returns
543 /// The modified constraint with the lower bound applied.
544 pub fn with_lower_bound(mut self, lower: BigUint) -> SimulationResult<Self> {
545 match &mut self {
546 SwapConstraint::PoolTargetPrice { min_amount_in, .. } => {
547 *min_amount_in = Some(lower);
548 Ok(self)
549 }
550 SwapConstraint::TradeLimitPrice { min_amount_in, .. } => {
551 *min_amount_in = Some(lower);
552 Ok(self)
553 }
554 }
555 }
556
557 /// Adds an upper bound to the constraint's search range.
558 ///
559 /// # Arguments
560 /// * `upper` - The maximum amount_in to consider
561 ///
562 /// # Returns
563 /// The modified constraint with the upper bound applied.
564 pub fn with_upper_bound(mut self, upper: BigUint) -> SimulationResult<Self> {
565 match &mut self {
566 SwapConstraint::PoolTargetPrice { max_amount_in, .. } => {
567 *max_amount_in = Some(upper);
568 Ok(self)
569 }
570 SwapConstraint::TradeLimitPrice { max_amount_in, .. } => {
571 *max_amount_in = Some(upper);
572 Ok(self)
573 }
574 }
575 }
576}
577
578params_with_context! {
579/// Parameters for advanced swap queries with constraints.
580///
581/// Used for sophisticated swap calculations that respect price limits or target
582/// prices instead of given amount values.
583pub struct QuerySwapParams<'a> {
584 token_in: &'a TokenAddress,
585 token_out: &'a TokenAddress,
586 swap_constraint: SwapConstraint,
587}
588}
589
590impl<'a> QuerySwapParams<'a> {
591 /// Creates new parameters for constrained swap queries.
592 ///
593 /// # Arguments
594 /// * `token_in` - The input token address
595 /// * `token_out` - The output token metadata
596 /// * `swap_constraint` - The constraint to apply to the swap calculation
597 pub fn new(
598 token_in: &'a TokenAddress,
599 token_out: &'a TokenAddress,
600 swap_constraint: SwapConstraint,
601 ) -> Self {
602 Self { context: Context::default(), token_in, token_out, swap_constraint }
603 }
604
605 pub fn token_in(&self) -> &'a TokenAddress {
606 self.token_in
607 }
608
609 pub fn token_out(&self) -> &'a TokenAddress {
610 self.token_out
611 }
612
613 pub fn swap_constraint(&self) -> &SwapConstraint {
614 &self.swap_constraint
615 }
616}
617
618/// Result of an advanced swap calculation with constraints.
619///
620/// Contains the calculated swap amounts, optionally the new pool state,
621/// and price points traversed during calculation for optimization purposes.
622pub struct Swap {
623 /// The amount of token_in sold to the component
624 amount_in: BigUint,
625 /// The amount of token_out bought from the component
626 amount_out: BigUint,
627 /// The new state of the component after the swap
628 new_state: Option<Arc<dyn SwapQuoter>>,
629 /// Optional price points that the pool was transitioned through while computing this swap.
630 /// The values are tuples of (amount_in, amount_out, price). This is useful for repeated calls
631 /// by providing good bounds for the next call.
632 price_points: Option<Vec<PricePoint>>,
633}
634
635/// A point on the AMM price curve.
636///
637/// Collected during iterative numerical search algorithms.
638/// These points can be reused as bounds for subsequent searches, improving convergence speed.
639#[derive(Debug, Clone)]
640pub struct PricePoint {
641 /// The amount of token_in in atomic units (wei).
642 amount_in: BigUint,
643 /// The amount of token_out in atomic units (wei).
644 amount_out: BigUint,
645 /// The price in units of `[token_out/token_in]` scaled by decimals.
646 ///
647 /// Computed as `(amount_out / 10^token_out_decimals) / (amount_in / 10^token_in_decimals)`.
648 price: f64,
649}
650
651impl PricePoint {
652 pub fn amount_in(&self) -> &BigUint {
653 &self.amount_in
654 }
655
656 pub fn amount_out(&self) -> &BigUint {
657 &self.amount_out
658 }
659
660 pub fn price(&self) -> f64 {
661 self.price
662 }
663}
664
665impl Swap {
666 /// Creates a new swap result.
667 ///
668 /// # Arguments
669 /// * `amount_in` - The amount of input tokens used
670 /// * `amount_out` - The amount of output tokens received
671 /// * `new_state` - The new pool state after the swap (if calculated)
672 /// * `price_points` - Optional price trajectory data for optimization
673 pub fn new(
674 amount_in: BigUint,
675 amount_out: BigUint,
676 new_state: Option<Arc<dyn SwapQuoter>>,
677 price_points: Option<Vec<PricePoint>>,
678 ) -> Self {
679 Self { amount_in, amount_out, new_state, price_points }
680 }
681
682 /// Returns the amount of input tokens used.
683 pub fn amount_in(&self) -> &BigUint {
684 &self.amount_in
685 }
686
687 /// Returns the amount of output tokens received.
688 pub fn amount_out(&self) -> &BigUint {
689 &self.amount_out
690 }
691
692 /// Returns the new pool state after the swap, if calculated.
693 pub fn new_state(&self) -> Option<Arc<dyn SwapQuoter>> {
694 self.new_state.clone()
695 }
696
697 /// Returns the price points traversed during calculation.
698 ///
699 /// Each tuple contains (amount_in, amount_out, price) at various points
700 /// during the swap calculation, useful for optimizing subsequent calls.
701 pub fn price_points(&self) -> &Option<Vec<PricePoint>> {
702 &self.price_points
703 }
704}
705
706/// Core trait for implementing swap quote functionality.
707///
708/// This trait defines the interface that all liquidity sources must implement
709/// to participate in swap quotes. It provides methods for price discovery,
710/// quote calculation, state transitions, and advanced swap queries.
711///
712/// Implementations should be thread-safe and support cloning for parallel simulations.
713#[typetag::serde(tag = "protocol", content = "state")]
714pub trait SwapQuoter: fmt::Debug + Send + Sync + 'static {
715 /// Returns the [`ProtocolComponent`] describing the protocol instance this quoter
716 /// is associated with.
717 ///
718 /// The component provides **structural and descriptive metadata** about the protocol,
719 /// such as the set of involved tokens and protocol-specific configuration, but does not
720 /// represent a mutable simulation state.
721 ///
722 /// # Semantics
723 ///
724 /// - The returned component is expected to be **stable for the lifetime of the quoter**.
725 /// - Multiple quoter instances may share the same component instance; callers should not assume
726 /// unique ownership (e.g. quoter instances may represent the state at different points in
727 /// time)
728 /// - The component is used for discovery and introspection (e.g. determining supported tokens
729 /// or deriving default quotable pairs), not for executing swaps.
730 ///
731 /// # Ownership
732 ///
733 /// This method returns an `Arc` to allow cheap cloning and shared access without
734 /// constraining the internal storage strategy of the implementation.
735 ///
736 /// # Intended use
737 ///
738 /// Typical uses include:
739 /// - Inspecting the tokens and configuration exposed by the protocol
740 /// - Deriving default [`quotable_pairs`](Self::quotable_pairs)
741 /// - Identifying or grouping quoters by protocol metadata
742 fn component(&self) -> Arc<ProtocolComponent<Arc<Token>>>;
743
744 /// Returns the set of **directed token pairs** for which this quoter can produce swap quotes.
745 ///
746 /// Each `(base, quote)` pair indicates that a swap from `base` to `quote` is supported.
747 /// Direction matters: `(A, B)` and `(B, A)` are considered distinct pairs and might not
748 /// both be present.
749 ///
750 /// # Semantics
751 ///
752 /// - The returned set represents **capability**, not liquidity or pricing guarantees. A pair
753 /// being present does not imply that a quote will be favorable or even currently executable,
754 /// only that the quoter understands how to price it.
755 /// - The set may be **computed dynamically** and is not required to be stable across calls,
756 /// though most implementations are expected to return the same result unless the underlying
757 /// protocol configuration changes.
758 ///
759 /// # Default behavior
760 ///
761 /// The default implementation derives the pairs from the tokens exposed by
762 /// [`component()`], returning all ordered pairs `(a, b)` where `a != b`.
763 /// Protocols with restricted or asymmetric support (e.g. RFQ-based or single-sided
764 /// designs) should override this method.
765 ///
766 ///
767 /// # Intended use
768 ///
769 /// This method is primarily intended for routing, discovery, and validation logic,
770 /// allowing callers to determine whether a quote request is meaningful before invoking
771 /// [`quote`].
772 fn quotable_pairs(&self) -> Vec<(Arc<Token>, Arc<Token>)> {
773 let component = self.component();
774 component
775 .tokens
776 .iter()
777 .permutations(2)
778 .map(|token| (token[0].clone(), token[1].clone()))
779 .collect()
780 }
781
782 /// Computes the protocol fee applicable to a prospective swap described by `params`.
783 ///
784 /// This method evaluates the fee that would be charged by the protocol for the given
785 /// swap parameters, without performing the swap or mutating any internal state.
786 ///
787 /// # Semantics
788 ///
789 /// - The returned [`SwapFee`] represents the **protocol-defined fee component** of the swap
790 /// (e.g. LP fee, protocol fee, or RFQ spread), as understood by this quoter.
791 /// - Fee computation is **pure and side-effect free**; calling this method must not modify the
792 /// internal state of the quoter.
793 /// - The fee may depend on the full set of quote parameters (including direction, amount, or
794 /// other protocol-specific inputs).
795 ///
796 /// # Relation to quoting
797 ///
798 /// Implementations may internally reuse logic from [`quote`], but this method exists to
799 /// allow callers to:
800 /// - Inspect or decompose pricing components
801 /// - Perform fee-aware routing or optimization
802 /// - Estimate costs without requesting a full quote
803 ///
804 /// # Errors
805 ///
806 /// Returns an error if the fee cannot be determined for the given parameters (e.g. the
807 /// pair is not quotable or required inputs are missing)
808 fn fee(&self, params: QuoteParams) -> SimulationResult<SwapFee>;
809
810 /// Computes the **marginal (infinitesimal) price** for a swap described by `params`,
811 /// with **all protocol fees included**.
812 ///
813 /// The marginal price represents the instantaneous exchange rate at the current
814 /// protocol state, evaluated at an infinitesimally small trade size. It reflects the
815 /// derivative of output amount with respect to input amount for the specified swap
816 /// direction, inclusive of any protocol-defined fees or spreads.
817 ///
818 /// # Semantics
819 ///
820 /// - Fees are **always included** in the returned [`MarginalPrice`].
821 /// - The price is evaluated **at the margin** and does not represent an executable price for a
822 /// finite trade.
823 /// - This method is **pure and side-effect free**; it must not mutate internal state.
824 /// - For sufficiently small trade sizes, the marginal price should be consistent with
825 /// [`quote`], up to numerical precision.
826 ///
827 /// # Use cases
828 ///
829 /// Typical uses include:
830 /// - Price display and monitoring
831 /// - Slippage estimation and sensitivity analysis
832 /// - Routing heuristics and initial path selection
833 ///
834 /// # Errors
835 ///
836 /// Returns an error if the marginal price is undefined or cannot be computed for the
837 /// given parameters (e.g. unsupported pair, zero liquidity, or missing inputs).
838 fn marginal_price(&self, params: MarginalPriceParams) -> SimulationResult<MarginalPrice>;
839
840 /// Produces a swap quote for the trade described by `params`, with **all protocol fees
841 /// included**.
842 ///
843 /// The returned [`Quote`] represents the effective execution terms of the swap as
844 /// understood by this quoter, including any protocol-defined fees, spreads, or
845 /// adjustments. Calling this method does not perform the swap and does not mutate
846 /// internal state.
847 ///
848 /// # Semantics
849 ///
850 /// - Fees are **always included** in the quoted price and amounts. Callers should not apply
851 /// additional protocol fees on top of the returned quote.
852 /// - Fees mentioned above **do not include** chain fees such as gas costs.
853 /// - The quote reflects a **finite-size trade** and therefore accounts for price impact where
854 /// applicable.
855 /// - This method is **pure and side-effect free**; it must not mutate internal state.
856 /// - For sufficiently small trade sizes, the quote should be consistent with
857 /// [`marginal_price`], up to numerical precision.
858 ///
859 /// # Intended use
860 ///
861 /// This method is the primary entry point for consumers of [`SwapQuoter`] and is
862 /// intended for:
863 /// - User-facing price discovery
864 /// - Routing and optimization across multiple quoters
865 /// - Simulation and what-if analysis
866 ///
867 /// # Errors
868 ///
869 /// Returns an error if a quote cannot be produced for the given parameters (e.g. the
870 /// pair is not quotable, required inputs are missing, or the quote is undefined).
871 fn quote(&self, params: QuoteParams) -> SimulationResult<Quote>;
872
873 /// Returns the valid execution limits for a prospective quote
874 ///
875 /// The returned [`SwapLimits`] describes the bounds within which a swap can be quoted or
876 /// simulated, such as minimum and maximum input or output amounts, given the current
877 /// protocol state.
878 ///
879 /// # Semantics
880 ///
881 /// - Limits are evaluated **at the current state** of the quoter and do not imply that a quote
882 /// will succeed outside the returned bounds.
883 /// - The limits may depend on swap direction, fees, liquidity constraints, or protocol-specific
884 /// rules.
885 /// - This method is **pure and side-effect free**; it must not mutate internal state.
886 /// - Limits are expressed in **fee-inclusive terms**, consistent with [`quote`] and
887 /// [`marginal_price`].
888 ///
889 /// # Intended use
890 ///
891 /// Typical uses include:
892 /// - Pre-validating quote requests before quoting
893 /// - Bounding search spaces for routing and optimization
894 /// - UI validation and input clamping
895 ///
896 /// # Errors
897 ///
898 /// Returns an error if limits cannot be determined for the given parameters (e.g. the
899 /// pair is not quotable or required inputs are missing).
900 fn swap_limits(&self, params: LimitsParams) -> SimulationResult<SwapLimits>;
901
902 /// Produces an **advanced, price-constraint-based quote**
903 ///
904 /// Unlike [`quote`], which prices a swap for a fixed input or output amount,
905 /// `query_swap` solves for a swap that satisfies a higher-level [`SwapConstraint`],
906 /// such as a minimum execution price or a target post-trade pool price.
907 ///
908 /// The returned [`Swap`] describes the swap that best satisfies the given constraint
909 /// under the current protocol state, with **all protocol fees included**.
910 ///
911 /// # Semantics
912 ///
913 /// - The method is **read-only** and does not mutate internal state.
914 /// - All amounts and prices in the returned [`Swap`] are **fee-inclusive**, consistent with
915 /// [`quote`] and [`marginal_price`].
916 /// - The constraint defines *what is solved for* (e.g. maximum trade size, target price),
917 /// rather than supplying an explicit trade amount.
918 /// - Implementations may use analytical or numerical methods to satisfy the constraint, subject
919 /// to the provided tolerances and bounds.
920 ///
921 /// # Supported constraints
922 ///
923 /// The behavior of this method is defined by the [`SwapConstraint`] provided in
924 /// `params`, including:
925 ///
926 /// - **Trade limit price**: computes the maximum executable trade size whose effective price
927 /// (amount_out / amount_in) meets or exceeds a specified limit.
928 /// - **Pool target price**: computes the trade required to move the pool’s marginal price down
929 /// to a target level, within a specified tolerance.
930 ///
931 /// Bounds on the search space (minimum and maximum `amount_in`) are respected when
932 /// provided.
933 ///
934 /// # Intended use
935 ///
936 /// Typical uses include:
937 /// - Price-impact-aware execution planning
938 /// - Strategy-driven routing (e.g. price-capped or price-targeting trades)
939 /// - Liquidity probing and pool sensitivity analysis
940 ///
941 /// # Errors
942 ///
943 /// Returns an error if:
944 /// - The constraint cannot be satisfied within the provided bounds or tolerances
945 /// - The token pair is not quotable
946 /// - The swap is undefined under the current protocol state
947 fn query_swap(&self, params: QuerySwapParams) -> SimulationResult<Swap>;
948
949 /// Applies a **protocol state delta**
950 ///
951 /// This method updates the internal protocol state of the quoter by applying the
952 /// incremental changes described by `params`, typically derived from an external
953 /// indexer or on-chain data source. It is used to keep the quoter’s local view of the
954 /// protocol state in sync with the latest block.
955 ///
956 /// # Semantics
957 ///
958 /// - The provided delta is assumed to represent a **valid, externally observed state
959 /// transition** (e.g. from an indexer service) and is not re-validated as a swap.
960 /// - Calling this method **mutates internal state**; all subsequent quotes, prices, and limits
961 /// will reflect the updated state.
962 /// - The transition is applied **incrementally** and is expected to be composable with previous
963 /// deltas.
964 ///
965 /// # Intended use
966 ///
967 /// Typical uses include:
968 /// - Applying per-block protocol updates received from an indexer
969 /// - Advancing local state during historical replay or backfilling
970 /// - Keeping multiple quoters synchronized with chain state
971 ///
972 /// # Errors
973 ///
974 /// Returns an error if the delta cannot be applied to the current state (e.g. it is
975 /// incompatible, malformed, or violates protocol invariants).
976 fn delta_transition(&mut self, params: TransitionParams)
977 -> Result<Transition, TransitionError>;
978
979 /// Clones the protocol state as a trait object.
980 ///
981 /// This method enables cloning when the pool is used as a boxed trait object,
982 /// which is necessary for parallel simulations and state management.
983 ///
984 /// # Returns
985 /// A new boxed instance with the same state as this pool.
986 fn clone_box(&self) -> Box<dyn SwapQuoter>;
987
988 /// Attempts to cast this pool to an indicatively priced pool.
989 ///
990 /// This is used for RFQ (Request for Quote) protocols that provide
991 /// indicative pricing rather than deterministic calculations.
992 ///
993 /// # Returns
994 /// A reference to the `IndicativelyPriced` implementation, or an error
995 /// if this pool type doesn't support indicative pricing.
996 ///
997 /// # Default Implementation
998 /// Returns an error indicating that indicative pricing is not supported.
999 fn as_indicatively_priced(&self) -> Result<&dyn IndicativelyPriced, SimulationError> {
1000 Err(SimulationError::FatalError("Pool State does not implement IndicativelyPriced".into()))
1001 }
1002
1003 #[deprecated(note = "ProtocolSim is deprecated. This method will be removed in v1.0.0")]
1004 fn to_protocol_sim(&self) -> Box<dyn ProtocolSim>;
1005}
1006
1007/// Testing extension trait for SwapQuoter implementations.
1008///
1009/// Provides additional methods needed for testing and validation
1010/// that are not part of the main SwapQuoter interface.
1011#[cfg(test)]
1012pub trait SwapQuoterTestExt {
1013 /// Compares this pool state with another for equality.
1014 ///
1015 /// This method is used in tests to verify that pool states
1016 /// are equivalent after various operations.
1017 ///
1018 /// # Arguments
1019 /// * `other` - Another SwapQuoter to compare against
1020 ///
1021 /// # Returns
1022 /// `true` if the pool states are equivalent, `false` otherwise.
1023 fn eq(&self, other: &dyn SwapQuoter) -> bool;
1024}