Skip to main content

breez_sdk_spark/models/
mod.rs

1pub(crate) mod adaptors;
2pub mod payment_observer;
3pub use payment_observer::*;
4
5// Re-export public conversion types from the conversion module
6pub use crate::token_conversion::{
7    AmountAdjustmentReason, ConversionEstimate, ConversionInfo, ConversionOptions,
8    ConversionPurpose, ConversionStatus, ConversionType, FetchConversionLimitsRequest,
9    FetchConversionLimitsResponse,
10};
11
12use core::fmt;
13use lnurl_models::RecoverLnurlPayResponse;
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16use std::{
17    collections::{HashMap, HashSet},
18    fmt::Display,
19    str::FromStr,
20};
21
22use crate::{
23    BitcoinAddressDetails, BitcoinChainService, BitcoinNetwork, Bolt11InvoiceDetails,
24    ExternalInputParser, FiatCurrency, LnurlPayRequestDetails, LnurlWithdrawRequestDetails, Rate,
25    SdkError, SparkInvoiceDetails, SuccessAction, SuccessActionProcessed,
26    cross_chain::{CrossChainFeeMode, CrossChainProviderContext, CrossChainRoutePair},
27    error::DepositClaimError,
28};
29
30/// A list of external input parsers that are used by default.
31/// To opt-out, set `use_default_external_input_parsers` in [Config] to false.
32pub const DEFAULT_EXTERNAL_INPUT_PARSERS: &[(&str, &str, &str)] = &[
33    (
34        "picknpay",
35        "(.*)(za.co.electrum.picknpay)(.*)",
36        "https://cryptoqr.net/.well-known/lnurlp/<input>",
37    ),
38    (
39        "bootleggers",
40        r"(.*)(wigroup\.co|yoyogroup\.co)(.*)",
41        "https://cryptoqr.net/.well-known/lnurlw/<input>",
42    ),
43];
44
45/// Represents the seed for wallet generation, either as a mnemonic phrase with an optional
46/// passphrase or as raw entropy bytes.
47#[derive(Debug, Clone)]
48#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
49pub enum Seed {
50    /// A BIP-39 mnemonic phrase with an optional passphrase.
51    Mnemonic {
52        /// The mnemonic phrase. 12 or 24 words.
53        mnemonic: String,
54        /// An optional passphrase for the mnemonic.
55        passphrase: Option<String>,
56    },
57    /// Raw entropy bytes.
58    Entropy(Vec<u8>),
59}
60
61impl Seed {
62    pub fn to_bytes(&self) -> Result<Vec<u8>, SdkError> {
63        match self {
64            Seed::Mnemonic {
65                mnemonic,
66                passphrase,
67            } => {
68                let mnemonic = bip39::Mnemonic::parse(mnemonic)
69                    .map_err(|e| SdkError::Generic(e.to_string()))?;
70
71                Ok(mnemonic
72                    .to_seed(passphrase.as_deref().unwrap_or(""))
73                    .to_vec())
74            }
75            Seed::Entropy(entropy) => Ok(entropy.clone()),
76        }
77    }
78}
79
80#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
81pub struct ConnectRequest {
82    pub config: Config,
83    pub seed: Seed,
84    pub storage_dir: String,
85}
86
87/// Request object for connecting to the Spark network using an external signer.
88///
89/// This allows using a custom signer implementation instead of providing a seed directly.
90#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
91#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
92pub struct ConnectWithSignerRequest {
93    pub config: Config,
94    /// External signer for non-Spark SDK signing (LNURL-auth, sync, message
95    /// signing, ECIES).
96    pub breez_signer: std::sync::Arc<dyn crate::signer::ExternalBreezSigner>,
97    /// External high-level Spark signer for the Spark wallet flows.
98    pub spark_signer: std::sync::Arc<dyn crate::signer::ExternalSparkSigner>,
99    pub storage_dir: String,
100}
101
102/// Request object for connecting to the Spark network using a signing-only
103/// external signer.
104///
105/// Use this instead of [`ConnectWithSignerRequest`] for a signer that can't
106/// perform the SDK's local ECIES/HMAC operations (for example a
107/// policy-restricted enclave). The SDK keeps session tokens in plaintext and
108/// disables the features that rely on ECIES/HMAC.
109#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
110#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
111pub struct ConnectWithSigningOnlySignerRequest {
112    pub config: Config,
113    /// Signing-only external signer for non-Spark SDK signing.
114    pub breez_signer: std::sync::Arc<dyn crate::signer::ExternalSigningSigner>,
115    /// External high-level Spark signer for the Spark wallet flows.
116    pub spark_signer: std::sync::Arc<dyn crate::signer::ExternalSparkSigner>,
117    pub storage_dir: String,
118}
119
120/// The type of payment
121#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
122#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
123pub enum PaymentType {
124    /// Payment sent from this wallet
125    Send,
126    /// Payment received to this wallet
127    Receive,
128}
129
130impl fmt::Display for PaymentType {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        match self {
133            PaymentType::Send => write!(f, "send"),
134            PaymentType::Receive => write!(f, "receive"),
135        }
136    }
137}
138
139impl FromStr for PaymentType {
140    type Err = String;
141
142    fn from_str(s: &str) -> Result<Self, Self::Err> {
143        Ok(match s.to_lowercase().as_str() {
144            "receive" => PaymentType::Receive,
145            "send" => PaymentType::Send,
146            _ => return Err(format!("invalid payment type '{s}'")),
147        })
148    }
149}
150
151/// The status of a payment
152#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
153#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
154pub enum PaymentStatus {
155    /// Payment is completed successfully
156    Completed,
157    /// Payment is in progress
158    Pending,
159    /// Payment has failed
160    Failed,
161}
162
163impl PaymentStatus {
164    /// Returns true if the payment status is final (either Completed or Failed)
165    pub fn is_final(&self) -> bool {
166        matches!(self, PaymentStatus::Completed | PaymentStatus::Failed)
167    }
168}
169
170impl fmt::Display for PaymentStatus {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        match self {
173            PaymentStatus::Completed => write!(f, "completed"),
174            PaymentStatus::Pending => write!(f, "pending"),
175            PaymentStatus::Failed => write!(f, "failed"),
176        }
177    }
178}
179
180impl FromStr for PaymentStatus {
181    type Err = String;
182
183    fn from_str(s: &str) -> Result<Self, Self::Err> {
184        Ok(match s.to_lowercase().as_str() {
185            "completed" => PaymentStatus::Completed,
186            "pending" => PaymentStatus::Pending,
187            "failed" => PaymentStatus::Failed,
188            _ => return Err(format!("Invalid payment status '{s}'")),
189        })
190    }
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
194#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
195pub enum PaymentMethod {
196    Lightning,
197    Spark,
198    Token,
199    Deposit,
200    Withdraw,
201    Unknown,
202}
203
204impl Display for PaymentMethod {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        match self {
207            PaymentMethod::Lightning => write!(f, "lightning"),
208            PaymentMethod::Spark => write!(f, "spark"),
209            PaymentMethod::Token => write!(f, "token"),
210            PaymentMethod::Deposit => write!(f, "deposit"),
211            PaymentMethod::Withdraw => write!(f, "withdraw"),
212            PaymentMethod::Unknown => write!(f, "unknown"),
213        }
214    }
215}
216
217impl FromStr for PaymentMethod {
218    type Err = ();
219
220    fn from_str(s: &str) -> Result<Self, Self::Err> {
221        match s {
222            "lightning" => Ok(PaymentMethod::Lightning),
223            "spark" => Ok(PaymentMethod::Spark),
224            "token" => Ok(PaymentMethod::Token),
225            "deposit" => Ok(PaymentMethod::Deposit),
226            "withdraw" => Ok(PaymentMethod::Withdraw),
227            "unknown" => Ok(PaymentMethod::Unknown),
228            _ => Err(()),
229        }
230    }
231}
232
233/// Represents a payment (sent or received)
234#[derive(Debug, Clone, Serialize, Deserialize)]
235#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
236pub struct Payment {
237    /// Unique identifier for the payment
238    pub id: String,
239    /// Type of payment (send or receive)
240    pub payment_type: PaymentType,
241    /// Status of the payment
242    pub status: PaymentStatus,
243    /// Amount in satoshis or token base units
244    pub amount: u128,
245    /// Fee paid in satoshis or token base units
246    pub fees: u128,
247    /// Timestamp of when the payment was created
248    pub timestamp: u64,
249    /// Method of payment. Sometimes the payment details is empty so this field
250    /// is used to determine the payment method.
251    pub method: PaymentMethod,
252    /// Details of the payment
253    pub details: Option<PaymentDetails>,
254    /// If set, this payment involved a conversion before the payment
255    pub conversion_details: Option<ConversionDetails>,
256}
257
258impl Payment {
259    /// Returns `true` if this payment is a child of a conversion operation.
260    ///
261    /// Conversion operations (stable balance, ongoing sends) create internal child
262    /// payments (send sats→Flashnet, receive tokens). These are identified by having
263    /// `conversion_info` set in their payment details.
264    pub fn is_conversion_child(&self) -> bool {
265        matches!(
266            &self.details,
267            Some(
268                PaymentDetails::Spark {
269                    conversion_info: Some(_),
270                    ..
271                } | PaymentDetails::Token {
272                    conversion_info: Some(_),
273                    ..
274                }
275            )
276        )
277    }
278}
279
280/// Outlines the steps involved in one or more conversions on a payment.
281///
282/// Built progressively: `status` is available immediately from payment metadata,
283/// while `conversions` are enriched later from child payments and conversion info.
284#[derive(Debug, Clone, Serialize, Deserialize)]
285#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
286pub struct ConversionDetails {
287    /// Overall status of the conversion (persisted in storage)
288    pub status: ConversionStatus,
289    /// Ordered list of conversion steps. For sends: [AMM, cross-chain].
290    /// For receives: [cross-chain, AMM]. Rebuilt on retrieval, not persisted.
291    #[serde(default)]
292    pub conversions: Vec<Conversion>,
293}
294
295/// The provider that performed a conversion.
296#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
297#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
298pub enum ConversionProvider {
299    /// AMM (Flashnet pool) conversion between token and BTC on Spark
300    Amm,
301    /// Orchestra cross-chain conversion
302    Orchestra,
303    /// Boltz reverse-swap cross-chain conversion
304    Boltz,
305}
306
307/// The chain or network that a [`ConversionSide`] lives on.
308#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
309#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
310pub enum ConversionChain {
311    /// Spark layer-2 network.
312    Spark,
313    /// Bitcoin Lightning Network.
314    Lightning,
315    /// An external chain reached via a cross-chain provider.
316    External {
317        /// Human-readable chain name (e.g. `"base"`, `"solana"`, `"arbitrum"`).
318        name: String,
319        /// Stable chain identifier (e.g. EVM `chainId` as a decimal string,
320        /// or a chain-native identifier). `None` when the provider does not
321        /// expose one for this route.
322        chain_id: Option<String>,
323    },
324}
325
326/// The asset on a [`ConversionSide`] — groups the ticker, stable identifier,
327/// and decimals that always travel together.
328#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
329#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
330pub struct ConversionAsset {
331    /// Ticker (e.g. `"BTC"`, `"USDB"`, `"USDC"`, `"USDT"`). Tickers alone
332    /// are ambiguous across chains — pair with [`Self::identifier`] for a
333    /// hard match.
334    pub ticker: String,
335    /// Stable identifier: a Spark token identifier for Spark tokens, or a
336    /// contract/mint address for cross-chain assets. `None` for BTC/sats.
337    pub identifier: Option<String>,
338    /// Number of decimals for the asset.
339    /// `0` for BTC/sats sides (amount is already in the smallest unit,
340    /// so no scaling is needed); non-zero for token assets (e.g. `6` for
341    /// USDC/USDT/USDB).
342    pub decimals: u32,
343}
344
345/// One side (source or destination) of a conversion.
346#[derive(Debug, Clone, Serialize, Deserialize)]
347#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
348pub struct ConversionSide {
349    /// The chain or network for this side.
350    pub chain: ConversionChain,
351    /// The asset being converted on this side.
352    pub asset: ConversionAsset,
353    /// Amount in base units (satoshis or token base units)
354    pub amount: u128,
355    /// Fee in the same base units
356    pub fee: u128,
357}
358
359/// A single conversion in a payment's conversion chain.
360#[derive(Debug, Clone, Serialize, Deserialize)]
361#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
362pub struct Conversion {
363    /// The provider that performed this conversion
364    pub provider: ConversionProvider,
365    /// Status of this specific conversion step
366    pub status: ConversionStatus,
367    /// Source side of the conversion
368    pub from: ConversionSide,
369    /// Destination side of the conversion
370    pub to: ConversionSide,
371    /// Reason the conversion amount was adjusted, if applicable (AMM only)
372    #[serde(default)]
373    pub amount_adjustment: Option<AmountAdjustmentReason>,
374}
375
376#[cfg(feature = "uniffi")]
377uniffi::custom_type!(u128, String, {
378    remote,
379    try_lift: |val| val.parse::<u128>().map_err(uniffi::deps::anyhow::Error::msg),
380    lower: |obj| obj.to_string(),
381});
382
383// TODO: fix large enum variant lint - may be done by boxing lnurl_pay_info but that requires
384//  some changes to the wasm bindgen macro
385#[allow(clippy::large_enum_variant)]
386#[derive(Debug, Clone, Serialize, Deserialize)]
387#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
388pub enum PaymentDetails {
389    Spark {
390        /// The invoice details if the payment fulfilled a spark invoice
391        invoice_details: Option<SparkInvoicePaymentDetails>,
392        /// The HTLC transfer details if the payment fulfilled an HTLC transfer
393        htlc_details: Option<SparkHtlcDetails>,
394        /// The information for a conversion
395        conversion_info: Option<ConversionInfo>,
396    },
397    Token {
398        metadata: TokenMetadata,
399        tx_hash: String,
400        tx_type: TokenTransactionType,
401        /// The invoice details if the payment fulfilled a spark invoice
402        invoice_details: Option<SparkInvoicePaymentDetails>,
403        /// The information for a conversion
404        conversion_info: Option<ConversionInfo>,
405    },
406    Lightning {
407        /// Represents the invoice description
408        description: Option<String>,
409        /// Represents the Bolt11/Bolt12 invoice associated with a payment
410        /// In the case of a Send payment, this is the invoice paid by the user
411        /// In the case of a Receive payment, this is the invoice paid to the user
412        invoice: String,
413
414        /// The invoice destination/payee pubkey
415        destination_pubkey: String,
416
417        /// The HTLC transfer details
418        htlc_details: SparkHtlcDetails,
419
420        /// Lnurl payment information if this was an lnurl payment.
421        lnurl_pay_info: Option<LnurlPayInfo>,
422
423        /// Lnurl withdrawal information if this was an lnurl payment.
424        lnurl_withdraw_info: Option<LnurlWithdrawInfo>,
425
426        /// Lnurl receive information if this was a received lnurl payment.
427        lnurl_receive_metadata: Option<LnurlReceiveMetadata>,
428
429        /// The information for a conversion — populated when this Lightning
430        /// payment is the source leg of a cross-chain conversion (e.g. a
431        /// Boltz reverse swap paying a hold invoice).
432        conversion_info: Option<ConversionInfo>,
433    },
434    Withdraw {
435        tx_id: String,
436    },
437    Deposit {
438        tx_id: String,
439        vout: u32,
440    },
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
444#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
445pub enum TokenTransactionType {
446    Transfer,
447    Mint,
448    Burn,
449}
450
451impl fmt::Display for TokenTransactionType {
452    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
453        match self {
454            TokenTransactionType::Transfer => write!(f, "transfer"),
455            TokenTransactionType::Mint => write!(f, "mint"),
456            TokenTransactionType::Burn => write!(f, "burn"),
457        }
458    }
459}
460
461impl FromStr for TokenTransactionType {
462    type Err = String;
463
464    fn from_str(s: &str) -> Result<Self, Self::Err> {
465        match s.to_lowercase().as_str() {
466            "transfer" => Ok(TokenTransactionType::Transfer),
467            "mint" => Ok(TokenTransactionType::Mint),
468            "burn" => Ok(TokenTransactionType::Burn),
469            _ => Err(format!("Invalid token transaction type '{s}'")),
470        }
471    }
472}
473
474#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
475#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
476pub struct SparkInvoicePaymentDetails {
477    /// Represents the spark invoice description
478    pub description: Option<String>,
479    /// The raw spark invoice string
480    pub invoice: String,
481}
482
483#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
484#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
485pub struct SparkHtlcDetails {
486    /// The payment hash of the HTLC
487    pub payment_hash: String,
488    /// The preimage of the HTLC. Empty until receiver has released it.
489    pub preimage: Option<String>,
490    /// The expiry time of the HTLC as a unix timestamp in seconds
491    pub expiry_time: u64,
492    /// The HTLC status
493    pub status: SparkHtlcStatus,
494}
495
496#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
497#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
498pub enum SparkHtlcStatus {
499    /// The HTLC is waiting for the preimage to be shared by the receiver
500    WaitingForPreimage,
501    /// The HTLC preimage has been shared and the transfer can be or has been claimed by the receiver
502    PreimageShared,
503    /// The HTLC has been returned to the sender due to expiry
504    Returned,
505}
506
507impl fmt::Display for SparkHtlcStatus {
508    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
509        match self {
510            SparkHtlcStatus::WaitingForPreimage => write!(f, "WaitingForPreimage"),
511            SparkHtlcStatus::PreimageShared => write!(f, "PreimageShared"),
512            SparkHtlcStatus::Returned => write!(f, "Returned"),
513        }
514    }
515}
516
517impl FromStr for SparkHtlcStatus {
518    type Err = String;
519
520    fn from_str(s: &str) -> Result<Self, Self::Err> {
521        match s {
522            "WaitingForPreimage" => Ok(SparkHtlcStatus::WaitingForPreimage),
523            "PreimageShared" => Ok(SparkHtlcStatus::PreimageShared),
524            "Returned" => Ok(SparkHtlcStatus::Returned),
525            _ => Err("Invalid Spark HTLC status".to_string()),
526        }
527    }
528}
529
530#[derive(Debug, Clone, Copy, PartialEq, Eq)]
531#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
532pub enum Network {
533    Mainnet,
534    Regtest,
535}
536
537impl std::fmt::Display for Network {
538    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
539        match self {
540            Network::Mainnet => write!(f, "Mainnet"),
541            Network::Regtest => write!(f, "Regtest"),
542        }
543    }
544}
545
546impl From<Network> for BitcoinNetwork {
547    fn from(network: Network) -> Self {
548        match network {
549            Network::Mainnet => BitcoinNetwork::Bitcoin,
550            Network::Regtest => BitcoinNetwork::Regtest,
551        }
552    }
553}
554
555impl From<Network> for breez_sdk_common::network::BitcoinNetwork {
556    fn from(network: Network) -> Self {
557        match network {
558            Network::Mainnet => breez_sdk_common::network::BitcoinNetwork::Bitcoin,
559            Network::Regtest => breez_sdk_common::network::BitcoinNetwork::Regtest,
560        }
561    }
562}
563
564impl From<Network> for bitcoin::Network {
565    fn from(network: Network) -> Self {
566        match network {
567            Network::Mainnet => bitcoin::Network::Bitcoin,
568            Network::Regtest => bitcoin::Network::Regtest,
569        }
570    }
571}
572
573impl FromStr for Network {
574    type Err = String;
575
576    fn from_str(s: &str) -> Result<Self, Self::Err> {
577        match s {
578            "mainnet" => Ok(Network::Mainnet),
579            "regtest" => Ok(Network::Regtest),
580            _ => Err("Invalid network".to_string()),
581        }
582    }
583}
584
585#[derive(Debug, Clone)]
586#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
587#[allow(clippy::struct_excessive_bools)]
588pub struct Config {
589    pub api_key: Option<String>,
590    pub network: Network,
591    pub sync_interval_secs: u32,
592
593    // The maximum fee that can be paid for a static deposit claim
594    // If not set then any fee is allowed
595    pub max_deposit_claim_fee: Option<MaxFee>,
596
597    /// The domain used for receiving through lnurl-pay and lightning address.
598    pub lnurl_domain: Option<String>,
599
600    /// When this is set to `true` we will prefer to use spark payments over
601    /// lightning when sending and receiving. This has the benefit of lower fees
602    /// but is at the cost of privacy.
603    pub prefer_spark_over_lightning: bool,
604
605    /// A set of external input parsers that are used by [`BreezSdk::parse`](crate::sdk::BreezSdk::parse) when the input
606    /// is not recognized. See [`ExternalInputParser`] for more details on how to configure
607    /// external parsing.
608    pub external_input_parsers: Option<Vec<ExternalInputParser>>,
609    /// The SDK includes some default external input parsers
610    /// ([`DEFAULT_EXTERNAL_INPUT_PARSERS`]).
611    /// Set this to false in order to prevent their use.
612    pub use_default_external_input_parsers: bool,
613
614    /// Url to use for the real-time sync server. Defaults to the Breez real-time sync server.
615    pub real_time_sync_server_url: Option<String>,
616
617    /// Whether the Spark private mode is enabled by default.
618    ///
619    /// If set to true, the Spark private mode will be enabled on the first
620    /// initialization of the SDK. If set to false, no changes will be made
621    /// to the Spark private mode.
622    ///
623    /// This default is only auto-applied when `background_tasks_enabled` is
624    /// `true`. When `background_tasks_enabled` is `false`, the SDK does not
625    /// touch the Spark private mode on startup; call `update_user_settings`
626    /// with `spark_private_mode_enabled` set as needed on a one-time setup
627    /// pass.
628    pub private_enabled_default: bool,
629
630    /// Configuration for leaf optimization.
631    ///
632    /// Leaf optimization controls the denominations of leaves that are held in the wallet.
633    /// Fewer, bigger leaves allow for more funds to be exited unilaterally.
634    /// More leaves allow payments to be made without needing a swap, reducing payment latency.
635    pub leaf_optimization_config: LeafOptimizationConfig,
636
637    /// Configuration for token-output optimization.
638    ///
639    /// Token-output optimization controls automatic consolidation of a token's
640    /// available outputs. Keeping the output set small reduces transaction size,
641    /// while keeping enough distinct outputs preserves concurrency for parallel
642    /// sends.
643    pub token_optimization_config: TokenOptimizationConfig,
644
645    /// Configuration for automatic conversion of Bitcoin to stable tokens.
646    ///
647    /// When set, received sats will be automatically converted to the specified token
648    /// once the balance exceeds the threshold.
649    pub stable_balance_config: Option<StableBalanceConfig>,
650
651    /// Maximum number of concurrent transfer claims.
652    ///
653    /// Default is 4. Increase for server environments with high incoming payment volume.
654    pub max_concurrent_claims: u32,
655
656    /// Optional custom Spark environment configuration.
657    ///
658    /// When set, overrides the default Spark operator pool, service provider,
659    /// threshold, and token settings. Use this to connect to alternative Spark
660    /// deployments (e.g. dev/staging environments).
661    pub spark_config: Option<SparkConfig>,
662
663    /// Master switch for per-instance background services.
664    ///
665    /// When `true` (default), the SDK runs its standard background work:
666    /// periodic sync, lightning-address recovery, private-mode initialization,
667    /// the leaf and token-output optimizers, the Spark server-event
668    /// subscription, and the real-time sync client (when
669    /// `real_time_sync_server_url` is set).
670    ///
671    /// When `false`, **no background service is started**, regardless of any
672    /// other setting on this config. This is intended for multi-tenant server
673    /// deployments where the host application orchestrates sync and claims
674    /// explicitly and receives events via webhooks. Use
675    /// `default_server_config` to get this preset.
676    ///
677    /// Explicit operations (`sync_wallet`, `claim_deposit`,
678    /// `list_unclaimed_deposits`, `refund_deposit`,
679    /// `refund_pending_conversions`, leaf/token optimization, etc.) work
680    /// regardless of this flag.
681    ///
682    /// When `false`, the SDK rejects builds where fields whose backing
683    /// service is gated off are still in their active shape:
684    /// `stable_balance_config` must be `None`, `real_time_sync_server_url`
685    /// must be `None`, and `optimization_config.auto_enabled` must be `false`.
686    /// `default_server_config` already sets these compatible values.
687    pub background_tasks_enabled: bool,
688
689    /// Configuration for cross-chain sends via Orchestra and Boltz.
690    ///
691    /// `Some(_)` enables cross-chain sends (sats to USDT on external chains).
692    /// `None` (default) disables them entirely. Opt in by setting this to
693    /// [`CrossChainConfig::default`] (or a customized value): the providers
694    /// run background work (e.g. web sockets), so enabling is left to the
695    /// caller. Cross-chain sends are only supported on mainnet.
696    pub cross_chain_config: Option<CrossChainConfig>,
697}
698
699/// Configuration for cross-chain sends.
700///
701/// The presence of this struct on [`Config::cross_chain_config`] enables
702/// cross-chain providers; `None` disables them.
703#[derive(Debug, Clone, Default)]
704#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
705pub struct CrossChainConfig {
706    /// Default maximum slippage in basis points used when
707    /// [`PaymentRequest::CrossChain::max_slippage_bps`] is not set on the
708    /// prepare request. Must be in `10..=500`. Falls back to 100 bps (1%)
709    /// when this field is `None`.
710    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
711    pub default_slippage_bps: Option<u32>,
712    /// Default target-overpay pad in basis points applied to the user's
713    /// destination amount on `FeesExcluded` conversion sends. Bumps the
714    /// target upward before quoting so the recipient lands at or above the
715    /// requested amount despite provider slippage. Must be in `0..=500`.
716    /// Falls back to 15 bps when `None`.
717    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
718    pub default_target_overpay_bps: Option<u32>,
719}
720
721/// Configuration for leaf optimization.
722#[derive(Debug, Clone)]
723#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
724pub struct LeafOptimizationConfig {
725    /// Whether automatic leaf optimization is enabled.
726    ///
727    /// If set to true, the SDK will automatically optimize the leaf set when it changes.
728    /// Otherwise, the manual optimization API must be used to optimize the leaf set.
729    ///
730    /// Default value is true.
731    pub auto_enabled: bool,
732    /// The desired multiplicity for the leaf set.
733    ///
734    /// Setting this to 0 will optimize for maximizing unilateral exit.
735    /// Higher values will optimize for minimizing transfer swaps, with higher values
736    /// being more aggressive and allowing better TPS rates.
737    ///
738    /// For end-user wallets, values of 1-5 are recommended. Values above 5 are
739    /// intended for high-throughput server environments and are not recommended
740    /// for end-user wallets due to significantly higher unilateral exit costs.
741    ///
742    /// Default value is 1.
743    pub multiplicity: u8,
744}
745
746/// Configuration for token-output optimization.
747#[derive(Debug, Clone)]
748#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
749pub struct TokenOptimizationConfig {
750    /// Whether automatic token-output consolidation is enabled.
751    ///
752    /// If set to true, the SDK will periodically consolidate a token's outputs
753    /// once their count exceeds [`Self::min_outputs_threshold`]. Otherwise, no
754    /// automatic consolidation is performed.
755    ///
756    /// Default value is true.
757    pub auto_enabled: bool,
758    /// Number of token outputs to produce when token-output auto-consolidation
759    /// fires.
760    ///
761    /// Instead of collapsing a token's outputs into a single output (which
762    /// serializes subsequent payments), the SDK splits the consolidated balance
763    /// across this many outputs of roughly equal value. Higher values preserve
764    /// concurrency for parallel sends at the cost of a slightly larger output
765    /// set.
766    ///
767    /// Must be >= 1 and strictly less than [`Self::min_outputs_threshold`].
768    ///
769    /// Default value is 5.
770    pub target_output_count: u32,
771    /// Output count that triggers per-token auto-consolidation.
772    ///
773    /// Auto-consolidation triggers for a token when its available output count
774    /// strictly exceeds this threshold.
775    ///
776    /// Must be greater than 1.
777    ///
778    /// Default value is 50.
779    pub min_outputs_threshold: u32,
780}
781
782/// A stable token that can be used for automatic balance conversion.
783#[derive(Debug, Clone)]
784#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
785pub struct StableBalanceToken {
786    /// Integrator-defined display label for the token, e.g. "USD".
787    ///
788    /// This is a short, human-readable name set by the integrator for display purposes.
789    /// It is **not** a canonical Spark token ticker — it has no protocol-level meaning.
790    /// Labels must be unique within the [`StableBalanceConfig::tokens`] list.
791    pub label: String,
792
793    /// The full token identifier string used for conversions.
794    pub token_identifier: String,
795}
796
797/// Configuration for automatic conversion of Bitcoin to stable tokens.
798///
799/// When configured, the SDK automatically monitors the Bitcoin balance after each
800/// wallet sync. When the balance exceeds the configured threshold plus the reserved
801/// amount, the SDK automatically converts the excess balance (above the reserve)
802/// to the active stable token.
803///
804/// When the balance is held in a stable token, Bitcoin payments can still be sent.
805/// The SDK automatically detects when there's not enough Bitcoin balance to cover a
806/// payment and auto-populates the token-to-Bitcoin conversion options to facilitate
807/// the payment.
808///
809/// The active token can be changed at runtime via [`UpdateUserSettingsRequest`].
810#[derive(Debug, Clone)]
811#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
812pub struct StableBalanceConfig {
813    /// Available tokens that can be used for stable balance.
814    pub tokens: Vec<StableBalanceToken>,
815
816    /// The label of the token to activate by default.
817    ///
818    /// If `None`, stable balance starts deactivated. The user can activate it
819    /// at runtime via [`UpdateUserSettingsRequest`]. If a user setting is cached
820    /// locally, it takes precedence over this default.
821    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
822    pub default_active_label: Option<String>,
823
824    /// The minimum sats balance that triggers auto-conversion.
825    ///
826    /// If not provided, uses the minimum from conversion limits.
827    /// If provided but less than the conversion limit minimum, the limit minimum is used.
828    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
829    pub threshold_sats: Option<u64>,
830
831    /// Maximum slippage in basis points (1/100 of a percent).
832    ///
833    /// Defaults to 10 bps (0.1%) if not set.
834    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
835    pub max_slippage_bps: Option<u32>,
836}
837
838/// Specifies how to update the active stable balance token.
839#[derive(Debug, Clone)]
840#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
841pub enum StableBalanceActiveLabel {
842    /// Activate stable balance with the given label.
843    Set { label: String },
844    /// Deactivate stable balance.
845    Unset,
846}
847
848/// Configuration for a custom Spark environment.
849///
850/// When set on [`Config`], overrides the default Spark operator pool,
851/// service provider, threshold, and token settings. This allows connecting
852/// to alternative Spark deployments (e.g. dev/staging environments).
853#[derive(Debug, Clone)]
854#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
855pub struct SparkConfig {
856    /// Hex-encoded identifier of the coordinator operator.
857    pub coordinator_identifier: String,
858    /// The FROST signing threshold (e.g. 2 of 3).
859    pub threshold: u32,
860    /// The set of signing operators.
861    pub signing_operators: Vec<SparkSigningOperator>,
862    /// Service provider (SSP) configuration.
863    pub ssp_config: SparkSspConfig,
864    /// Expected bond amount in sats for token withdrawals.
865    pub expected_withdraw_bond_sats: u64,
866    /// Expected relative block locktime for token withdrawals.
867    pub expected_withdraw_relative_block_locktime: u64,
868    /// Cap on the inputs a single token transaction may spend. A send needing
869    /// more first consolidates the wallet's token outputs. Unset uses the SDK
870    /// default (500).
871    pub max_token_transaction_inputs: Option<u32>,
872}
873
874/// A Spark signing operator.
875#[derive(Debug, Clone)]
876#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
877pub struct SparkSigningOperator {
878    /// Sequential operator ID (0-indexed).
879    pub id: u32,
880    /// Hex-encoded 32-byte FROST identifier.
881    pub identifier: String,
882    /// gRPC address of the operator (e.g. `https://0.spark.lightspark.com`).
883    pub address: String,
884    /// Hex-encoded compressed public key of the operator.
885    pub identity_public_key: String,
886    /// Optional PEM-encoded CA certificate for TLS verification.
887    /// When set, the SDK uses this CA to verify the operator's TLS certificate
888    /// instead of the system/default roots. Useful for local development with
889    /// self-signed certificates.
890    pub ca_cert_pem: Option<String>,
891}
892
893/// Configuration for the Spark Service Provider (SSP).
894#[derive(Debug, Clone)]
895#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
896pub struct SparkSspConfig {
897    /// Base URL of the SSP GraphQL API.
898    pub base_url: String,
899    /// Hex-encoded compressed public key of the SSP.
900    pub identity_public_key: String,
901    /// Optional GraphQL schema endpoint path (e.g. "graphql/spark/rc").
902    /// Defaults to the hardcoded schema endpoint if not set.
903    pub schema_endpoint: Option<String>,
904}
905
906impl Config {
907    /// Validates the configuration.
908    ///
909    /// Returns an error if any configuration values are invalid.
910    pub fn validate(&self) -> Result<(), SdkError> {
911        if self.max_concurrent_claims == 0 {
912            return Err(SdkError::InvalidInput(
913                "max_concurrent_claims must be greater than 0".to_string(),
914            ));
915        }
916
917        if let Some(sb) = &self.stable_balance_config {
918            if sb.tokens.is_empty() {
919                return Err(SdkError::InvalidInput(
920                    "tokens must not be empty".to_string(),
921                ));
922            }
923
924            let mut seen_labels = HashSet::new();
925            let mut seen_identifiers = HashSet::new();
926            for token in &sb.tokens {
927                if token.label.is_empty() {
928                    return Err(SdkError::InvalidInput(
929                        "token label must not be empty".to_string(),
930                    ));
931                }
932                if token.token_identifier.is_empty() {
933                    return Err(SdkError::InvalidInput(
934                        "token_identifier must not be empty".to_string(),
935                    ));
936                }
937                if !seen_labels.insert(&token.label) {
938                    return Err(SdkError::InvalidInput(format!(
939                        "tokens contains duplicate label: {}",
940                        token.label
941                    )));
942                }
943                if !seen_identifiers.insert(&token.token_identifier) {
944                    return Err(SdkError::InvalidInput(format!(
945                        "tokens contains duplicate token_identifier: {}",
946                        token.token_identifier
947                    )));
948                }
949            }
950
951            if let Some(bps) = sb.max_slippage_bps
952                && bps > 10000
953            {
954                return Err(SdkError::InvalidInput(
955                    "max_slippage_bps must be <= 10000".to_string(),
956                ));
957            }
958
959            if let Some(default_label) = &sb.default_active_label
960                && !seen_labels.contains(default_label)
961            {
962                return Err(SdkError::InvalidInput(format!(
963                    "default_active_label '{default_label}' not found in tokens list"
964                )));
965            }
966        }
967
968        let token_opt = &self.token_optimization_config;
969        if token_opt.min_outputs_threshold <= 1 {
970            return Err(SdkError::InvalidInput(
971                "token optimization minimum outputs threshold must be greater than 1".to_string(),
972            ));
973        }
974        if token_opt.target_output_count < 1 {
975            return Err(SdkError::InvalidInput(
976                "token optimization target output count must be at least 1".to_string(),
977            ));
978        }
979        if token_opt.target_output_count >= token_opt.min_outputs_threshold {
980            return Err(SdkError::InvalidInput(
981                "token optimization target output count must be less than the minimum outputs threshold".to_string(),
982            ));
983        }
984
985        if let Some(cc) = &self.cross_chain_config {
986            if self.network != Network::Mainnet {
987                return Err(SdkError::InvalidInput(format!(
988                    "Cross-chain sends are only available on Mainnet, not on {}.",
989                    self.network,
990                )));
991            }
992            if let Some(bps) = cc.default_slippage_bps
993                && !(crate::cross_chain::MIN_CROSS_CHAIN_SLIPPAGE_BPS
994                    ..=crate::cross_chain::MAX_CROSS_CHAIN_SLIPPAGE_BPS)
995                    .contains(&bps)
996            {
997                return Err(SdkError::InvalidInput(format!(
998                    "Default cross-chain slippage must be between {} and {} basis points, but got {bps}.",
999                    crate::cross_chain::MIN_CROSS_CHAIN_SLIPPAGE_BPS,
1000                    crate::cross_chain::MAX_CROSS_CHAIN_SLIPPAGE_BPS,
1001                )));
1002            }
1003            if let Some(bps) = cc.default_target_overpay_bps
1004                && !(crate::cross_chain::MIN_TARGET_OVERPAY_BPS
1005                    ..=crate::cross_chain::MAX_TARGET_OVERPAY_BPS)
1006                    .contains(&bps)
1007            {
1008                return Err(SdkError::InvalidInput(format!(
1009                    "Default cross-chain target-overpay must be between {} and {} basis points, but got {bps}.",
1010                    crate::cross_chain::MIN_TARGET_OVERPAY_BPS,
1011                    crate::cross_chain::MAX_TARGET_OVERPAY_BPS,
1012                )));
1013            }
1014        }
1015
1016        Ok(())
1017    }
1018
1019    pub(crate) fn get_all_external_input_parsers(&self) -> Vec<ExternalInputParser> {
1020        let mut external_input_parsers = Vec::new();
1021        if self.use_default_external_input_parsers {
1022            let default_parsers = DEFAULT_EXTERNAL_INPUT_PARSERS
1023                .iter()
1024                .map(|(id, regex, url)| ExternalInputParser {
1025                    provider_id: (*id).to_string(),
1026                    input_regex: (*regex).to_string(),
1027                    parser_url: (*url).to_string(),
1028                })
1029                .collect::<Vec<_>>();
1030            external_input_parsers.extend(default_parsers);
1031        }
1032        external_input_parsers.extend(self.external_input_parsers.clone().unwrap_or_default());
1033
1034        external_input_parsers
1035    }
1036}
1037
1038#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1039#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1040pub enum MaxFee {
1041    // Fixed fee amount in sats
1042    Fixed { amount: u64 },
1043    // Relative fee rate in satoshis per vbyte
1044    Rate { sat_per_vbyte: u64 },
1045    // Fastest network recommended fee at the time of claim, with a leeway in satoshis per vbyte
1046    NetworkRecommended { leeway_sat_per_vbyte: u64 },
1047}
1048
1049impl MaxFee {
1050    pub(crate) async fn to_fee(&self, client: &dyn BitcoinChainService) -> Result<Fee, SdkError> {
1051        match self {
1052            MaxFee::Fixed { amount } => Ok(Fee::Fixed { amount: *amount }),
1053            MaxFee::Rate { sat_per_vbyte } => Ok(Fee::Rate {
1054                sat_per_vbyte: *sat_per_vbyte,
1055            }),
1056            MaxFee::NetworkRecommended {
1057                leeway_sat_per_vbyte,
1058            } => {
1059                let recommended_fees = client.recommended_fees().await?;
1060                let max_fee_rate = recommended_fees
1061                    .fastest_fee
1062                    .saturating_add(*leeway_sat_per_vbyte);
1063                Ok(Fee::Rate {
1064                    sat_per_vbyte: max_fee_rate,
1065                })
1066            }
1067        }
1068    }
1069}
1070
1071#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1072#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1073pub enum Fee {
1074    // Fixed fee amount in sats
1075    Fixed { amount: u64 },
1076    // Relative fee rate in satoshis per vbyte
1077    Rate { sat_per_vbyte: u64 },
1078}
1079
1080impl Fee {
1081    pub fn to_sats(&self, vbytes: u64) -> u64 {
1082        match self {
1083            Fee::Fixed { amount } => *amount,
1084            Fee::Rate { sat_per_vbyte } => sat_per_vbyte.saturating_mul(vbytes),
1085        }
1086    }
1087}
1088
1089#[derive(Debug, Clone, Serialize, Deserialize)]
1090#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1091pub struct DepositInfo {
1092    pub txid: String,
1093    pub vout: u32,
1094    pub amount_sats: u64,
1095    pub is_mature: bool,
1096    pub refund_tx: Option<String>,
1097    pub refund_tx_id: Option<String>,
1098    pub claim_error: Option<DepositClaimError>,
1099}
1100
1101#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1102pub struct ClaimDepositRequest {
1103    pub txid: String,
1104    pub vout: u32,
1105    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1106    pub max_fee: Option<MaxFee>,
1107}
1108
1109#[derive(Debug, Clone, Serialize)]
1110#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1111pub struct ClaimDepositResponse {
1112    pub payment: Payment,
1113}
1114
1115#[derive(Debug, Clone, Serialize)]
1116#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1117pub struct RefundDepositRequest {
1118    pub txid: String,
1119    pub vout: u32,
1120    pub destination_address: String,
1121    pub fee: Fee,
1122}
1123
1124#[derive(Debug, Clone, Serialize)]
1125#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1126pub struct RefundDepositResponse {
1127    pub tx_id: String,
1128    pub tx_hex: String,
1129}
1130
1131#[derive(Debug, Clone, Serialize)]
1132#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1133pub struct ListUnclaimedDepositsRequest {}
1134
1135#[derive(Debug, Clone, Serialize)]
1136#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1137pub struct ListUnclaimedDepositsResponse {
1138    pub deposits: Vec<DepositInfo>,
1139}
1140
1141/// The available providers for buying Bitcoin
1142/// Request to buy Bitcoin using an external provider.
1143///
1144/// Each variant carries only the parameters relevant to that provider.
1145#[derive(Debug, Clone)]
1146#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1147pub enum BuyBitcoinRequest {
1148    /// `MoonPay`: Fiat-to-Bitcoin via credit card, Apple Pay, etc.
1149    /// Uses an on-chain deposit address.
1150    Moonpay {
1151        /// Lock the purchase to a specific amount in satoshis.
1152        locked_amount_sat: Option<u64>,
1153        /// Custom redirect URL after purchase completion.
1154        redirect_url: Option<String>,
1155    },
1156    /// `CashApp`: Pay via the Lightning Network.
1157    /// Generates a bolt11 invoice for the given amount and returns a
1158    /// `cash.app` deep link. Only available on mainnet.
1159    ///
1160    /// The amount is required. With an amountless invoice, Cash App only
1161    /// lets the payer fund from their existing Cash App BTC balance. With
1162    /// a fixed-amount invoice, Cash App opens up funding via fiat balance
1163    /// and debit card.
1164    CashApp {
1165        /// Amount in satoshis for the Lightning invoice. Must be non-zero.
1166        amount_sats: u64,
1167    },
1168}
1169
1170impl Default for BuyBitcoinRequest {
1171    fn default() -> Self {
1172        Self::Moonpay {
1173            locked_amount_sat: None,
1174            redirect_url: None,
1175        }
1176    }
1177}
1178
1179/// Response containing a URL to complete the Bitcoin purchase
1180#[derive(Debug, Clone, Serialize)]
1181#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1182pub struct BuyBitcoinResponse {
1183    /// The URL to open in a browser to complete the purchase
1184    pub url: String,
1185}
1186
1187impl std::fmt::Display for MaxFee {
1188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1189        match self {
1190            MaxFee::Fixed { amount } => write!(f, "Fixed: {amount}"),
1191            MaxFee::Rate { sat_per_vbyte } => write!(f, "Rate: {sat_per_vbyte}"),
1192            MaxFee::NetworkRecommended {
1193                leeway_sat_per_vbyte,
1194            } => write!(f, "NetworkRecommended: {leeway_sat_per_vbyte}"),
1195        }
1196    }
1197}
1198
1199#[derive(Debug, Clone)]
1200#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1201pub struct Credentials {
1202    pub username: String,
1203    pub password: String,
1204}
1205
1206/// Request to get the balance of the wallet
1207#[derive(Debug, Clone)]
1208#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1209pub struct GetInfoRequest {
1210    /// When `Some(true)`, and `background_tasks_enabled` is `true`, the call
1211    /// waits for the initial Full sync to complete before returning.
1212    ///
1213    /// When `background_tasks_enabled` is `false`, setting this to `Some(true)`
1214    /// is rejected with an invalid-input error. There is no background sync to
1215    /// wait on; call `sync_wallet` explicitly first if you need fresh state.
1216    pub ensure_synced: Option<bool>,
1217}
1218
1219/// Response containing the balance of the wallet
1220#[derive(Debug, Clone, Serialize)]
1221#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1222pub struct GetInfoResponse {
1223    /// The identity public key of the wallet as a hex string
1224    pub identity_pubkey: String,
1225    /// The balance in satoshis
1226    pub balance_sats: u64,
1227    /// The balances of the tokens in the wallet keyed by the token identifier
1228    pub token_balances: HashMap<String, TokenBalance>,
1229}
1230
1231#[derive(Debug, Clone, Serialize, Deserialize)]
1232#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1233pub struct TokenBalance {
1234    pub balance: u128,
1235    pub token_metadata: TokenMetadata,
1236}
1237
1238#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1239#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1240pub struct TokenMetadata {
1241    pub identifier: String,
1242    /// Hex representation of the issuer public key
1243    pub issuer_public_key: String,
1244    pub name: String,
1245    pub ticker: String,
1246    /// Number of decimals the token uses
1247    pub decimals: u32,
1248    pub max_supply: u128,
1249    pub is_freezable: bool,
1250}
1251
1252/// Request to sync the wallet with the Spark network
1253#[derive(Debug, Clone)]
1254#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1255pub struct SyncWalletRequest {}
1256
1257/// Response from synchronizing the wallet
1258#[derive(Debug, Clone, Serialize)]
1259#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1260pub struct SyncWalletResponse {}
1261
1262#[derive(Debug, Clone, Serialize)]
1263#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1264pub enum ReceivePaymentMethod {
1265    SparkAddress,
1266    SparkInvoice {
1267        /// Amount to receive. Denominated in sats if token identifier is empty, otherwise in the token base units
1268        amount: Option<u128>,
1269        /// The presence of this field indicates that the payment is for a token
1270        /// If empty, it is a Bitcoin payment
1271        token_identifier: Option<String>,
1272        /// The expiry time of the invoice as a unix timestamp in seconds
1273        expiry_time: Option<u64>,
1274        /// A description to embed in the invoice.
1275        description: Option<String>,
1276        /// If set, the invoice may only be fulfilled by a payer with this public key
1277        sender_public_key: Option<String>,
1278    },
1279    BitcoinAddress {
1280        /// If true, rotate to a new deposit address. Previous ones remain valid.
1281        /// If false or absent, return the existing address (creating one if none
1282        /// exists yet).
1283        new_address: Option<bool>,
1284    },
1285    Bolt11Invoice {
1286        description: String,
1287        amount_sats: Option<u64>,
1288        /// The expiry of the invoice as a duration in seconds
1289        expiry_secs: Option<u32>,
1290        /// If set, creates a HODL invoice with this payment hash (hex-encoded).
1291        /// The payer's HTLC will be held until the preimage is provided via
1292        /// `claim_htlc_payment` or the HTLC expires.
1293        payment_hash: Option<String>,
1294    },
1295}
1296
1297#[derive(Debug, Clone, Serialize)]
1298#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1299pub enum SendPaymentMethod {
1300    BitcoinAddress {
1301        address: BitcoinAddressDetails,
1302        fee_quote: SendOnchainFeeQuote,
1303    },
1304    Bolt11Invoice {
1305        invoice_details: Bolt11InvoiceDetails,
1306        spark_transfer_fee_sats: Option<u64>,
1307        lightning_fee_sats: u64,
1308    }, // should be replaced with the parsed invoice
1309    SparkAddress {
1310        address: String,
1311        /// Fee to pay for the transaction
1312        /// Denominated in sats if token identifier is empty, otherwise in the token base units
1313        fee: u128,
1314        /// The presence of this field indicates that the payment is for a token
1315        /// If empty, it is a Bitcoin payment
1316        token_identifier: Option<String>,
1317    },
1318    SparkInvoice {
1319        spark_invoice_details: SparkInvoiceDetails,
1320        /// Fee to pay for the transaction
1321        /// Denominated in sats if token identifier is empty, otherwise in the token base units
1322        fee: u128,
1323        /// The presence of this field indicates that the payment is for a token
1324        /// If empty, it is a Bitcoin payment
1325        token_identifier: Option<String>,
1326    },
1327    /// A cross-chain send via a bridge/swap provider.
1328    CrossChainAddress {
1329        /// The route selected for this cross-chain send (includes provider, chain, asset).
1330        route: CrossChainRoutePair,
1331        /// Raw destination address (e.g. `0xabc...`).
1332        recipient_address: String,
1333        /// Amount routed to the provider, in the route's source-asset units
1334        /// (Boltz invoice sats; Orchestra deposit sats/token). On the
1335        /// token-conversion path (both `FeesIncluded` and `FeesExcluded`)
1336        /// the dispatcher overrides this with the wallet-side token debit
1337        /// when the source token and destination asset form a USD-stable pair.
1338        amount_in: u128,
1339        /// `amount_in` expressed in the cross-chain (destination) asset's
1340        /// base units, via the same rate the SDK used at prepare time.
1341        asset_amount_in: u128,
1342        /// Estimated recipient amount in cross-chain asset base units.
1343        estimated_out: u128,
1344        /// Prepare-time total user-visible fee in cross-chain asset base units.
1345        /// Covers provider spread + bridge/gas + DEX slippage. On the
1346        /// token-conversion path it also rolls in the LN routing budget; on
1347        /// the direct path that budget lives separately in
1348        /// `source_transfer_fee_sats`.
1349        fee_amount: u128,
1350        /// Provider's own service fee/spread in its native denomination.
1351        service_fee_amount: u128,
1352        /// Asset which service fee is denominated in. Unset means BTC sats.
1353        service_fee_asset: Option<String>,
1354        /// Sats budget for moving the amount in from the wallet to the provider.
1355        source_transfer_fee_sats: u64,
1356        /// Fee mode the prepare ran under; the send stage matches.
1357        fee_mode: CrossChainFeeMode,
1358        /// ISO8601 timestamp after which the quote is no longer valid.
1359        expires_at: String,
1360        /// Provider-internal state, produced when preparing and consumed
1361        /// when sending.
1362        provider_context: CrossChainProviderContext,
1363    },
1364}
1365
1366#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1367#[derive(Debug, Clone, Serialize, Deserialize)]
1368pub struct SendOnchainFeeQuote {
1369    pub id: String,
1370    pub expires_at: u64,
1371    pub speed_fast: SendOnchainSpeedFeeQuote,
1372    pub speed_medium: SendOnchainSpeedFeeQuote,
1373    pub speed_slow: SendOnchainSpeedFeeQuote,
1374}
1375
1376#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1377#[derive(Debug, Clone, Serialize, Deserialize)]
1378pub struct SendOnchainSpeedFeeQuote {
1379    pub user_fee_sat: u64,
1380    pub l1_broadcast_fee_sat: u64,
1381}
1382
1383impl SendOnchainSpeedFeeQuote {
1384    pub fn total_fee_sat(&self) -> u64 {
1385        self.user_fee_sat.saturating_add(self.l1_broadcast_fee_sat)
1386    }
1387}
1388
1389#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1390pub struct ReceivePaymentRequest {
1391    pub payment_method: ReceivePaymentMethod,
1392}
1393
1394#[derive(Debug, Clone, Serialize)]
1395#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1396pub struct ReceivePaymentResponse {
1397    pub payment_request: String,
1398    /// Fee to pay to receive the payment
1399    /// Denominated in sats or token base units
1400    pub fee: u128,
1401}
1402
1403#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1404pub struct PrepareLnurlPayRequest {
1405    /// The amount to send. Denominated in satoshis, or in token base units
1406    /// when `token_identifier` is set.
1407    pub amount: u128,
1408    pub pay_request: LnurlPayRequestDetails,
1409    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1410    pub comment: Option<String>,
1411    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1412    pub validate_success_action_url: Option<bool>,
1413    /// The token identifier when sending a token amount with conversion.
1414    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1415    pub token_identifier: Option<String>,
1416    /// If provided, the payment will include a token conversion step before sending the payment
1417    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1418    pub conversion_options: Option<ConversionOptions>,
1419    /// How fees are handled. See [`FeePolicy`]. Defaults to `FeesExcluded`.
1420    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1421    pub fee_policy: Option<FeePolicy>,
1422}
1423
1424#[derive(Debug, Clone)]
1425#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1426pub struct PrepareLnurlPayResponse {
1427    /// The amount for the payment, always denominated in sats, even when a
1428    /// `token_identifier` and conversion are present.
1429    /// When a conversion is present, the token input amount is available in
1430    /// `conversion_estimate.amount_in`.
1431    pub amount_sats: u64,
1432    pub comment: Option<String>,
1433    pub pay_request: LnurlPayRequestDetails,
1434    /// The fee in satoshis. For `FeesIncluded` operations, this represents the total fee
1435    /// (including potential overpayment).
1436    pub fee_sats: u64,
1437    pub invoice_details: Bolt11InvoiceDetails,
1438    pub success_action: Option<SuccessAction>,
1439    /// When set, the payment will include a token conversion step before sending the payment
1440    pub conversion_estimate: Option<ConversionEstimate>,
1441    /// The fee policy actually applied. May differ from the request — e.g.,
1442    /// LNURL sends with `token_identifier` set + conversion are always
1443    /// `FeesIncluded` (explicit `FeesExcluded` is rejected).
1444    pub fee_policy: FeePolicy,
1445}
1446
1447#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1448pub struct LnurlPayRequest {
1449    pub prepare_response: PrepareLnurlPayResponse,
1450    /// If set, providing the same idempotency key for multiple requests will ensure that only one
1451    /// payment is made. If an idempotency key is re-used, the same payment will be returned.
1452    /// The idempotency key must be a valid UUID.
1453    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1454    pub idempotency_key: Option<String>,
1455}
1456
1457#[derive(Debug, Serialize)]
1458#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1459pub struct LnurlPayResponse {
1460    pub payment: Payment,
1461    pub success_action: Option<SuccessActionProcessed>,
1462}
1463
1464#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1465pub struct BuildUnsignedLnurlPayPackageRequest {
1466    pub prepare_response: PrepareLnurlPayResponse,
1467}
1468
1469#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1470pub struct PublishSignedLnurlPayPackageRequest {
1471    pub signed_package: SignedTransferPackage,
1472}
1473
1474#[allow(clippy::large_enum_variant)]
1475#[derive(Debug)]
1476#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1477pub enum PublishSignedLnurlPayResponse {
1478    SwapCompleted,
1479    PaymentSent { response: LnurlPayResponse },
1480}
1481
1482#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1483pub struct LnurlWithdrawRequest {
1484    /// The amount to withdraw in satoshis
1485    /// Must be within the min and max withdrawable limits
1486    pub amount_sats: u64,
1487    pub withdraw_request: LnurlWithdrawRequestDetails,
1488    /// If set, the function will return the payment if it is still pending after this
1489    /// number of seconds. If unset, the function will return immediately after
1490    /// initiating the LNURL withdraw.
1491    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1492    pub completion_timeout_secs: Option<u32>,
1493}
1494
1495#[derive(Debug, Serialize)]
1496#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1497pub struct LnurlWithdrawResponse {
1498    /// The Lightning invoice generated for the LNURL withdraw
1499    pub payment_request: String,
1500    pub payment: Option<Payment>,
1501}
1502
1503/// Represents the payment LNURL info
1504#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
1505#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1506pub struct LnurlPayInfo {
1507    pub ln_address: Option<String>,
1508    pub comment: Option<String>,
1509    pub domain: Option<String>,
1510    pub metadata: Option<String>,
1511    pub processed_success_action: Option<SuccessActionProcessed>,
1512    pub raw_success_action: Option<SuccessAction>,
1513}
1514
1515/// Represents the withdraw LNURL info
1516#[derive(Clone, Debug, Deserialize, Serialize)]
1517#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1518pub struct LnurlWithdrawInfo {
1519    pub withdraw_url: String,
1520}
1521
1522impl LnurlPayInfo {
1523    pub fn extract_description(&self) -> Option<String> {
1524        let Some(metadata) = &self.metadata else {
1525            return None;
1526        };
1527
1528        let Ok(metadata) = serde_json::from_str::<Vec<Vec<Value>>>(metadata) else {
1529            return None;
1530        };
1531
1532        for arr in metadata {
1533            if arr.len() != 2 {
1534                continue;
1535            }
1536            if let (Some(key), Some(value)) = (arr[0].as_str(), arr[1].as_str())
1537                && key == "text/plain"
1538            {
1539                return Some(value.to_string());
1540            }
1541        }
1542
1543        None
1544    }
1545}
1546
1547/// Specifies how fees are handled in a payment.
1548///
1549/// "Fees" are the wallet's sender-paid fees (Lightning routing, on-chain,
1550/// Spark transfer). They do not include provider spreads or destination-chain
1551/// costs on cross-chain routes; those are reported separately via
1552/// `estimated_out` on the prepare response and are not deterministic.
1553/// `FeePolicy` only controls the wallet's spend accounting.
1554#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1555#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1556pub enum FeePolicy {
1557    /// Fees are added on top of `amount`. Wallet's total spend is
1558    /// `amount + fees`. For direct sat sends, the recipient receives exactly
1559    /// `amount`. Default.
1560    #[default]
1561    FeesExcluded,
1562    /// Fees are deducted from `amount`. Wallet's total spend is `amount`.
1563    /// Use this to drain a balance — pass `amount = balance` and the wallet
1564    /// spends exactly that.
1565    FeesIncluded,
1566}
1567
1568#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1569#[derive(Debug, Clone, Serialize, Deserialize)]
1570pub enum OnchainConfirmationSpeed {
1571    Fast,
1572    Medium,
1573    Slow,
1574}
1575
1576/// The payment destination. Either a raw string (bolt11, spark address, BIP-21,
1577/// cross-chain URI, etc.) that is parsed internally, or a structured
1578/// cross-chain destination with explicit chain + asset selection.
1579#[derive(Debug, Clone, Serialize)]
1580#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1581pub enum PaymentRequest {
1582    /// Unparsed user input string (bolt11, spark address, BIP-21, cross-chain URI, etc.)
1583    Input { input: String },
1584    /// Cross-chain send with a selected route from `get_cross_chain_routes()`.
1585    /// Amount comes from `PrepareSendPaymentRequest.amount`, not here.
1586    CrossChain {
1587        address: String,
1588        route: CrossChainRoutePair,
1589        /// Maximum slippage tolerance in basis points (1/100 of a percent)
1590        /// for the cross-chain quote. Must be in `10..=500`. Falls back to
1591        /// [`Config::default_slippage_bps`] when `None`, which itself
1592        /// defaults to 100 bps (1%) when unset.
1593        max_slippage_bps: Option<u32>,
1594        /// Target-overpay pad in basis points applied on `FeesExcluded`
1595        /// conversion sends. Inflates the destination target before quoting
1596        /// so the recipient lands at or above the user's requested amount
1597        /// despite provider slippage. Must be in `0..=500`. Falls back to
1598        /// [`CrossChainConfig::default_target_overpay_bps`] when `None`,
1599        /// which itself defaults to 15 bps.
1600        target_overpay_bps: Option<u32>,
1601    },
1602}
1603
1604#[allow(clippy::large_enum_variant)]
1605#[derive(Debug, Clone, Serialize, Deserialize)]
1606#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1607pub enum UnsignedTransferPackage {
1608    Swap {
1609        prepare_transfer: crate::signer::ExternalPrepareTransferRequest,
1610        target_amounts: Vec<u64>,
1611        amount_sat: u64,
1612        fee_sat: u64,
1613    },
1614    Transfer {
1615        prepare_transfer: crate::signer::ExternalPrepareTransferRequest,
1616        amount_sat: u64,
1617        fee_sat: u64,
1618        target: TransferTarget,
1619    },
1620    Token {
1621        prepare_token_transaction: crate::signer::ExternalPrepareTokenTransactionRequest,
1622        token_context: Vec<u8>,
1623        token_identifier: String,
1624        amount: u128,
1625        fee: u128,
1626        /// When set, this package re-shapes the wallet's token outputs instead of
1627        /// sending a payment. Publishing it returns `SwapCompleted`: rebuild the
1628        /// original send from the same prepare response and submit again.
1629        is_swap: bool,
1630    },
1631}
1632
1633#[derive(Debug, Clone, Serialize, Deserialize)]
1634#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1635pub enum TransferTarget {
1636    Spark {
1637        address: String,
1638        spark_invoice: Option<String>,
1639    },
1640    Lightning {
1641        bolt11: String,
1642        lnurl_pay: Option<LnurlPayContext>,
1643        fee_policy: FeePolicy,
1644        completion_timeout_secs: Option<u32>,
1645    },
1646    CoopExit {
1647        address: String,
1648        fee_quote: SendOnchainFeeQuote,
1649        confirmation_speed: OnchainConfirmationSpeed,
1650    },
1651}
1652
1653#[derive(Debug, Clone, Serialize, Deserialize)]
1654#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1655pub struct LnurlPayContext {
1656    pub pay_request: LnurlPayRequestDetails,
1657    pub comment: Option<String>,
1658    pub success_action: Option<SuccessAction>,
1659}
1660
1661#[derive(Debug, Clone, Serialize, Deserialize)]
1662#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1663pub struct SignedTransferPackage {
1664    pub unsigned: UnsignedTransferPackage,
1665    pub signature: TransferSignature,
1666}
1667
1668#[derive(Debug, Clone, Serialize, Deserialize)]
1669#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1670pub enum TransferSignature {
1671    Transfer {
1672        signed: crate::signer::ExternalPreparedTransfer,
1673    },
1674    Token {
1675        signed: crate::signer::ExternalPreparedTokenTransaction,
1676    },
1677}
1678
1679#[derive(Debug, Clone)]
1680#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1681pub enum BuildTransferPackageOptions {
1682    BitcoinAddress {
1683        confirmation_speed: OnchainConfirmationSpeed,
1684    },
1685    Bolt11Invoice {
1686        prefer_spark: bool,
1687
1688        /// If set, publishing the package waits up to this many seconds for the
1689        /// payment to complete before returning it while still pending. If unset,
1690        /// publishing returns immediately after initiating the payment.
1691        completion_timeout_secs: Option<u32>,
1692    },
1693}
1694
1695#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1696pub struct BuildUnsignedTransferPackageRequest {
1697    pub prepare_response: PrepareSendPaymentResponse,
1698    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1699    pub options: Option<BuildTransferPackageOptions>,
1700}
1701
1702#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1703pub struct PrepareSendPaymentRequest {
1704    pub payment_request: PaymentRequest,
1705    /// The amount to send.
1706    /// Optional for payment requests with embedded amounts (e.g., Spark/Bolt11 invoices with amounts).
1707    /// Required for Spark addresses, Bitcoin addresses, and amountless invoices.
1708    /// Denominated in satoshis for Bitcoin payments, or token base units for token payments.
1709    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1710    pub amount: Option<u128>,
1711    /// Optional token identifier for token payments.
1712    /// Absence indicates that the payment is a Bitcoin payment.
1713    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1714    pub token_identifier: Option<String>,
1715    /// If provided, the payment will include a conversion step before sending the payment
1716    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1717    pub conversion_options: Option<ConversionOptions>,
1718    /// How fees are handled. See [`FeePolicy`]. Defaults to `FeesExcluded`.
1719    ///
1720    /// Ignored on cross-chain AMM-conversion sends (whether the conversion was
1721    /// explicitly requested or auto-injected by stable balance) — fees come
1722    /// out of the converted sats. Bolt11 and Bitcoin AMM-conversion sends
1723    /// still respect this field by sizing the conversion to cover fees. The
1724    /// prepare response's `fee_policy` reflects what was actually applied.
1725    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1726    pub fee_policy: Option<FeePolicy>,
1727}
1728
1729#[derive(Debug, Clone, Serialize)]
1730#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1731pub struct PrepareSendPaymentResponse {
1732    pub payment_method: SendPaymentMethod,
1733    /// The amount to be sent, denominated in satoshis for Bitcoin payments
1734    /// (including token-to-Bitcoin conversions), or token base units for token payments.
1735    /// When a conversion is present, the input amount is in
1736    /// `conversion_estimate.amount_in`.
1737    pub amount: u128,
1738    /// Optional token identifier for token payments.
1739    /// Absence indicates that the payment is a Bitcoin payment.
1740    pub token_identifier: Option<String>,
1741    /// When set, the payment will include a conversion step before sending the payment
1742    pub conversion_estimate: Option<ConversionEstimate>,
1743    /// The fee policy actually applied. May differ from the request — e.g.,
1744    /// cross-chain AMM-conversion sends are always `FeesIncluded`.
1745    pub fee_policy: FeePolicy,
1746}
1747
1748#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1749pub enum SendPaymentOptions {
1750    BitcoinAddress {
1751        /// Confirmation speed for the on-chain transaction.
1752        confirmation_speed: OnchainConfirmationSpeed,
1753    },
1754    Bolt11Invoice {
1755        prefer_spark: bool,
1756
1757        /// If set, the function will return the payment if it is still pending after this
1758        /// number of seconds. If unset, the function will return immediately after initiating the payment.
1759        completion_timeout_secs: Option<u32>,
1760    },
1761    SparkAddress {
1762        /// Can only be provided for Bitcoin payments. If set, a Spark HTLC transfer will be created.
1763        /// The receiver will need to provide the preimage to claim it.
1764        htlc_options: Option<SparkHtlcOptions>,
1765    },
1766}
1767
1768#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1769pub struct SparkHtlcOptions {
1770    /// The payment hash of the HTLC. The receiver will need to provide the associated preimage to claim it.
1771    pub payment_hash: String,
1772    /// The duration of the HTLC in seconds.
1773    /// After this time, the HTLC will be returned.
1774    pub expiry_duration_secs: u64,
1775}
1776
1777#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1778pub struct SendPaymentRequest {
1779    pub prepare_response: PrepareSendPaymentResponse,
1780    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1781    pub options: Option<SendPaymentOptions>,
1782    /// The optional idempotency key for all Spark based transfers (excludes token payments
1783    /// and cross-chain sends).
1784    /// If set, providing the same idempotency key for multiple requests will ensure that only one
1785    /// payment is made. If an idempotency key is re-used, the same payment will be returned.
1786    /// The idempotency key must be a valid UUID.
1787    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1788    pub idempotency_key: Option<String>,
1789}
1790
1791#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1792pub struct PublishSignedTransferPackageRequest {
1793    pub signed_package: SignedTransferPackage,
1794}
1795
1796#[allow(clippy::large_enum_variant)]
1797#[derive(Debug, Clone)]
1798#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1799pub enum PublishSignedTransferPackageResponse {
1800    SwapCompleted,
1801    PaymentSent { payment: Payment },
1802}
1803
1804#[derive(Debug, Clone, Serialize)]
1805#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1806pub struct SendPaymentResponse {
1807    pub payment: Payment,
1808}
1809
1810#[derive(Debug, Clone)]
1811#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1812pub enum PaymentDetailsFilter {
1813    Spark {
1814        /// Filter specific Spark HTLC statuses
1815        htlc_status: Option<Vec<SparkHtlcStatus>>,
1816        /// Filter conversion payments with refund information
1817        conversion_refund_needed: Option<bool>,
1818    },
1819    Token {
1820        /// Filter conversion payments with refund information
1821        conversion_refund_needed: Option<bool>,
1822        /// Filter by transaction hash
1823        tx_hash: Option<String>,
1824        /// Filter by transaction type
1825        tx_type: Option<TokenTransactionType>,
1826    },
1827    Lightning {
1828        /// Filter specific Spark HTLC statuses
1829        htlc_status: Option<Vec<SparkHtlcStatus>>,
1830    },
1831}
1832
1833/// Request to list payments with optional filters and pagination
1834#[derive(Debug, Clone, Default)]
1835#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1836pub struct ListPaymentsRequest {
1837    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1838    pub type_filter: Option<Vec<PaymentType>>,
1839    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1840    pub status_filter: Option<Vec<PaymentStatus>>,
1841    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1842    pub asset_filter: Option<AssetFilter>,
1843    /// Only include payments matching at least one of these payment details filters
1844    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1845    pub payment_details_filter: Option<Vec<PaymentDetailsFilter>>,
1846    /// Only include payments created after this timestamp (inclusive)
1847    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1848    pub from_timestamp: Option<u64>,
1849    /// Only include payments created before this timestamp (exclusive)
1850    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1851    pub to_timestamp: Option<u64>,
1852    /// Number of records to skip
1853    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1854    pub offset: Option<u32>,
1855    /// Maximum number of records to return
1856    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1857    pub limit: Option<u32>,
1858    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1859    pub sort_ascending: Option<bool>,
1860}
1861
1862/// A field of [`ListPaymentsRequest`] when listing payments filtered by asset
1863#[derive(Debug, Clone)]
1864#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1865pub enum AssetFilter {
1866    Bitcoin,
1867    Token {
1868        /// Optional token identifier to filter by
1869        token_identifier: Option<String>,
1870    },
1871}
1872
1873impl FromStr for AssetFilter {
1874    type Err = String;
1875
1876    fn from_str(s: &str) -> Result<Self, Self::Err> {
1877        Ok(match s.to_lowercase().as_str() {
1878            "bitcoin" => AssetFilter::Bitcoin,
1879            "token" => AssetFilter::Token {
1880                token_identifier: None,
1881            },
1882            str if str.starts_with("token:") => AssetFilter::Token {
1883                token_identifier: Some(
1884                    str.split_once(':')
1885                        .ok_or(format!("Invalid asset filter '{s}'"))?
1886                        .1
1887                        .to_string(),
1888                ),
1889            },
1890            _ => return Err(format!("Invalid asset filter '{s}'")),
1891        })
1892    }
1893}
1894
1895/// Response from listing payments
1896#[derive(Debug, Clone, Serialize)]
1897#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1898pub struct ListPaymentsResponse {
1899    /// The list of payments
1900    pub payments: Vec<Payment>,
1901}
1902
1903#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1904pub struct GetPaymentRequest {
1905    pub payment_id: String,
1906}
1907
1908#[derive(Debug, Clone, Serialize)]
1909#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1910pub struct GetPaymentResponse {
1911    pub payment: Payment,
1912}
1913
1914#[cfg_attr(feature = "uniffi", uniffi::export(callback_interface))]
1915pub trait Logger: Send + Sync {
1916    fn log(&self, l: LogEntry);
1917}
1918
1919#[derive(Debug, Clone, Serialize)]
1920#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1921pub struct LogEntry {
1922    pub line: String,
1923    pub level: String,
1924}
1925
1926#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1927#[derive(Debug, Clone, Serialize, Deserialize)]
1928pub struct CheckLightningAddressRequest {
1929    pub username: String,
1930}
1931
1932#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1933#[derive(Debug, Clone, Serialize, Deserialize)]
1934pub struct RegisterLightningAddressRequest {
1935    pub username: String,
1936    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1937    pub description: Option<String>,
1938}
1939
1940/// Authorization from the current owner granting a specific new owner the
1941/// right to take over a username. Produced by
1942/// [`BreezSdk::authorize_lightning_address_transfer`] and handed to the new
1943/// owner, who passes it to [`BreezSdk::claim_lightning_address_transfer`]. It
1944/// fully describes the transfer, so the new owner needs nothing else to claim.
1945#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1946#[derive(Debug, Clone, Serialize, Deserialize)]
1947pub struct TransferAuthorization {
1948    /// The username being handed over.
1949    pub username: String,
1950    /// The current owner's public key.
1951    pub pubkey: String,
1952    /// The current owner's signature authorizing the transfer.
1953    pub signature: String,
1954}
1955
1956/// Request for [`BreezSdk::authorize_lightning_address_transfer`]. Called by
1957/// the *current owner* to authorize handing their registered username over to
1958/// `transferee_pubkey`.
1959#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1960#[derive(Debug, Clone, Serialize, Deserialize)]
1961pub struct AuthorizeTransferRequest {
1962    /// The new owner's identity public key.
1963    pub transferee_pubkey: String,
1964}
1965
1966/// Request for [`BreezSdk::claim_lightning_address_transfer`]. Called by the
1967/// *new owner* to complete the takeover using the authorization produced by
1968/// the current owner.
1969#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1970#[derive(Debug, Clone, Serialize, Deserialize)]
1971pub struct ClaimTransferRequest {
1972    /// Authorization produced by the current owner via
1973    /// [`BreezSdk::authorize_lightning_address_transfer`].
1974    pub authorization: TransferAuthorization,
1975    /// Description for the address. Defaults to `"Pay to {username}@{domain}"`.
1976    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1977    pub description: Option<String>,
1978}
1979
1980#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1981#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
1982pub struct LnurlInfo {
1983    pub url: String,
1984    pub bech32: String,
1985}
1986
1987impl LnurlInfo {
1988    pub fn new(url: String) -> Self {
1989        let bech32 =
1990            breez_sdk_common::lnurl::encode_lnurl_to_bech32(&url).unwrap_or_else(|_| url.clone());
1991        Self { url, bech32 }
1992    }
1993}
1994
1995#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1996#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
1997pub struct LightningAddressInfo {
1998    pub description: String,
1999    pub lightning_address: String,
2000    pub lnurl: LnurlInfo,
2001    pub username: String,
2002}
2003
2004impl From<RecoverLnurlPayResponse> for LightningAddressInfo {
2005    fn from(resp: RecoverLnurlPayResponse) -> Self {
2006        Self {
2007            description: resp.description,
2008            lightning_address: resp.lightning_address,
2009            lnurl: LnurlInfo::new(resp.lnurl),
2010            username: resp.username,
2011        }
2012    }
2013}
2014
2015/// Response from listing fiat currencies
2016#[derive(Debug, Clone, Serialize)]
2017#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2018pub struct ListFiatCurrenciesResponse {
2019    /// The list of fiat currencies
2020    pub currencies: Vec<FiatCurrency>,
2021}
2022
2023/// Response from listing fiat rates
2024#[derive(Debug, Clone, Serialize)]
2025#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2026pub struct ListFiatRatesResponse {
2027    /// The list of fiat rates
2028    pub rates: Vec<Rate>,
2029}
2030
2031/// The operational status of a Spark service.
2032#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2033#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2034pub enum ServiceStatus {
2035    /// Service is fully operational.
2036    Operational,
2037    /// Service is experiencing degraded performance.
2038    Degraded,
2039    /// Service is partially unavailable.
2040    Partial,
2041    /// Service status is unknown.
2042    Unknown,
2043    /// Service is experiencing a major outage.
2044    Major,
2045}
2046
2047/// The status of the Spark network services relevant to the SDK.
2048#[derive(Debug, Clone, Serialize)]
2049#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2050pub struct SparkStatus {
2051    /// The worst status across all relevant services.
2052    pub status: ServiceStatus,
2053    /// The last time the status was updated, as a unix timestamp in seconds.
2054    pub last_updated: u64,
2055}
2056
2057pub(crate) enum WaitForPaymentIdentifier {
2058    PaymentId(String),
2059    LightningReceive { invoice: String, ssp_id: String },
2060}
2061
2062#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2063pub struct GetTokensMetadataRequest {
2064    pub token_identifiers: Vec<String>,
2065}
2066
2067#[derive(Debug, Clone, Serialize)]
2068#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2069pub struct GetTokensMetadataResponse {
2070    pub tokens_metadata: Vec<TokenMetadata>,
2071}
2072
2073#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2074pub struct SignMessageRequest {
2075    pub message: String,
2076    /// If true, the signature will be encoded in compact format instead of DER format
2077    pub compact: bool,
2078}
2079
2080#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2081pub struct SignMessageResponse {
2082    pub pubkey: String,
2083    /// The DER or compact hex encoded signature
2084    pub signature: String,
2085}
2086
2087#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2088pub struct CheckMessageRequest {
2089    /// The message that was signed
2090    pub message: String,
2091    /// The public key that signed the message
2092    pub pubkey: String,
2093    /// The DER or compact hex encoded signature
2094    pub signature: String,
2095}
2096
2097#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2098pub struct CheckMessageResponse {
2099    pub is_valid: bool,
2100}
2101
2102#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2103#[derive(Debug, Clone, Serialize)]
2104pub struct UserSettings {
2105    pub spark_private_mode_enabled: bool,
2106
2107    /// The label of the currently active stable balance token, or `None` if deactivated.
2108    pub stable_balance_active_label: Option<String>,
2109}
2110
2111#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2112pub struct UpdateUserSettingsRequest {
2113    pub spark_private_mode_enabled: Option<bool>,
2114
2115    /// Update the active stable balance token. `None` means no change.
2116    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
2117    pub stable_balance_active_label: Option<StableBalanceActiveLabel>,
2118}
2119
2120#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2121pub struct ClaimHtlcPaymentRequest {
2122    pub preimage: String,
2123}
2124
2125#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2126pub struct ClaimHtlcPaymentResponse {
2127    pub payment: Payment,
2128}
2129
2130#[derive(Debug, Clone, Deserialize, Serialize)]
2131#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2132pub struct LnurlReceiveMetadata {
2133    pub nostr_zap_request: Option<String>,
2134    pub nostr_zap_receipt: Option<String>,
2135    pub sender_comment: Option<String>,
2136}
2137
2138/// Mode of a manually-triggered optimization run.
2139#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
2140#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2141pub enum OptimizationMode {
2142    /// Run until no further optimization is productive.
2143    #[default]
2144    Full,
2145    /// Execute a single round and return so the caller can drive progress.
2146    SingleRound,
2147}
2148
2149/// Request for [`BreezSdk::optimize_leaves`]. Defaults to
2150/// [`OptimizationMode::Full`].
2151#[derive(Debug, Clone, Default, Deserialize, Serialize)]
2152#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2153pub struct OptimizeLeavesRequest {
2154    /// Controls how much work the call performs before returning.
2155    pub mode: OptimizationMode,
2156}
2157
2158/// Response from a [`BreezSdk::optimize_leaves`] call.
2159#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
2160#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2161pub struct OptimizeLeavesResponse {
2162    /// The outcome of the optimization run.
2163    pub outcome: OptimizationOutcome,
2164}
2165
2166/// Outcome of a [`BreezSdk::optimize_leaves`] call.
2167///
2168/// `rounds_executed` on `Completed` refers to rounds run by *this call*.
2169/// The SDK holds no cross-call state — callers driving a `SingleRound`
2170/// loop maintain their own cumulative counter if they need one.
2171///
2172/// A `Completed { rounds_executed: 0 }` outcome means the wallet was
2173/// already optimal at call time (no swap was needed).
2174///
2175/// **`SingleRound` loop pattern**: terminate on anything that isn't
2176/// `InProgress`. `Completed` covers both the final swap of a productive
2177/// run and the "already optimal" no-op case (the latter as
2178/// `rounds_executed: 0`).
2179///
2180/// ```ignore
2181/// loop {
2182///     let request = OptimizeLeavesRequest { mode: OptimizationMode::SingleRound };
2183///     match sdk.optimize_leaves(request).await?.outcome {
2184///         OptimizationOutcome::InProgress => continue,
2185///         OptimizationOutcome::Completed { .. } => break,
2186///     }
2187/// }
2188/// ```
2189#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
2190#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2191pub enum OptimizationOutcome {
2192    /// All planned optimization work was executed in this call.
2193    /// Returned by `Full` runs on success, and by `SingleRound` runs
2194    /// whose swap was the final one needed (the planner produced a
2195    /// single-swap plan with a convergence guarantee).
2196    /// `rounds_executed == 0` means the wallet was already optimal —
2197    /// no work was performed.
2198    Completed { rounds_executed: u32 },
2199    /// `SingleRound` only: a round ran but the planner could not
2200    /// guarantee it was the last. The caller should invoke
2201    /// `optimize_leaves` again.
2202    InProgress,
2203}
2204
2205/// A contact entry containing a name and payment identifier.
2206#[derive(Debug, Clone, Serialize, Deserialize)]
2207#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2208pub struct Contact {
2209    pub id: String,
2210    pub name: String,
2211    /// A Lightning address (user@domain).
2212    pub payment_identifier: String,
2213    pub created_at: u64,
2214    pub updated_at: u64,
2215}
2216
2217/// Request to add a new contact.
2218#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2219pub struct AddContactRequest {
2220    pub name: String,
2221    /// A Lightning address (user@domain).
2222    pub payment_identifier: String,
2223}
2224
2225/// Request to update an existing contact.
2226#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2227pub struct UpdateContactRequest {
2228    pub id: String,
2229    pub name: String,
2230    /// A Lightning address (user@domain).
2231    pub payment_identifier: String,
2232}
2233
2234/// Request to list contacts with optional pagination.
2235#[derive(Debug, Clone, Default)]
2236#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2237pub struct ListContactsRequest {
2238    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2239    pub offset: Option<u32>,
2240    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2241    pub limit: Option<u32>,
2242}
2243
2244/// The type of event that triggers a webhook notification.
2245#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2246#[allow(clippy::enum_variant_names)]
2247#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2248pub enum WebhookEventType {
2249    /// Triggered when a Lightning receive operation completes.
2250    LightningReceiveFinished,
2251    /// Triggered when a Lightning send operation completes.
2252    LightningSendFinished,
2253    /// Triggered when a cooperative exit completes.
2254    CoopExitFinished,
2255    /// Triggered when a static deposit completes.
2256    StaticDepositFinished,
2257    /// An event type not yet recognized by this version of the SDK.
2258    Unknown(String),
2259}
2260
2261/// A registered webhook entry.
2262#[derive(Debug, Clone, Serialize)]
2263#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2264pub struct Webhook {
2265    /// Unique identifier for this webhook.
2266    pub id: String,
2267    /// The URL that receives webhook notifications.
2268    pub url: String,
2269    /// The event types this webhook is subscribed to.
2270    pub event_types: Vec<WebhookEventType>,
2271}
2272
2273/// Request to register a new webhook.
2274#[derive(Debug, Clone)]
2275#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2276pub struct RegisterWebhookRequest {
2277    /// The URL that will receive webhook notifications.
2278    pub url: String,
2279    /// A secret used for HMAC-SHA256 signature verification of webhook payloads.
2280    pub secret: String,
2281    /// The event types to subscribe to.
2282    pub event_types: Vec<WebhookEventType>,
2283}
2284
2285/// Response from registering a webhook.
2286#[derive(Debug, Clone, Serialize)]
2287#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2288pub struct RegisterWebhookResponse {
2289    /// The unique identifier of the newly registered webhook.
2290    pub webhook_id: String,
2291}
2292
2293/// Request to unregister an existing webhook.
2294#[derive(Debug, Clone)]
2295#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2296pub struct UnregisterWebhookRequest {
2297    /// The unique identifier of the webhook to unregister.
2298    pub webhook_id: String,
2299}