breez_sdk_spark/token_conversion/mod.rs
1mod error;
2mod flashnet;
3mod middleware;
4mod models;
5
6pub use error::ConversionError;
7pub(crate) use flashnet::FlashnetTokenConverter;
8pub(crate) use middleware::TokenConversionMiddleware;
9pub use models::*;
10
11use std::sync::Arc;
12
13use spark_wallet::{PublicKey, TransferId};
14use tokio::sync::broadcast;
15
16use crate::{EventEmitter, RefundPendingConversionsResponse};
17
18/// Trait for conversion implementations.
19///
20/// This trait abstracts the conversion mechanics, allowing different
21/// implementations (e.g., Flashnet) to be used interchangeably.
22/// Business logic for when/how much to convert is handled by `StableBalance`.
23///
24/// Implementations are reachable from the `EventEmitter` (the stable balance
25/// middleware holds the converter), so they must not store an emitter
26/// reference; the caller passes one into [`convert`](Self::convert) instead.
27#[macros::async_trait]
28pub(crate) trait TokenConverter: Send + Sync {
29 /// Execute a conversion swap.
30 ///
31 /// # Arguments
32 /// * `event_emitter` - Emitter for the payment events of the swap legs
33 /// * `options` - The conversion options including type and slippage
34 /// * `purpose` - The purpose of the conversion
35 /// * `token_identifier` - Optional token identifier for `FromBitcoin` conversions
36 /// * `amount` - Either the minimum output amount or exact input amount
37 /// * `transfer_id` - Optional transfer ID for idempotency
38 async fn convert(
39 &self,
40 event_emitter: Arc<EventEmitter>,
41 options: &ConversionOptions,
42 purpose: &ConversionPurpose,
43 token_identifier: Option<&String>,
44 amount: ConversionAmount,
45 transfer_id: Option<TransferId>,
46 ) -> Result<TokenConversionResponse, ConversionError>;
47
48 /// The legs of the conversion whose sent leg is the transfer
49 /// `transfer_id`, recorded as completed, when the pool reports its swap
50 /// ran. Sends nothing. Errors when the pool's swaps could not be listed,
51 /// which is distinct from a swap that did not run.
52 async fn find_completed_conversion(
53 &self,
54 transfer_id: &TransferId,
55 purpose: &ConversionPurpose,
56 ) -> Result<Option<TokenConversionResponse>, ConversionError>;
57
58 /// Validate a conversion and return the estimated conversion.
59 ///
60 /// Called during `prepare_send_payment` to calculate the conversion fee,
61 /// and during auto-conversion to estimate the token output.
62 ///
63 /// # Arguments
64 /// * `options` - The conversion options to validate
65 /// * `token_identifier` - Optional token identifier for `FromBitcoin` conversions
66 /// * `amount` - Either the minimum output amount or exact input amount
67 ///
68 /// # Returns
69 /// The estimated conversion including amount and fee, or None if options is None.
70 /// `estimate.amount_in` is the input amount, `estimate.amount_out` is the estimated output.
71 async fn validate(
72 &self,
73 options: Option<&ConversionOptions>,
74 token_identifier: Option<&String>,
75 amount: ConversionAmount,
76 ) -> Result<Option<ConversionEstimate>, ConversionError>;
77
78 /// Fetch conversion limits for a given conversion type.
79 ///
80 /// # Arguments
81 /// * `request` - The request containing conversion type and optional token identifier
82 async fn fetch_limits(
83 &self,
84 request: &FetchConversionLimitsRequest,
85 ) -> Result<FetchConversionLimitsResponse, ConversionError>;
86
87 /// Runs a local and a remote pass to refund any pending conversions.
88 async fn refund_pending(
89 &self,
90 event_emitter: &EventEmitter,
91 ) -> Result<RefundPendingConversionsResponse, ConversionError>;
92
93 /// Runs a local pass to refund any pending conversions.
94 async fn refund_local_pending(
95 &self,
96 event_emitter: &EventEmitter,
97 ) -> Result<RefundPendingConversionsResponse, ConversionError>;
98
99 /// Records the swap if it ran after all, and otherwise claws the input back.
100 async fn settle_stranded_input(&self, input: StrandedInput, event_emitter: &EventEmitter);
101
102 /// Optional requests that wake the client-mode periodic refunder.
103 fn subscribe_refund_requests(&self) -> Option<broadcast::Receiver<RefundRequest>> {
104 None
105 }
106}
107
108/// How long the refunder waits before settling a stranded input, so the pool's
109/// swap listing can catch up with a swap that ran despite the failed call.
110pub(crate) const STRANDED_INPUT_SETTLE_SECS: u64 = 10;
111
112/// A swap input a failed conversion left at the pool.
113#[derive(Clone, Debug)]
114pub(crate) struct StrandedInput {
115 pub(crate) clawback_id: String,
116 pub(crate) pool_id: PublicKey,
117 pub(crate) payment_id: Option<String>,
118 pub(crate) prior_info: Option<ConversionInfo>,
119}
120
121/// Work for the client-mode conversion refunder.
122#[derive(Clone, Debug)]
123pub(crate) enum RefundRequest {
124 /// Run a pass over the payments marked `RefundNeeded`.
125 Pass,
126 /// Settle one stranded input, after the settle wait.
127 Settle(Box<StrandedInput>),
128}