Skip to main content

breez_sdk_spark/token_conversion/
models.rs

1use std::fmt;
2use std::str::FromStr;
3
4use flashnet::{BTC_ASSET_ADDRESS, Pool};
5use serde::{Deserialize, Serialize};
6
7use crate::SdkError;
8
9use crate::utils::serde_helpers::{serde_option_u128_as_string, serde_u128_as_string};
10
11/// Default maximum slippage for conversions in basis points (0.1%)
12pub const DEFAULT_CONVERSION_MAX_SLIPPAGE_BPS: u32 = 10;
13/// Default timeout for conversion operations in seconds
14pub const DEFAULT_CONVERSION_TIMEOUT_SECS: u32 = 30;
15/// Default integrator pubkey used when executing conversions
16pub const DEFAULT_INTEGRATOR_PUBKEY: &str =
17    "037e26d9d62e0b3df2d3e66805f61de2a33914465297abf76817296a92ac3f2379";
18/// Default integrator fee BPS used when simulating/executing conversions
19pub const DEFAULT_INTEGRATOR_FEE_BPS: u32 = 5;
20
21/// Fee attribution for a conversion, indicating which side of the conversion
22/// (sent or received) the pool fee is denominated in. The two variants are
23/// mutually exclusive — a pool fee is always denominated in one asset.
24pub(crate) enum FeeSplit {
25    /// Fee is on the sent (outbound/`asset_in`) payment, denominated in `asset_in`.
26    Sent(u128),
27    /// Fee is on the received (inbound/`asset_out`) payment, denominated in `asset_out`.
28    Received(u128),
29}
30
31/// Response from estimating a conversion, used when preparing a payment that requires conversion
32#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
33#[derive(Debug, Clone, Serialize)]
34pub struct ConversionEstimate {
35    /// The conversion options used for the estimate
36    pub options: ConversionOptions,
37    /// The input amount for the conversion.
38    /// For `FromBitcoin`: the satoshis required to produce the desired token output.
39    /// For `ToBitcoin`: the token amount being converted.
40    pub amount_in: u128,
41    /// The estimated output amount from the conversion.
42    /// For `FromBitcoin`: the estimated token amount received.
43    /// For `ToBitcoin`: the estimated satoshis received.
44    pub amount_out: u128,
45    /// The fee estimated for the conversion.
46    /// Denominated in satoshis if converting from Bitcoin, otherwise in the token base units.
47    pub fee: u128,
48    /// The reason the conversion amount was adjusted, if applicable.
49    pub amount_adjustment: Option<AmountAdjustmentReason>,
50}
51
52/// The purpose of the conversion, which is used to provide context for the conversion
53/// if its related to an ongoing payment or a self-transfer.
54#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
56pub enum ConversionPurpose {
57    /// Conversion is associated with an ongoing payment
58    OngoingPayment {
59        /// The payment request of the ongoing payment
60        payment_request: String,
61    },
62    /// Conversion is for self-transfer
63    SelfTransfer,
64    /// Conversion triggered automatically
65    AutoConversion,
66}
67
68/// Specifies how to determine the conversion amount.
69#[derive(Debug, Clone)]
70pub(crate) enum ConversionAmount {
71    /// Specify the minimum output amount - the input will be calculated.
72    /// Used for payment conversions where we know the required output.
73    MinAmountOut(u128),
74    /// Specify the exact input amount - used for auto-conversion where we know the sats balance.
75    AmountIn(u128),
76}
77
78/// The reason why a conversion amount was adjusted from the originally requested value.
79#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
80#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
81pub enum AmountAdjustmentReason {
82    /// The amount was increased to meet the minimum conversion limit.
83    FlooredToMinLimit,
84    /// The amount was increased to convert the full token balance,
85    /// avoiding a remaining balance below the minimum conversion limit (token dust).
86    IncreasedToAvoidDust,
87}
88
89/// The status of the conversion
90#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
92pub enum ConversionStatus {
93    /// Conversion is in-flight (queued or started, not yet completed)
94    Pending,
95    /// The conversion was successful
96    Completed,
97    /// The conversion failed (e.g., the initial send payment failed)
98    Failed,
99    /// The conversion failed and no refund was made yet, which requires action by the SDK to
100    /// perform the refund. This can happen if there was a failure during the conversion process.
101    RefundNeeded,
102    /// The conversion failed and a refund was made
103    Refunded,
104}
105
106impl fmt::Display for ConversionStatus {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        match self {
109            ConversionStatus::Pending => write!(f, "pending"),
110            ConversionStatus::Completed => write!(f, "completed"),
111            ConversionStatus::Failed => write!(f, "failed"),
112            ConversionStatus::RefundNeeded => write!(f, "refund_needed"),
113            ConversionStatus::Refunded => write!(f, "refunded"),
114        }
115    }
116}
117
118impl FromStr for ConversionStatus {
119    type Err = String;
120
121    fn from_str(s: &str) -> Result<Self, Self::Err> {
122        match s {
123            "pending" => Ok(ConversionStatus::Pending),
124            "completed" => Ok(ConversionStatus::Completed),
125            "failed" => Ok(ConversionStatus::Failed),
126            "refund_needed" => Ok(ConversionStatus::RefundNeeded),
127            "refunded" => Ok(ConversionStatus::Refunded),
128            _ => Err(format!("Invalid conversion status '{s}'")),
129        }
130    }
131}
132
133/// Details of the asset conversion attached to a payment, when the payment
134/// involves a swap or cross-chain bridge in addition to the on-Spark transfer.
135///
136/// The variant identifies which provider handled the conversion:
137/// - [`ConversionInfo::Amm`] for Spark token swaps via Flashnet AMM pools.
138/// - [`ConversionInfo::Orchestra`] for cross-chain sends via Flashnet
139///   Orchestra (Spark → external chain).
140/// - [`ConversionInfo::Boltz`] for sats → stable-coin reverse swaps via Boltz.
141#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
142#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
143#[serde(tag = "type")]
144pub enum ConversionInfo {
145    /// AMM (Flashnet pool-based) conversion — Spark ↔ Spark token swaps.
146    #[serde(rename = "amm")]
147    Amm {
148        /// The pool id associated with the conversion
149        pool_id: String,
150        /// The conversion id shared by both sides of the conversion
151        conversion_id: String,
152        /// The status of the conversion
153        status: ConversionStatus,
154        /// The fee paid for the conversion.
155        /// Denominated in satoshis if converting from Bitcoin, otherwise in the token base units.
156        #[serde(default, with = "serde_option_u128_as_string")]
157        fee: Option<u128>,
158        /// The purpose of the conversion
159        purpose: Option<ConversionPurpose>,
160        /// The reason the conversion amount was adjusted, if applicable.
161        #[serde(default)]
162        amount_adjustment: Option<AmountAdjustmentReason>,
163    },
164    /// Orchestra cross-chain conversion via the Flashnet orchestration API.
165    #[serde(rename = "orchestra")]
166    Orchestra {
167        /// The Orchestra order id returned by `/v1/orchestration/submit`.
168        order_id: String,
169        /// The Orchestra quote id used to create this order.
170        quote_id: String,
171        /// Opaque token required for querying order status.
172        #[serde(default)]
173        read_token: Option<String>,
174
175        /// Chain name (e.g. `"base"`, `"solana"`, `"tron"`).
176        chain: String,
177        /// Stable chain identifier (e.g. EVM `chainId` decimal string `"8453"`
178        /// for Base, SLIP-44 or similar for other chains). `None` if the
179        /// provider doesn't expose one for this route.
180        #[serde(default)]
181        chain_id: Option<String>,
182        /// Asset ticker (e.g. `"USDC"`, `"USDT"`).
183        #[serde(default)]
184        asset: String,
185        /// Recipient address on the target chain.
186        recipient_address: String,
187        /// Amount in expressed in the cross-chain asset's base units, via
188        /// the rate the SDK used at prepare time.
189        #[serde(default, with = "serde_option_u128_as_string")]
190        asset_amount_in: Option<u128>,
191        /// Estimated recipient amount, frozen at prepare time.
192        #[serde(with = "serde_u128_as_string")]
193        estimated_out: u128,
194        /// Actual delivered amount, Unset until the order reaches a terminal state.
195        #[serde(default, with = "serde_option_u128_as_string")]
196        delivered_amount: Option<u128>,
197        status: ConversionStatus,
198        /// Best-available total fee in destination asset base units.
199        /// Prepare-time estimate while pending, realized fee when Completed.
200        #[serde(default, with = "serde_option_u128_as_string")]
201        fee_amount: Option<u128>,
202        /// Orchestra service fee.
203        #[serde(
204            default,
205            alias = "fee",
206            rename = "service_fee_amount",
207            with = "serde_option_u128_as_string"
208        )]
209        service_fee_amount: Option<u128>,
210        /// Asset the service fee is denominated in. Unset means BTC sats.
211        #[serde(default)]
212        service_fee_asset: Option<String>,
213        /// Asset decimals (e.g. 6 for USDC).
214        asset_decimals: u32,
215        /// Token contract / mint address. Unset for native-asset destinations.
216        #[serde(default)]
217        asset_contract: Option<String>,
218    },
219    /// Boltz reverse swap: cross-chain conversion via Lightning hold invoice.
220    ///
221    /// The swap's secrets and lifecycle state live on the synced Boltz swap row
222    /// keyed by `swap_id`, which also drives cross-instance recovery.
223    #[serde(rename = "boltz")]
224    Boltz {
225        /// The Boltz swap id returned by `POST /swap/reverse`.
226        swap_id: String,
227        /// The BOLT11 hold invoice paid on the Spark/Lightning side.
228        invoice: String,
229        /// Amount of the hold invoice in sats.
230        invoice_amount_sats: u64,
231        /// Cross-chain bridge tracking handle for bridged swaps: a `LayerZero`
232        /// message GUID for OFT (USDT0) routes, or a CCTP reference for USDC
233        /// routes. `None` for same-chain (Arbitrum-direct) delivery.
234        #[serde(default, alias = "lz_guid")]
235        bridge_ref: Option<String>,
236        /// DEX slippage tolerance (basis points) committed at prepare time.
237        max_slippage_bps: u32,
238        /// Whether the claim-time DEX quote drifted beyond `max_slippage_bps`.
239        #[serde(default)]
240        quote_degraded: bool,
241
242        /// Chain name (e.g. `"arbitrum"`, `"solana"`, `"tron"`).
243        chain: String,
244        /// Stable chain identifier (e.g. EVM `chainId` decimal string `"42161"`
245        /// for Arbitrum). `None` if the provider doesn't expose one for this
246        /// route.
247        #[serde(default)]
248        chain_id: Option<String>,
249        /// Asset ticker (e.g. `"USDT"`, `"USDT0"`).
250        #[serde(default)]
251        asset: String,
252        /// Recipient address on the target chain.
253        recipient_address: String,
254        /// Estimated amount in the asset's base units, frozen at prepare time.
255        #[serde(with = "serde_u128_as_string")]
256        estimated_out: u128,
257        /// Actual amount delivered. `None` until the claim receipt is processed.
258        #[serde(default, with = "serde_option_u128_as_string")]
259        delivered_amount: Option<u128>,
260        /// Current status of the reverse swap.
261        status: ConversionStatus,
262        /// Amount in expressed in the cross-chain asset's base units, via the
263        /// BTC/USD rate the SDK used at prepare time.
264        #[serde(default, with = "serde_option_u128_as_string")]
265        asset_amount_in: Option<u128>,
266        /// Best-available total fee in destination asset base units.
267        /// Prepare-time estimate while pending, realized fee on Completed.
268        #[serde(default, with = "serde_option_u128_as_string")]
269        fee_amount: Option<u128>,
270        /// Boltz spread in sats.
271        #[serde(
272            default,
273            alias = "fee",
274            rename = "service_fee_amount",
275            with = "serde_option_u128_as_string"
276        )]
277        service_fee_amount: Option<u128>,
278        /// Asset service fee is denominated in. Unset means BTC sats.
279        #[serde(default)]
280        service_fee_asset: Option<String>,
281        /// Asset decimals (e.g. 6 for USDT).
282        asset_decimals: u32,
283        /// Token contract / mint address. Unset for native-asset destinations.
284        #[serde(default)]
285        asset_contract: Option<String>,
286    },
287}
288
289impl ConversionInfo {
290    /// The current status, regardless of conversion type.
291    pub fn status(&self) -> &ConversionStatus {
292        match self {
293            ConversionInfo::Amm { status, .. }
294            | ConversionInfo::Orchestra { status, .. }
295            | ConversionInfo::Boltz { status, .. } => status,
296        }
297    }
298
299    /// A mutable reference to the status, for in-place updates.
300    pub fn status_mut(&mut self) -> &mut ConversionStatus {
301        match self {
302            ConversionInfo::Amm { status, .. }
303            | ConversionInfo::Orchestra { status, .. }
304            | ConversionInfo::Boltz { status, .. } => status,
305        }
306    }
307
308    /// Headline fee: AMM pool fee in source units, or the cross-chain total
309    /// in destination-asset base units.
310    pub fn fee(&self) -> Option<u128> {
311        match self {
312            ConversionInfo::Amm { fee, .. } => *fee,
313            ConversionInfo::Orchestra { fee_amount, .. }
314            | ConversionInfo::Boltz { fee_amount, .. } => *fee_amount,
315        }
316    }
317
318    /// Whether this is an AMM (Flashnet pool) conversion.
319    pub fn is_amm(&self) -> bool {
320        matches!(self, ConversionInfo::Amm { .. })
321    }
322
323    /// Whether this is an Orchestra (cross-chain) conversion.
324    pub fn is_orchestra(&self) -> bool {
325        matches!(self, ConversionInfo::Orchestra { .. })
326    }
327
328    /// Whether this is a Boltz reverse swap.
329    pub fn is_boltz(&self) -> bool {
330        matches!(self, ConversionInfo::Boltz { .. })
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    #[test]
339    fn boltz_conversion_info_roundtrip() {
340        let original = ConversionInfo::Boltz {
341            swap_id: "boltz_swap_abc".to_string(),
342            chain: "solana".to_string(),
343            chain_id: None,
344            asset: "USDT0".to_string(),
345            recipient_address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
346            invoice: "lnbc1000n1pexample".to_string(),
347            invoice_amount_sats: 150_000,
348            asset_amount_in: Some(100_500_000),
349            estimated_out: 99_000_000,
350            delivered_amount: Some(98_750_000),
351            bridge_ref: Some("0xdeadbeef".to_string()),
352            status: ConversionStatus::Pending,
353            fee_amount: Some(1_500_000),
354            service_fee_amount: Some(2_500),
355            service_fee_asset: None,
356            max_slippage_bps: 100,
357            quote_degraded: false,
358            asset_decimals: 6,
359            asset_contract: Some("0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string()),
360        };
361
362        let json = serde_json::to_string(&original).unwrap();
363        let decoded: ConversionInfo = serde_json::from_str(&json).unwrap();
364        assert_eq!(decoded, original);
365        assert!(decoded.is_boltz());
366        assert!(!decoded.is_orchestra());
367        assert!(!decoded.is_amm());
368        assert_eq!(decoded.status(), &ConversionStatus::Pending);
369        assert_eq!(decoded.fee(), Some(1_500_000));
370
371        // The `"type"` tag discriminator must match the rename attribute.
372        assert!(json.contains(r#""type":"boltz""#));
373        // u128 fields serialize as strings, not JSON numbers.
374        assert!(json.contains(r#""estimated_out":"99000000""#));
375    }
376
377    /// Pre-rename rows persisted in the wild only carried a `fee` key (the
378    /// provider service fee), and lacked `asset_amount_in`, `fee_amount`,
379    /// `service_fee_amount`, `service_fee_asset`. Reading such a row must:
380    /// - route the legacy `fee` value into `service_fee_amount`,
381    /// - default every new field to `None`,
382    /// - succeed for both provider variants.
383    #[test]
384    fn boltz_legacy_fee_alias_deserializes_into_service_fee_amount() {
385        let legacy = r#"{
386            "type": "boltz",
387            "swap_id": "swap_legacy",
388            "invoice": "lnbc1000n1pold",
389            "invoice_amount_sats": 100000,
390            "max_slippage_bps": 100,
391            "chain": "arbitrum",
392            "asset": "USDT",
393            "recipient_address": "0xrecipient",
394            "estimated_out": "1450000",
395            "status": "Pending",
396            "fee": "1500",
397            "asset_decimals": 6
398        }"#;
399        let decoded: ConversionInfo = serde_json::from_str(legacy).unwrap();
400        let ConversionInfo::Boltz {
401            asset_amount_in,
402            fee_amount,
403            service_fee_amount,
404            service_fee_asset,
405            ..
406        } = &decoded
407        else {
408            panic!("expected Boltz variant, got: {decoded:?}");
409        };
410        assert_eq!(*asset_amount_in, None);
411        assert_eq!(*fee_amount, None);
412        assert_eq!(
413            *service_fee_amount,
414            Some(1500),
415            "legacy `fee` key must map to `service_fee_amount`"
416        );
417        assert_eq!(*service_fee_asset, None);
418    }
419
420    #[test]
421    fn orchestra_legacy_fee_alias_deserializes_into_service_fee_amount() {
422        let legacy = r#"{
423            "type": "orchestra",
424            "order_id": "ord_legacy",
425            "quote_id": "q_legacy",
426            "chain": "base",
427            "asset": "USDC",
428            "recipient_address": "0xrecipient",
429            "estimated_out": "99500000",
430            "status": "Completed",
431            "fee": "500",
432            "asset_decimals": 6
433        }"#;
434        let decoded: ConversionInfo = serde_json::from_str(legacy).unwrap();
435        let ConversionInfo::Orchestra {
436            asset_amount_in,
437            fee_amount,
438            service_fee_amount,
439            service_fee_asset,
440            ..
441        } = &decoded
442        else {
443            panic!("expected Orchestra variant, got: {decoded:?}");
444        };
445        assert_eq!(*asset_amount_in, None);
446        assert_eq!(*fee_amount, None);
447        assert_eq!(
448            *service_fee_amount,
449            Some(500),
450            "legacy `fee` key must map to `service_fee_amount`"
451        );
452        assert_eq!(*service_fee_asset, None);
453    }
454
455    /// New writes must use the renamed `service_fee_amount` key (never the
456    /// legacy `fee`), and must emit the three new fields when populated.
457    #[test]
458    fn new_rows_use_renamed_keys_on_serialize() {
459        let info = ConversionInfo::Boltz {
460            swap_id: "s".to_string(),
461            chain: "arbitrum".to_string(),
462            chain_id: None,
463            asset: "USDT".to_string(),
464            recipient_address: "0xr".to_string(),
465            invoice: "lnbc".to_string(),
466            invoice_amount_sats: 100,
467            asset_amount_in: Some(1_500_000),
468            estimated_out: 1_450_000,
469            delivered_amount: None,
470            bridge_ref: None,
471            status: ConversionStatus::Pending,
472            fee_amount: Some(50_000),
473            service_fee_amount: Some(1_500),
474            service_fee_asset: Some("USD".to_string()),
475            max_slippage_bps: 100,
476            quote_degraded: false,
477            asset_decimals: 6,
478            asset_contract: None,
479        };
480        let json = serde_json::to_string(&info).unwrap();
481        assert!(
482            json.contains(r#""service_fee_amount":"1500""#),
483            "must serialize under the new key, got: {json}"
484        );
485        assert!(
486            !json.contains(r#""fee":"#),
487            "must not emit the legacy `fee` key, got: {json}"
488        );
489        assert!(json.contains(r#""asset_amount_in":"1500000""#));
490        assert!(json.contains(r#""fee_amount":"50000""#));
491        assert!(json.contains(r#""service_fee_asset":"USD""#));
492    }
493
494    /// Backward-compat round-trip — make sure a legacy row, once deserialized
495    /// and re-serialized, stays self-consistent (the upgrade path doesn't lose
496    /// the legacy value).
497    #[test]
498    fn legacy_row_roundtrip_after_upgrade() {
499        let legacy = r#"{
500            "type": "boltz",
501            "swap_id": "swap_legacy",
502            "invoice": "lnbc",
503            "invoice_amount_sats": 100000,
504            "max_slippage_bps": 100,
505            "chain": "arbitrum",
506            "asset": "USDT",
507            "recipient_address": "0xr",
508            "estimated_out": "1450000",
509            "status": "Pending",
510            "fee": "1500",
511            "asset_decimals": 6
512        }"#;
513        let decoded: ConversionInfo = serde_json::from_str(legacy).unwrap();
514        let re_encoded = serde_json::to_string(&decoded).unwrap();
515        let re_decoded: ConversionInfo = serde_json::from_str(&re_encoded).unwrap();
516        assert_eq!(
517            decoded, re_decoded,
518            "upgraded row must round-trip identically"
519        );
520    }
521
522    #[test]
523    fn boltz_status_mut_updates_status_in_place() {
524        let mut info = ConversionInfo::Boltz {
525            swap_id: "s1".to_string(),
526            chain: "arbitrum".to_string(),
527            chain_id: Some("42161".to_string()),
528            asset: "USDT".to_string(),
529            recipient_address: "0xdest".to_string(),
530            invoice: "lnbc".to_string(),
531            invoice_amount_sats: 100,
532            asset_amount_in: None,
533            estimated_out: 1,
534            delivered_amount: None,
535            bridge_ref: None,
536            status: ConversionStatus::Pending,
537            fee_amount: None,
538            service_fee_amount: None,
539            service_fee_asset: None,
540            max_slippage_bps: 100,
541            quote_degraded: false,
542            asset_decimals: 6,
543            asset_contract: None,
544        };
545        *info.status_mut() = ConversionStatus::Completed;
546        assert_eq!(info.status(), &ConversionStatus::Completed);
547    }
548}
549
550pub(crate) struct TokenConversionPool {
551    pub(crate) asset_in_address: String,
552    pub(crate) asset_out_address: String,
553    pub(crate) pool: Pool,
554}
555
556pub(crate) struct TokenConversionResponse {
557    /// The sent payment id for the conversion
558    pub(crate) sent_payment_id: String,
559    /// The received payment id for the conversion
560    pub(crate) received_payment_id: String,
561}
562
563/// Options for conversion when fulfilling a payment. When set, the SDK will
564/// perform a conversion before fulfilling the payment. If not set, the payment
565/// will only be fulfilled if the wallet has sufficient balance of the required asset.
566#[derive(Debug, Clone, Serialize, PartialEq)]
567#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
568pub struct ConversionOptions {
569    /// The type of conversion to perform when fulfilling the payment
570    pub conversion_type: ConversionType,
571    /// The optional maximum slippage in basis points (1/100 of a percent) allowed when
572    /// a conversion is needed to fulfill the payment. Defaults to 10 bps (0.1%) if not set.
573    /// The conversion will fail if the actual amount received is less than
574    /// `estimated_amount * (1 - max_slippage_bps / 10_000)`.
575    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
576    pub max_slippage_bps: Option<u32>,
577    /// The optional timeout in seconds to wait for the conversion to complete
578    /// when fulfilling the payment. This timeout only concerns waiting for the received
579    /// payment of the conversion. If the timeout is reached before the conversion
580    /// is complete, the payment will fail. Defaults to 30 seconds if not set.
581    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
582    pub completion_timeout_secs: Option<u32>,
583}
584
585#[derive(Debug, Clone, Serialize, PartialEq)]
586#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
587pub enum ConversionType {
588    /// Converting from Bitcoin to a token
589    FromBitcoin,
590    /// Converting from a token to Bitcoin
591    ToBitcoin { from_token_identifier: String },
592}
593
594impl ConversionType {
595    /// Returns the asset addresses for the conversion type
596    ///
597    /// # Arguments
598    ///
599    /// * `token_identifier` - The token identifier when converting from Bitcoin to a token
600    ///
601    /// # Returns
602    ///
603    /// Result containing:
604    /// * (String, String): A tuple containing the asset in address and asset out address
605    /// * `SdkError`: If the token identifier is required but not provided
606    pub(crate) fn as_asset_addresses(
607        &self,
608        token_identifier: Option<&String>,
609    ) -> Result<(String, String), SdkError> {
610        Ok(match self {
611            ConversionType::FromBitcoin => (
612                BTC_ASSET_ADDRESS.to_string(),
613                token_identifier
614                    .ok_or(SdkError::InvalidInput(
615                        "Token identifier is required for from Bitcoin conversion".to_string(),
616                    ))?
617                    .clone(),
618            ),
619            ConversionType::ToBitcoin {
620                from_token_identifier,
621            } => (from_token_identifier.clone(), BTC_ASSET_ADDRESS.to_string()),
622        })
623    }
624}
625
626#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
627pub struct FetchConversionLimitsRequest {
628    /// The type of conversion, either from or to Bitcoin.
629    pub conversion_type: ConversionType,
630    /// The token identifier when converting to a token.
631    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
632    pub token_identifier: Option<String>,
633}
634
635#[derive(Debug, Clone, Serialize)]
636#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
637pub struct FetchConversionLimitsResponse {
638    /// The minimum amount to be converted.
639    /// Denominated in satoshis if converting from Bitcoin, otherwise in the token base units.
640    pub min_from_amount: Option<u128>,
641    /// The minimum amount to be received from the conversion.
642    /// Denominated in satoshis if converting to Bitcoin, otherwise in the token base units.
643    pub min_to_amount: Option<u128>,
644}