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, error::DepositClaimError,
26};
27
28/// A list of external input parsers that are used by default.
29/// To opt-out, set `use_default_external_input_parsers` in [Config] to false.
30pub const DEFAULT_EXTERNAL_INPUT_PARSERS: &[(&str, &str, &str)] = &[
31    (
32        "picknpay",
33        "(.*)(za.co.electrum.picknpay)(.*)",
34        "https://cryptoqr.net/.well-known/lnurlp/<input>",
35    ),
36    (
37        "bootleggers",
38        r"(.*)(wigroup\.co|yoyogroup\.co)(.*)",
39        "https://cryptoqr.net/.well-known/lnurlw/<input>",
40    ),
41];
42
43/// Represents the seed for wallet generation, either as a mnemonic phrase with an optional
44/// passphrase or as raw entropy bytes.
45#[derive(Debug, Clone)]
46#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
47pub enum Seed {
48    /// A BIP-39 mnemonic phrase with an optional passphrase.
49    Mnemonic {
50        /// The mnemonic phrase. 12 or 24 words.
51        mnemonic: String,
52        /// An optional passphrase for the mnemonic.
53        passphrase: Option<String>,
54    },
55    /// Raw entropy bytes.
56    Entropy(Vec<u8>),
57}
58
59impl Seed {
60    pub fn to_bytes(&self) -> Result<Vec<u8>, SdkError> {
61        match self {
62            Seed::Mnemonic {
63                mnemonic,
64                passphrase,
65            } => {
66                let mnemonic = bip39::Mnemonic::parse(mnemonic)
67                    .map_err(|e| SdkError::Generic(e.to_string()))?;
68
69                Ok(mnemonic
70                    .to_seed(passphrase.as_deref().unwrap_or(""))
71                    .to_vec())
72            }
73            Seed::Entropy(entropy) => Ok(entropy.clone()),
74        }
75    }
76}
77
78#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
79pub struct ConnectRequest {
80    pub config: Config,
81    pub seed: Seed,
82    pub storage_dir: String,
83}
84
85/// Request object for connecting to the Spark network using an external signer.
86///
87/// This allows using a custom signer implementation instead of providing a seed directly.
88#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
89#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
90pub struct ConnectWithSignerRequest {
91    pub config: Config,
92    pub signer: std::sync::Arc<dyn crate::signer::ExternalSigner>,
93    pub storage_dir: String,
94}
95
96/// The type of payment
97#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
98#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
99pub enum PaymentType {
100    /// Payment sent from this wallet
101    Send,
102    /// Payment received to this wallet
103    Receive,
104}
105
106impl fmt::Display for PaymentType {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        match self {
109            PaymentType::Send => write!(f, "send"),
110            PaymentType::Receive => write!(f, "receive"),
111        }
112    }
113}
114
115impl FromStr for PaymentType {
116    type Err = String;
117
118    fn from_str(s: &str) -> Result<Self, Self::Err> {
119        Ok(match s.to_lowercase().as_str() {
120            "receive" => PaymentType::Receive,
121            "send" => PaymentType::Send,
122            _ => return Err(format!("invalid payment type '{s}'")),
123        })
124    }
125}
126
127/// The status of a payment
128#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
129#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
130pub enum PaymentStatus {
131    /// Payment is completed successfully
132    Completed,
133    /// Payment is in progress
134    Pending,
135    /// Payment has failed
136    Failed,
137}
138
139impl PaymentStatus {
140    /// Returns true if the payment status is final (either Completed or Failed)
141    pub fn is_final(&self) -> bool {
142        matches!(self, PaymentStatus::Completed | PaymentStatus::Failed)
143    }
144}
145
146impl fmt::Display for PaymentStatus {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        match self {
149            PaymentStatus::Completed => write!(f, "completed"),
150            PaymentStatus::Pending => write!(f, "pending"),
151            PaymentStatus::Failed => write!(f, "failed"),
152        }
153    }
154}
155
156impl FromStr for PaymentStatus {
157    type Err = String;
158
159    fn from_str(s: &str) -> Result<Self, Self::Err> {
160        Ok(match s.to_lowercase().as_str() {
161            "completed" => PaymentStatus::Completed,
162            "pending" => PaymentStatus::Pending,
163            "failed" => PaymentStatus::Failed,
164            _ => return Err(format!("Invalid payment status '{s}'")),
165        })
166    }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
170#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
171pub enum PaymentMethod {
172    Lightning,
173    Spark,
174    Token,
175    Deposit,
176    Withdraw,
177    Unknown,
178}
179
180impl Display for PaymentMethod {
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        match self {
183            PaymentMethod::Lightning => write!(f, "lightning"),
184            PaymentMethod::Spark => write!(f, "spark"),
185            PaymentMethod::Token => write!(f, "token"),
186            PaymentMethod::Deposit => write!(f, "deposit"),
187            PaymentMethod::Withdraw => write!(f, "withdraw"),
188            PaymentMethod::Unknown => write!(f, "unknown"),
189        }
190    }
191}
192
193impl FromStr for PaymentMethod {
194    type Err = ();
195
196    fn from_str(s: &str) -> Result<Self, Self::Err> {
197        match s {
198            "lightning" => Ok(PaymentMethod::Lightning),
199            "spark" => Ok(PaymentMethod::Spark),
200            "token" => Ok(PaymentMethod::Token),
201            "deposit" => Ok(PaymentMethod::Deposit),
202            "withdraw" => Ok(PaymentMethod::Withdraw),
203            "unknown" => Ok(PaymentMethod::Unknown),
204            _ => Err(()),
205        }
206    }
207}
208
209/// Represents a payment (sent or received)
210#[derive(Debug, Clone, Serialize, Deserialize)]
211#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
212pub struct Payment {
213    /// Unique identifier for the payment
214    pub id: String,
215    /// Type of payment (send or receive)
216    pub payment_type: PaymentType,
217    /// Status of the payment
218    pub status: PaymentStatus,
219    /// Amount in satoshis or token base units
220    pub amount: u128,
221    /// Fee paid in satoshis or token base units
222    pub fees: u128,
223    /// Timestamp of when the payment was created
224    pub timestamp: u64,
225    /// Method of payment. Sometimes the payment details is empty so this field
226    /// is used to determine the payment method.
227    pub method: PaymentMethod,
228    /// Details of the payment
229    pub details: Option<PaymentDetails>,
230    /// If set, this payment involved a conversion before the payment
231    pub conversion_details: Option<ConversionDetails>,
232}
233
234impl Payment {
235    /// Returns `true` if this payment is a child of a conversion operation.
236    ///
237    /// Conversion operations (stable balance, ongoing sends) create internal child
238    /// payments (send sats→Flashnet, receive tokens). These are identified by having
239    /// `conversion_info` set in their payment details.
240    pub fn is_conversion_child(&self) -> bool {
241        matches!(
242            &self.details,
243            Some(
244                PaymentDetails::Spark {
245                    conversion_info: Some(_),
246                    ..
247                } | PaymentDetails::Token {
248                    conversion_info: Some(_),
249                    ..
250                }
251            )
252        )
253    }
254}
255
256/// Outlines the steps involved in a conversion.
257///
258/// Built progressively: `status` is available immediately from payment metadata,
259/// while `from`/`to` steps are enriched later from child payments.
260#[derive(Debug, Clone, Serialize, Deserialize)]
261#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
262pub struct ConversionDetails {
263    /// Current status of the conversion
264    pub status: ConversionStatus,
265    /// The send step of the conversion (e.g., sats sent to Flashnet)
266    pub from: Option<ConversionStep>,
267    /// The receive step of the conversion (e.g., tokens received from Flashnet)
268    pub to: Option<ConversionStep>,
269}
270
271/// Extracts conversion steps (from/to) from a conversion's child payments.
272///
273/// Each conversion produces a send payment (sats → Flashnet) and a receive payment
274/// (tokens ← Flashnet), linked to the parent payment via `parent_payment_id` in
275/// payment metadata. The SDK queries these children from storage and converts them
276/// into the `from` and `to` steps.
277///
278/// Returns `(None, None)` when no child payments exist yet (e.g. Pending or Failed).
279pub fn conversion_steps_from_payments(
280    payments: &[Payment],
281) -> Result<(Option<ConversionStep>, Option<ConversionStep>), SdkError> {
282    let from = payments
283        .iter()
284        .find(|p| p.payment_type == PaymentType::Send)
285        .map(TryInto::try_into)
286        .transpose()?;
287    let to = payments
288        .iter()
289        .find(|p| p.payment_type == PaymentType::Receive)
290        .map(TryInto::try_into)
291        .transpose()?;
292    Ok((from, to))
293}
294
295/// A single step in a conversion
296#[derive(Debug, Clone, Serialize, Deserialize)]
297#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
298pub struct ConversionStep {
299    /// The underlying payment id of the conversion step
300    pub payment_id: String,
301    /// Payment amount in satoshis or token base units
302    pub amount: u128,
303    /// Fee paid in satoshis or token base units
304    /// This represents the payment fee + the conversion fee
305    pub fee: u128,
306    /// Method of payment
307    pub method: PaymentMethod,
308    /// Token metadata if a token is used for payment
309    pub token_metadata: Option<TokenMetadata>,
310    /// The reason the conversion amount was adjusted, if applicable.
311    #[serde(default)]
312    pub amount_adjustment: Option<AmountAdjustmentReason>,
313}
314
315/// Converts a Spark or Token payment into a `ConversionStep`.
316/// Fees are a sum of the payment fee and the conversion fee, if applicable,
317/// from the payment details. Token metadata should only be set for a token payment.
318impl TryFrom<&Payment> for ConversionStep {
319    type Error = SdkError;
320    fn try_from(payment: &Payment) -> Result<Self, Self::Error> {
321        let (conversion_info, token_metadata) = match &payment.details {
322            Some(PaymentDetails::Spark {
323                conversion_info: Some(info),
324                ..
325            }) => (info, None),
326            Some(PaymentDetails::Token {
327                conversion_info: Some(info),
328                metadata,
329                ..
330            }) => (info, Some(metadata.clone())),
331            _ => {
332                return Err(SdkError::Generic(format!(
333                    "No conversion info available for payment {}",
334                    payment.id
335                )));
336            }
337        };
338        Ok(ConversionStep {
339            payment_id: payment.id.clone(),
340            amount: payment.amount,
341            fee: payment
342                .fees
343                .saturating_add(conversion_info.fee.unwrap_or(0)),
344            method: payment.method,
345            token_metadata,
346            amount_adjustment: conversion_info.amount_adjustment.clone(),
347        })
348    }
349}
350
351#[cfg(feature = "uniffi")]
352uniffi::custom_type!(u128, String, {
353    remote,
354    try_lift: |val| val.parse::<u128>().map_err(uniffi::deps::anyhow::Error::msg),
355    lower: |obj| obj.to_string(),
356});
357
358// TODO: fix large enum variant lint - may be done by boxing lnurl_pay_info but that requires
359//  some changes to the wasm bindgen macro
360#[allow(clippy::large_enum_variant)]
361#[derive(Debug, Clone, Serialize, Deserialize)]
362#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
363pub enum PaymentDetails {
364    Spark {
365        /// The invoice details if the payment fulfilled a spark invoice
366        invoice_details: Option<SparkInvoicePaymentDetails>,
367        /// The HTLC transfer details if the payment fulfilled an HTLC transfer
368        htlc_details: Option<SparkHtlcDetails>,
369        /// The information for a conversion
370        conversion_info: Option<ConversionInfo>,
371    },
372    Token {
373        metadata: TokenMetadata,
374        tx_hash: String,
375        tx_type: TokenTransactionType,
376        /// The invoice details if the payment fulfilled a spark invoice
377        invoice_details: Option<SparkInvoicePaymentDetails>,
378        /// The information for a conversion
379        conversion_info: Option<ConversionInfo>,
380    },
381    Lightning {
382        /// Represents the invoice description
383        description: Option<String>,
384        /// Represents the Bolt11/Bolt12 invoice associated with a payment
385        /// In the case of a Send payment, this is the invoice paid by the user
386        /// In the case of a Receive payment, this is the invoice paid to the user
387        invoice: String,
388
389        /// The invoice destination/payee pubkey
390        destination_pubkey: String,
391
392        /// The HTLC transfer details
393        htlc_details: SparkHtlcDetails,
394
395        /// Lnurl payment information if this was an lnurl payment.
396        lnurl_pay_info: Option<LnurlPayInfo>,
397
398        /// Lnurl withdrawal information if this was an lnurl payment.
399        lnurl_withdraw_info: Option<LnurlWithdrawInfo>,
400
401        /// Lnurl receive information if this was a received lnurl payment.
402        lnurl_receive_metadata: Option<LnurlReceiveMetadata>,
403    },
404    Withdraw {
405        tx_id: String,
406    },
407    Deposit {
408        tx_id: String,
409    },
410}
411
412#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
413#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
414pub enum TokenTransactionType {
415    Transfer,
416    Mint,
417    Burn,
418}
419
420impl fmt::Display for TokenTransactionType {
421    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
422        match self {
423            TokenTransactionType::Transfer => write!(f, "transfer"),
424            TokenTransactionType::Mint => write!(f, "mint"),
425            TokenTransactionType::Burn => write!(f, "burn"),
426        }
427    }
428}
429
430impl FromStr for TokenTransactionType {
431    type Err = String;
432
433    fn from_str(s: &str) -> Result<Self, Self::Err> {
434        match s.to_lowercase().as_str() {
435            "transfer" => Ok(TokenTransactionType::Transfer),
436            "mint" => Ok(TokenTransactionType::Mint),
437            "burn" => Ok(TokenTransactionType::Burn),
438            _ => Err(format!("Invalid token transaction type '{s}'")),
439        }
440    }
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
444#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
445pub struct SparkInvoicePaymentDetails {
446    /// Represents the spark invoice description
447    pub description: Option<String>,
448    /// The raw spark invoice string
449    pub invoice: String,
450}
451
452#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
453#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
454pub struct SparkHtlcDetails {
455    /// The payment hash of the HTLC
456    pub payment_hash: String,
457    /// The preimage of the HTLC. Empty until receiver has released it.
458    pub preimage: Option<String>,
459    /// The expiry time of the HTLC as a unix timestamp in seconds
460    pub expiry_time: u64,
461    /// The HTLC status
462    pub status: SparkHtlcStatus,
463}
464
465#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
466#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
467pub enum SparkHtlcStatus {
468    /// The HTLC is waiting for the preimage to be shared by the receiver
469    WaitingForPreimage,
470    /// The HTLC preimage has been shared and the transfer can be or has been claimed by the receiver
471    PreimageShared,
472    /// The HTLC has been returned to the sender due to expiry
473    Returned,
474}
475
476impl fmt::Display for SparkHtlcStatus {
477    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
478        match self {
479            SparkHtlcStatus::WaitingForPreimage => write!(f, "WaitingForPreimage"),
480            SparkHtlcStatus::PreimageShared => write!(f, "PreimageShared"),
481            SparkHtlcStatus::Returned => write!(f, "Returned"),
482        }
483    }
484}
485
486impl FromStr for SparkHtlcStatus {
487    type Err = String;
488
489    fn from_str(s: &str) -> Result<Self, Self::Err> {
490        match s {
491            "WaitingForPreimage" => Ok(SparkHtlcStatus::WaitingForPreimage),
492            "PreimageShared" => Ok(SparkHtlcStatus::PreimageShared),
493            "Returned" => Ok(SparkHtlcStatus::Returned),
494            _ => Err("Invalid Spark HTLC status".to_string()),
495        }
496    }
497}
498
499#[derive(Debug, Clone, Copy)]
500#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
501pub enum Network {
502    Mainnet,
503    Regtest,
504}
505
506impl std::fmt::Display for Network {
507    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
508        match self {
509            Network::Mainnet => write!(f, "Mainnet"),
510            Network::Regtest => write!(f, "Regtest"),
511        }
512    }
513}
514
515impl From<Network> for BitcoinNetwork {
516    fn from(network: Network) -> Self {
517        match network {
518            Network::Mainnet => BitcoinNetwork::Bitcoin,
519            Network::Regtest => BitcoinNetwork::Regtest,
520        }
521    }
522}
523
524impl From<Network> for breez_sdk_common::network::BitcoinNetwork {
525    fn from(network: Network) -> Self {
526        match network {
527            Network::Mainnet => breez_sdk_common::network::BitcoinNetwork::Bitcoin,
528            Network::Regtest => breez_sdk_common::network::BitcoinNetwork::Regtest,
529        }
530    }
531}
532
533impl From<Network> for bitcoin::Network {
534    fn from(network: Network) -> Self {
535        match network {
536            Network::Mainnet => bitcoin::Network::Bitcoin,
537            Network::Regtest => bitcoin::Network::Regtest,
538        }
539    }
540}
541
542impl FromStr for Network {
543    type Err = String;
544
545    fn from_str(s: &str) -> Result<Self, Self::Err> {
546        match s {
547            "mainnet" => Ok(Network::Mainnet),
548            "regtest" => Ok(Network::Regtest),
549            _ => Err("Invalid network".to_string()),
550        }
551    }
552}
553
554#[derive(Debug, Clone)]
555#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
556#[allow(clippy::struct_excessive_bools)]
557pub struct Config {
558    pub api_key: Option<String>,
559    pub network: Network,
560    pub sync_interval_secs: u32,
561
562    // The maximum fee that can be paid for a static deposit claim
563    // If not set then any fee is allowed
564    pub max_deposit_claim_fee: Option<MaxFee>,
565
566    /// The domain used for receiving through lnurl-pay and lightning address.
567    pub lnurl_domain: Option<String>,
568
569    /// When this is set to `true` we will prefer to use spark payments over
570    /// lightning when sending and receiving. This has the benefit of lower fees
571    /// but is at the cost of privacy.
572    pub prefer_spark_over_lightning: bool,
573
574    /// A set of external input parsers that are used by [`BreezSdk::parse`](crate::sdk::BreezSdk::parse) when the input
575    /// is not recognized. See [`ExternalInputParser`] for more details on how to configure
576    /// external parsing.
577    pub external_input_parsers: Option<Vec<ExternalInputParser>>,
578    /// The SDK includes some default external input parsers
579    /// ([`DEFAULT_EXTERNAL_INPUT_PARSERS`]).
580    /// Set this to false in order to prevent their use.
581    pub use_default_external_input_parsers: bool,
582
583    /// Url to use for the real-time sync server. Defaults to the Breez real-time sync server.
584    pub real_time_sync_server_url: Option<String>,
585
586    /// Whether the Spark private mode is enabled by default.
587    ///
588    /// If set to true, the Spark private mode will be enabled on the first initialization of the SDK.
589    /// If set to false, no changes will be made to the Spark private mode.
590    pub private_enabled_default: bool,
591
592    /// Configuration for leaf optimization.
593    ///
594    /// Leaf optimization controls the denominations of leaves that are held in the wallet.
595    /// Fewer, bigger leaves allow for more funds to be exited unilaterally.
596    /// More leaves allow payments to be made without needing a swap, reducing payment latency.
597    pub optimization_config: OptimizationConfig,
598
599    /// Configuration for automatic conversion of Bitcoin to stable tokens.
600    ///
601    /// When set, received sats will be automatically converted to the specified token
602    /// once the balance exceeds the threshold.
603    pub stable_balance_config: Option<StableBalanceConfig>,
604
605    /// Maximum number of concurrent transfer claims.
606    ///
607    /// Default is 4. Increase for server environments with high incoming payment volume.
608    pub max_concurrent_claims: u32,
609
610    /// Optional custom Spark environment configuration.
611    ///
612    /// When set, overrides the default Spark operator pool, service provider,
613    /// threshold, and token settings. Use this to connect to alternative Spark
614    /// deployments (e.g. dev/staging environments).
615    pub spark_config: Option<SparkConfig>,
616}
617
618#[derive(Debug, Clone)]
619#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
620pub struct OptimizationConfig {
621    /// Whether automatic leaf optimization is enabled.
622    ///
623    /// If set to true, the SDK will automatically optimize the leaf set when it changes.
624    /// Otherwise, the manual optimization API must be used to optimize the leaf set.
625    ///
626    /// Default value is true.
627    pub auto_enabled: bool,
628    /// The desired multiplicity for the leaf set.
629    ///
630    /// Setting this to 0 will optimize for maximizing unilateral exit.
631    /// Higher values will optimize for minimizing transfer swaps, with higher values
632    /// being more aggressive and allowing better TPS rates.
633    ///
634    /// For end-user wallets, values of 1-5 are recommended. Values above 5 are
635    /// intended for high-throughput server environments and are not recommended
636    /// for end-user wallets due to significantly higher unilateral exit costs.
637    ///
638    /// Default value is 1.
639    pub multiplicity: u8,
640}
641
642/// A stable token that can be used for automatic balance conversion.
643#[derive(Debug, Clone)]
644#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
645pub struct StableBalanceToken {
646    /// Integrator-defined display label for the token, e.g. "USD".
647    ///
648    /// This is a short, human-readable name set by the integrator for display purposes.
649    /// It is **not** a canonical Spark token ticker — it has no protocol-level meaning.
650    /// Labels must be unique within the [`StableBalanceConfig::tokens`] list.
651    pub label: String,
652
653    /// The full token identifier string used for conversions.
654    pub token_identifier: String,
655}
656
657/// Configuration for automatic conversion of Bitcoin to stable tokens.
658///
659/// When configured, the SDK automatically monitors the Bitcoin balance after each
660/// wallet sync. When the balance exceeds the configured threshold plus the reserved
661/// amount, the SDK automatically converts the excess balance (above the reserve)
662/// to the active stable token.
663///
664/// When the balance is held in a stable token, Bitcoin payments can still be sent.
665/// The SDK automatically detects when there's not enough Bitcoin balance to cover a
666/// payment and auto-populates the token-to-Bitcoin conversion options to facilitate
667/// the payment.
668///
669/// The active token can be changed at runtime via [`UpdateUserSettingsRequest`].
670#[derive(Debug, Clone)]
671#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
672pub struct StableBalanceConfig {
673    /// Available tokens that can be used for stable balance.
674    pub tokens: Vec<StableBalanceToken>,
675
676    /// The label of the token to activate by default.
677    ///
678    /// If `None`, stable balance starts deactivated. The user can activate it
679    /// at runtime via [`UpdateUserSettingsRequest`]. If a user setting is cached
680    /// locally, it takes precedence over this default.
681    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
682    pub default_active_label: Option<String>,
683
684    /// The minimum sats balance that triggers auto-conversion.
685    ///
686    /// If not provided, uses the minimum from conversion limits.
687    /// If provided but less than the conversion limit minimum, the limit minimum is used.
688    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
689    pub threshold_sats: Option<u64>,
690
691    /// Maximum slippage in basis points (1/100 of a percent).
692    ///
693    /// Defaults to 10 bps (0.1%) if not set.
694    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
695    pub max_slippage_bps: Option<u32>,
696}
697
698/// Specifies how to update the active stable balance token.
699#[derive(Debug, Clone)]
700#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
701pub enum StableBalanceActiveLabel {
702    /// Activate stable balance with the given label.
703    Set { label: String },
704    /// Deactivate stable balance.
705    Unset,
706}
707
708/// Configuration for a custom Spark environment.
709///
710/// When set on [`Config`], overrides the default Spark operator pool,
711/// service provider, threshold, and token settings. This allows connecting
712/// to alternative Spark deployments (e.g. dev/staging environments).
713#[derive(Debug, Clone)]
714#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
715pub struct SparkConfig {
716    /// Hex-encoded identifier of the coordinator operator.
717    pub coordinator_identifier: String,
718    /// The FROST signing threshold (e.g. 2 of 3).
719    pub threshold: u32,
720    /// The set of signing operators.
721    pub signing_operators: Vec<SparkSigningOperator>,
722    /// Service provider (SSP) configuration.
723    pub ssp_config: SparkSspConfig,
724    /// Expected bond amount in sats for token withdrawals.
725    pub expected_withdraw_bond_sats: u64,
726    /// Expected relative block locktime for token withdrawals.
727    pub expected_withdraw_relative_block_locktime: u64,
728}
729
730/// A Spark signing operator.
731#[derive(Debug, Clone)]
732#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
733pub struct SparkSigningOperator {
734    /// Sequential operator ID (0-indexed).
735    pub id: u32,
736    /// Hex-encoded 32-byte FROST identifier.
737    pub identifier: String,
738    /// gRPC address of the operator (e.g. `https://0.spark.lightspark.com`).
739    pub address: String,
740    /// Hex-encoded compressed public key of the operator.
741    pub identity_public_key: String,
742}
743
744/// Configuration for the Spark Service Provider (SSP).
745#[derive(Debug, Clone)]
746#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
747pub struct SparkSspConfig {
748    /// Base URL of the SSP GraphQL API.
749    pub base_url: String,
750    /// Hex-encoded compressed public key of the SSP.
751    pub identity_public_key: String,
752    /// Optional GraphQL schema endpoint path (e.g. "graphql/spark/rc").
753    /// Defaults to the hardcoded schema endpoint if not set.
754    pub schema_endpoint: Option<String>,
755}
756
757impl Config {
758    /// Validates the configuration.
759    ///
760    /// Returns an error if any configuration values are invalid.
761    pub fn validate(&self) -> Result<(), SdkError> {
762        if self.max_concurrent_claims == 0 {
763            return Err(SdkError::InvalidInput(
764                "max_concurrent_claims must be greater than 0".to_string(),
765            ));
766        }
767
768        if let Some(sb) = &self.stable_balance_config {
769            if sb.tokens.is_empty() {
770                return Err(SdkError::InvalidInput(
771                    "tokens must not be empty".to_string(),
772                ));
773            }
774
775            let mut seen_labels = HashSet::new();
776            let mut seen_identifiers = HashSet::new();
777            for token in &sb.tokens {
778                if token.label.is_empty() {
779                    return Err(SdkError::InvalidInput(
780                        "token label must not be empty".to_string(),
781                    ));
782                }
783                if token.token_identifier.is_empty() {
784                    return Err(SdkError::InvalidInput(
785                        "token_identifier must not be empty".to_string(),
786                    ));
787                }
788                if !seen_labels.insert(&token.label) {
789                    return Err(SdkError::InvalidInput(format!(
790                        "tokens contains duplicate label: {}",
791                        token.label
792                    )));
793                }
794                if !seen_identifiers.insert(&token.token_identifier) {
795                    return Err(SdkError::InvalidInput(format!(
796                        "tokens contains duplicate token_identifier: {}",
797                        token.token_identifier
798                    )));
799                }
800            }
801
802            if let Some(bps) = sb.max_slippage_bps
803                && bps > 10000
804            {
805                return Err(SdkError::InvalidInput(
806                    "max_slippage_bps must be <= 10000".to_string(),
807                ));
808            }
809
810            if let Some(default_label) = &sb.default_active_label
811                && !seen_labels.contains(default_label)
812            {
813                return Err(SdkError::InvalidInput(format!(
814                    "default_active_label '{default_label}' not found in tokens list"
815                )));
816            }
817        }
818
819        Ok(())
820    }
821
822    pub(crate) fn get_all_external_input_parsers(&self) -> Vec<ExternalInputParser> {
823        let mut external_input_parsers = Vec::new();
824        if self.use_default_external_input_parsers {
825            let default_parsers = DEFAULT_EXTERNAL_INPUT_PARSERS
826                .iter()
827                .map(|(id, regex, url)| ExternalInputParser {
828                    provider_id: (*id).to_string(),
829                    input_regex: (*regex).to_string(),
830                    parser_url: (*url).to_string(),
831                })
832                .collect::<Vec<_>>();
833            external_input_parsers.extend(default_parsers);
834        }
835        external_input_parsers.extend(self.external_input_parsers.clone().unwrap_or_default());
836
837        external_input_parsers
838    }
839}
840
841#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
842#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
843pub enum MaxFee {
844    // Fixed fee amount in sats
845    Fixed { amount: u64 },
846    // Relative fee rate in satoshis per vbyte
847    Rate { sat_per_vbyte: u64 },
848    // Fastest network recommended fee at the time of claim, with a leeway in satoshis per vbyte
849    NetworkRecommended { leeway_sat_per_vbyte: u64 },
850}
851
852impl MaxFee {
853    pub(crate) async fn to_fee(&self, client: &dyn BitcoinChainService) -> Result<Fee, SdkError> {
854        match self {
855            MaxFee::Fixed { amount } => Ok(Fee::Fixed { amount: *amount }),
856            MaxFee::Rate { sat_per_vbyte } => Ok(Fee::Rate {
857                sat_per_vbyte: *sat_per_vbyte,
858            }),
859            MaxFee::NetworkRecommended {
860                leeway_sat_per_vbyte,
861            } => {
862                let recommended_fees = client.recommended_fees().await?;
863                let max_fee_rate = recommended_fees
864                    .fastest_fee
865                    .saturating_add(*leeway_sat_per_vbyte);
866                Ok(Fee::Rate {
867                    sat_per_vbyte: max_fee_rate,
868                })
869            }
870        }
871    }
872}
873
874#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
875#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
876pub enum Fee {
877    // Fixed fee amount in sats
878    Fixed { amount: u64 },
879    // Relative fee rate in satoshis per vbyte
880    Rate { sat_per_vbyte: u64 },
881}
882
883impl Fee {
884    pub fn to_sats(&self, vbytes: u64) -> u64 {
885        match self {
886            Fee::Fixed { amount } => *amount,
887            Fee::Rate { sat_per_vbyte } => sat_per_vbyte.saturating_mul(vbytes),
888        }
889    }
890}
891
892#[derive(Debug, Clone, Serialize, Deserialize)]
893#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
894pub struct DepositInfo {
895    pub txid: String,
896    pub vout: u32,
897    pub amount_sats: u64,
898    pub is_mature: bool,
899    pub refund_tx: Option<String>,
900    pub refund_tx_id: Option<String>,
901    pub claim_error: Option<DepositClaimError>,
902}
903
904#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
905pub struct ClaimDepositRequest {
906    pub txid: String,
907    pub vout: u32,
908    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
909    pub max_fee: Option<MaxFee>,
910}
911
912#[derive(Debug, Clone, Serialize)]
913#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
914pub struct ClaimDepositResponse {
915    pub payment: Payment,
916}
917
918#[derive(Debug, Clone, Serialize)]
919#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
920pub struct RefundDepositRequest {
921    pub txid: String,
922    pub vout: u32,
923    pub destination_address: String,
924    pub fee: Fee,
925}
926
927#[derive(Debug, Clone, Serialize)]
928#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
929pub struct RefundDepositResponse {
930    pub tx_id: String,
931    pub tx_hex: String,
932}
933
934#[derive(Debug, Clone, Serialize)]
935#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
936pub struct ListUnclaimedDepositsRequest {}
937
938#[derive(Debug, Clone, Serialize)]
939#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
940pub struct ListUnclaimedDepositsResponse {
941    pub deposits: Vec<DepositInfo>,
942}
943
944/// The available providers for buying Bitcoin
945/// Request to buy Bitcoin using an external provider.
946///
947/// Each variant carries only the parameters relevant to that provider.
948#[derive(Debug, Clone)]
949#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
950pub enum BuyBitcoinRequest {
951    /// `MoonPay`: Fiat-to-Bitcoin via credit card, Apple Pay, etc.
952    /// Uses an on-chain deposit address.
953    Moonpay {
954        /// Lock the purchase to a specific amount in satoshis.
955        locked_amount_sat: Option<u64>,
956        /// Custom redirect URL after purchase completion.
957        redirect_url: Option<String>,
958    },
959    /// `CashApp`: Pay via the Lightning Network.
960    /// Generates a bolt11 invoice and returns a `cash.app` deep link.
961    /// Only available on mainnet.
962    CashApp {
963        /// Amount in satoshis for the Lightning invoice.
964        amount_sats: Option<u64>,
965    },
966}
967
968impl Default for BuyBitcoinRequest {
969    fn default() -> Self {
970        Self::Moonpay {
971            locked_amount_sat: None,
972            redirect_url: None,
973        }
974    }
975}
976
977/// Response containing a URL to complete the Bitcoin purchase
978#[derive(Debug, Clone, Serialize)]
979#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
980pub struct BuyBitcoinResponse {
981    /// The URL to open in a browser to complete the purchase
982    pub url: String,
983}
984
985impl std::fmt::Display for MaxFee {
986    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
987        match self {
988            MaxFee::Fixed { amount } => write!(f, "Fixed: {amount}"),
989            MaxFee::Rate { sat_per_vbyte } => write!(f, "Rate: {sat_per_vbyte}"),
990            MaxFee::NetworkRecommended {
991                leeway_sat_per_vbyte,
992            } => write!(f, "NetworkRecommended: {leeway_sat_per_vbyte}"),
993        }
994    }
995}
996
997#[derive(Debug, Clone)]
998#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
999pub struct Credentials {
1000    pub username: String,
1001    pub password: String,
1002}
1003
1004/// Request to get the balance of the wallet
1005#[derive(Debug, Clone)]
1006#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1007pub struct GetInfoRequest {
1008    pub ensure_synced: Option<bool>,
1009}
1010
1011/// Response containing the balance of the wallet
1012#[derive(Debug, Clone, Serialize)]
1013#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1014pub struct GetInfoResponse {
1015    /// The identity public key of the wallet as a hex string
1016    pub identity_pubkey: String,
1017    /// The balance in satoshis
1018    pub balance_sats: u64,
1019    /// The balances of the tokens in the wallet keyed by the token identifier
1020    pub token_balances: HashMap<String, TokenBalance>,
1021}
1022
1023#[derive(Debug, Clone, Serialize, Deserialize)]
1024#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1025pub struct TokenBalance {
1026    pub balance: u128,
1027    pub token_metadata: TokenMetadata,
1028}
1029
1030#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1031#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1032pub struct TokenMetadata {
1033    pub identifier: String,
1034    /// Hex representation of the issuer public key
1035    pub issuer_public_key: String,
1036    pub name: String,
1037    pub ticker: String,
1038    /// Number of decimals the token uses
1039    pub decimals: u32,
1040    pub max_supply: u128,
1041    pub is_freezable: bool,
1042}
1043
1044/// Request to sync the wallet with the Spark network
1045#[derive(Debug, Clone)]
1046#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1047pub struct SyncWalletRequest {}
1048
1049/// Response from synchronizing the wallet
1050#[derive(Debug, Clone, Serialize)]
1051#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1052pub struct SyncWalletResponse {}
1053
1054#[derive(Debug, Clone, Serialize)]
1055#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1056pub enum ReceivePaymentMethod {
1057    SparkAddress,
1058    SparkInvoice {
1059        /// Amount to receive. Denominated in sats if token identifier is empty, otherwise in the token base units
1060        amount: Option<u128>,
1061        /// The presence of this field indicates that the payment is for a token
1062        /// If empty, it is a Bitcoin payment
1063        token_identifier: Option<String>,
1064        /// The expiry time of the invoice as a unix timestamp in seconds
1065        expiry_time: Option<u64>,
1066        /// A description to embed in the invoice.
1067        description: Option<String>,
1068        /// If set, the invoice may only be fulfilled by a payer with this public key
1069        sender_public_key: Option<String>,
1070    },
1071    BitcoinAddress {
1072        /// If true, rotate to a new deposit address. Previous ones remain valid.
1073        /// If false or absent, return the existing address (creating one if none
1074        /// exists yet).
1075        new_address: Option<bool>,
1076    },
1077    Bolt11Invoice {
1078        description: String,
1079        amount_sats: Option<u64>,
1080        /// The expiry of the invoice as a duration in seconds
1081        expiry_secs: Option<u32>,
1082        /// If set, creates a HODL invoice with this payment hash (hex-encoded).
1083        /// The payer's HTLC will be held until the preimage is provided via
1084        /// `claim_htlc_payment` or the HTLC expires.
1085        payment_hash: Option<String>,
1086    },
1087}
1088
1089#[derive(Debug, Clone, Serialize)]
1090#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1091pub enum SendPaymentMethod {
1092    BitcoinAddress {
1093        address: BitcoinAddressDetails,
1094        fee_quote: SendOnchainFeeQuote,
1095    },
1096    Bolt11Invoice {
1097        invoice_details: Bolt11InvoiceDetails,
1098        spark_transfer_fee_sats: Option<u64>,
1099        lightning_fee_sats: u64,
1100    }, // should be replaced with the parsed invoice
1101    SparkAddress {
1102        address: String,
1103        /// Fee to pay for the transaction
1104        /// Denominated in sats if token identifier is empty, otherwise in the token base units
1105        fee: u128,
1106        /// The presence of this field indicates that the payment is for a token
1107        /// If empty, it is a Bitcoin payment
1108        token_identifier: Option<String>,
1109    },
1110    SparkInvoice {
1111        spark_invoice_details: SparkInvoiceDetails,
1112        /// Fee to pay for the transaction
1113        /// Denominated in sats if token identifier is empty, otherwise in the token base units
1114        fee: u128,
1115        /// The presence of this field indicates that the payment is for a token
1116        /// If empty, it is a Bitcoin payment
1117        token_identifier: Option<String>,
1118    },
1119}
1120
1121#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1122#[derive(Debug, Clone, Serialize)]
1123pub struct SendOnchainFeeQuote {
1124    pub id: String,
1125    pub expires_at: u64,
1126    pub speed_fast: SendOnchainSpeedFeeQuote,
1127    pub speed_medium: SendOnchainSpeedFeeQuote,
1128    pub speed_slow: SendOnchainSpeedFeeQuote,
1129}
1130
1131#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1132#[derive(Debug, Clone, Serialize)]
1133pub struct SendOnchainSpeedFeeQuote {
1134    pub user_fee_sat: u64,
1135    pub l1_broadcast_fee_sat: u64,
1136}
1137
1138impl SendOnchainSpeedFeeQuote {
1139    pub fn total_fee_sat(&self) -> u64 {
1140        self.user_fee_sat.saturating_add(self.l1_broadcast_fee_sat)
1141    }
1142}
1143
1144#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1145pub struct ReceivePaymentRequest {
1146    pub payment_method: ReceivePaymentMethod,
1147}
1148
1149#[derive(Debug, Clone, Serialize)]
1150#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1151pub struct ReceivePaymentResponse {
1152    pub payment_request: String,
1153    /// Fee to pay to receive the payment
1154    /// Denominated in sats or token base units
1155    pub fee: u128,
1156}
1157
1158#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1159pub struct PrepareLnurlPayRequest {
1160    /// The amount to send. Denominated in satoshis, or in token base units
1161    /// when `token_identifier` is set.
1162    pub amount: u128,
1163    pub pay_request: LnurlPayRequestDetails,
1164    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1165    pub comment: Option<String>,
1166    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1167    pub validate_success_action_url: Option<bool>,
1168    /// The token identifier when sending a token amount with conversion.
1169    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1170    pub token_identifier: Option<String>,
1171    /// If provided, the payment will include a token conversion step before sending the payment
1172    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1173    pub conversion_options: Option<ConversionOptions>,
1174    /// How fees should be handled. Defaults to `FeesExcluded` (fees added on top).
1175    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1176    pub fee_policy: Option<FeePolicy>,
1177}
1178
1179#[derive(Debug, Clone)]
1180#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1181pub struct PrepareLnurlPayResponse {
1182    /// The amount for the payment, always denominated in sats, even when a
1183    /// `token_identifier` and conversion are present.
1184    /// When a conversion is present, the token input amount is available in
1185    /// `conversion_estimate.amount_in`.
1186    pub amount_sats: u64,
1187    pub comment: Option<String>,
1188    pub pay_request: LnurlPayRequestDetails,
1189    /// The fee in satoshis. For `FeesIncluded` operations, this represents the total fee
1190    /// (including potential overpayment).
1191    pub fee_sats: u64,
1192    pub invoice_details: Bolt11InvoiceDetails,
1193    pub success_action: Option<SuccessAction>,
1194    /// When set, the payment will include a token conversion step before sending the payment
1195    pub conversion_estimate: Option<ConversionEstimate>,
1196    /// How fees are handled for this payment.
1197    pub fee_policy: FeePolicy,
1198}
1199
1200#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1201pub struct LnurlPayRequest {
1202    pub prepare_response: PrepareLnurlPayResponse,
1203    /// If set, providing the same idempotency key for multiple requests will ensure that only one
1204    /// payment is made. If an idempotency key is re-used, the same payment will be returned.
1205    /// The idempotency key must be a valid UUID.
1206    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1207    pub idempotency_key: Option<String>,
1208}
1209
1210#[derive(Debug, Serialize)]
1211#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1212pub struct LnurlPayResponse {
1213    pub payment: Payment,
1214    pub success_action: Option<SuccessActionProcessed>,
1215}
1216
1217#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1218pub struct LnurlWithdrawRequest {
1219    /// The amount to withdraw in satoshis
1220    /// Must be within the min and max withdrawable limits
1221    pub amount_sats: u64,
1222    pub withdraw_request: LnurlWithdrawRequestDetails,
1223    /// If set, the function will return the payment if it is still pending after this
1224    /// number of seconds. If unset, the function will return immediately after
1225    /// initiating the LNURL withdraw.
1226    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1227    pub completion_timeout_secs: Option<u32>,
1228}
1229
1230#[derive(Debug, Serialize)]
1231#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1232pub struct LnurlWithdrawResponse {
1233    /// The Lightning invoice generated for the LNURL withdraw
1234    pub payment_request: String,
1235    pub payment: Option<Payment>,
1236}
1237
1238/// Represents the payment LNURL info
1239#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
1240#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1241pub struct LnurlPayInfo {
1242    pub ln_address: Option<String>,
1243    pub comment: Option<String>,
1244    pub domain: Option<String>,
1245    pub metadata: Option<String>,
1246    pub processed_success_action: Option<SuccessActionProcessed>,
1247    pub raw_success_action: Option<SuccessAction>,
1248}
1249
1250/// Represents the withdraw LNURL info
1251#[derive(Clone, Debug, Deserialize, Serialize)]
1252#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1253pub struct LnurlWithdrawInfo {
1254    pub withdraw_url: String,
1255}
1256
1257impl LnurlPayInfo {
1258    pub fn extract_description(&self) -> Option<String> {
1259        let Some(metadata) = &self.metadata else {
1260            return None;
1261        };
1262
1263        let Ok(metadata) = serde_json::from_str::<Vec<Vec<Value>>>(metadata) else {
1264            return None;
1265        };
1266
1267        for arr in metadata {
1268            if arr.len() != 2 {
1269                continue;
1270            }
1271            if let (Some(key), Some(value)) = (arr[0].as_str(), arr[1].as_str())
1272                && key == "text/plain"
1273            {
1274                return Some(value.to_string());
1275            }
1276        }
1277
1278        None
1279    }
1280}
1281
1282/// Specifies how fees are handled in a payment.
1283#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1284#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1285pub enum FeePolicy {
1286    /// Fees are added on top of the specified amount (default behavior).
1287    /// The receiver gets the exact amount specified.
1288    #[default]
1289    FeesExcluded,
1290    /// Fees are deducted from the specified amount.
1291    /// The receiver gets the amount minus fees.
1292    FeesIncluded,
1293}
1294
1295#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1296#[derive(Debug, Clone, Serialize)]
1297pub enum OnchainConfirmationSpeed {
1298    Fast,
1299    Medium,
1300    Slow,
1301}
1302
1303#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1304pub struct PrepareSendPaymentRequest {
1305    pub payment_request: String,
1306    /// The amount to send.
1307    /// Optional for payment requests with embedded amounts (e.g., Spark/Bolt11 invoices with amounts).
1308    /// Required for Spark addresses, Bitcoin addresses, and amountless invoices.
1309    /// Denominated in satoshis for Bitcoin payments, or token base units for token payments.
1310    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1311    pub amount: Option<u128>,
1312    /// Optional token identifier for token payments.
1313    /// Absence indicates that the payment is a Bitcoin payment.
1314    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1315    pub token_identifier: Option<String>,
1316    /// If provided, the payment will include a conversion step before sending the payment
1317    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1318    pub conversion_options: Option<ConversionOptions>,
1319    /// How fees should be handled. Defaults to `FeesExcluded` (fees added on top).
1320    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1321    pub fee_policy: Option<FeePolicy>,
1322}
1323
1324#[derive(Debug, Clone, Serialize)]
1325#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1326pub struct PrepareSendPaymentResponse {
1327    pub payment_method: SendPaymentMethod,
1328    /// The amount to be sent, denominated in satoshis for Bitcoin payments
1329    /// (including token-to-Bitcoin conversions), or token base units for token payments.
1330    /// When a conversion is present, the input amount is in
1331    /// `conversion_estimate.amount_in`.
1332    pub amount: u128,
1333    /// Optional token identifier for token payments.
1334    /// Absence indicates that the payment is a Bitcoin payment.
1335    pub token_identifier: Option<String>,
1336    /// When set, the payment will include a conversion step before sending the payment
1337    pub conversion_estimate: Option<ConversionEstimate>,
1338    /// How fees are handled for this payment.
1339    pub fee_policy: FeePolicy,
1340}
1341
1342#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1343pub enum SendPaymentOptions {
1344    BitcoinAddress {
1345        /// Confirmation speed for the on-chain transaction.
1346        confirmation_speed: OnchainConfirmationSpeed,
1347    },
1348    Bolt11Invoice {
1349        prefer_spark: bool,
1350
1351        /// If set, the function will return the payment if it is still pending after this
1352        /// number of seconds. If unset, the function will return immediately after initiating the payment.
1353        completion_timeout_secs: Option<u32>,
1354    },
1355    SparkAddress {
1356        /// Can only be provided for Bitcoin payments. If set, a Spark HTLC transfer will be created.
1357        /// The receiver will need to provide the preimage to claim it.
1358        htlc_options: Option<SparkHtlcOptions>,
1359    },
1360}
1361
1362#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1363pub struct SparkHtlcOptions {
1364    /// The payment hash of the HTLC. The receiver will need to provide the associated preimage to claim it.
1365    pub payment_hash: String,
1366    /// The duration of the HTLC in seconds.
1367    /// After this time, the HTLC will be returned.
1368    pub expiry_duration_secs: u64,
1369}
1370
1371#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1372pub struct SendPaymentRequest {
1373    pub prepare_response: PrepareSendPaymentResponse,
1374    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1375    pub options: Option<SendPaymentOptions>,
1376    /// The optional idempotency key for all Spark based transfers (excludes token payments).
1377    /// If set, providing the same idempotency key for multiple requests will ensure that only one
1378    /// payment is made. If an idempotency key is re-used, the same payment will be returned.
1379    /// The idempotency key must be a valid UUID.
1380    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1381    pub idempotency_key: Option<String>,
1382}
1383
1384#[derive(Debug, Clone, Serialize)]
1385#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1386pub struct SendPaymentResponse {
1387    pub payment: Payment,
1388}
1389
1390#[derive(Debug, Clone)]
1391#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1392pub enum PaymentDetailsFilter {
1393    Spark {
1394        /// Filter specific Spark HTLC statuses
1395        htlc_status: Option<Vec<SparkHtlcStatus>>,
1396        /// Filter conversion payments with refund information
1397        conversion_refund_needed: Option<bool>,
1398    },
1399    Token {
1400        /// Filter conversion payments with refund information
1401        conversion_refund_needed: Option<bool>,
1402        /// Filter by transaction hash
1403        tx_hash: Option<String>,
1404        /// Filter by transaction type
1405        tx_type: Option<TokenTransactionType>,
1406    },
1407    Lightning {
1408        /// Filter specific Spark HTLC statuses
1409        htlc_status: Option<Vec<SparkHtlcStatus>>,
1410    },
1411}
1412
1413/// Request to list payments with optional filters and pagination
1414#[derive(Debug, Clone, Default)]
1415#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1416pub struct ListPaymentsRequest {
1417    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1418    pub type_filter: Option<Vec<PaymentType>>,
1419    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1420    pub status_filter: Option<Vec<PaymentStatus>>,
1421    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1422    pub asset_filter: Option<AssetFilter>,
1423    /// Only include payments matching at least one of these payment details filters
1424    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1425    pub payment_details_filter: Option<Vec<PaymentDetailsFilter>>,
1426    /// Only include payments created after this timestamp (inclusive)
1427    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1428    pub from_timestamp: Option<u64>,
1429    /// Only include payments created before this timestamp (exclusive)
1430    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1431    pub to_timestamp: Option<u64>,
1432    /// Number of records to skip
1433    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1434    pub offset: Option<u32>,
1435    /// Maximum number of records to return
1436    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1437    pub limit: Option<u32>,
1438    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1439    pub sort_ascending: Option<bool>,
1440}
1441
1442/// A field of [`ListPaymentsRequest`] when listing payments filtered by asset
1443#[derive(Debug, Clone)]
1444#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1445pub enum AssetFilter {
1446    Bitcoin,
1447    Token {
1448        /// Optional token identifier to filter by
1449        token_identifier: Option<String>,
1450    },
1451}
1452
1453impl FromStr for AssetFilter {
1454    type Err = String;
1455
1456    fn from_str(s: &str) -> Result<Self, Self::Err> {
1457        Ok(match s.to_lowercase().as_str() {
1458            "bitcoin" => AssetFilter::Bitcoin,
1459            "token" => AssetFilter::Token {
1460                token_identifier: None,
1461            },
1462            str if str.starts_with("token:") => AssetFilter::Token {
1463                token_identifier: Some(
1464                    str.split_once(':')
1465                        .ok_or(format!("Invalid asset filter '{s}'"))?
1466                        .1
1467                        .to_string(),
1468                ),
1469            },
1470            _ => return Err(format!("Invalid asset filter '{s}'")),
1471        })
1472    }
1473}
1474
1475/// Response from listing payments
1476#[derive(Debug, Clone, Serialize)]
1477#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1478pub struct ListPaymentsResponse {
1479    /// The list of payments
1480    pub payments: Vec<Payment>,
1481}
1482
1483#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1484pub struct GetPaymentRequest {
1485    pub payment_id: String,
1486}
1487
1488#[derive(Debug, Clone, Serialize)]
1489#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1490pub struct GetPaymentResponse {
1491    pub payment: Payment,
1492}
1493
1494#[cfg_attr(feature = "uniffi", uniffi::export(callback_interface))]
1495pub trait Logger: Send + Sync {
1496    fn log(&self, l: LogEntry);
1497}
1498
1499#[derive(Debug, Clone, Serialize)]
1500#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1501pub struct LogEntry {
1502    pub line: String,
1503    pub level: String,
1504}
1505
1506#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1507#[derive(Debug, Clone, Serialize, Deserialize)]
1508pub struct CheckLightningAddressRequest {
1509    pub username: String,
1510}
1511
1512#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1513#[derive(Debug, Clone, Serialize, Deserialize)]
1514pub struct RegisterLightningAddressRequest {
1515    pub username: String,
1516    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1517    pub description: Option<String>,
1518}
1519
1520#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1521#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
1522pub struct LnurlInfo {
1523    pub url: String,
1524    pub bech32: String,
1525}
1526
1527impl LnurlInfo {
1528    pub fn new(url: String) -> Self {
1529        let bech32 =
1530            breez_sdk_common::lnurl::encode_lnurl_to_bech32(&url).unwrap_or_else(|_| url.clone());
1531        Self { url, bech32 }
1532    }
1533}
1534
1535#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1536#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
1537pub struct LightningAddressInfo {
1538    pub description: String,
1539    pub lightning_address: String,
1540    pub lnurl: LnurlInfo,
1541    pub username: String,
1542}
1543
1544impl From<RecoverLnurlPayResponse> for LightningAddressInfo {
1545    fn from(resp: RecoverLnurlPayResponse) -> Self {
1546        Self {
1547            description: resp.description,
1548            lightning_address: resp.lightning_address,
1549            lnurl: LnurlInfo::new(resp.lnurl),
1550            username: resp.username,
1551        }
1552    }
1553}
1554
1555#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1556#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1557pub enum KeySetType {
1558    #[default]
1559    Default,
1560    Taproot,
1561    NativeSegwit,
1562    WrappedSegwit,
1563    Legacy,
1564}
1565
1566impl From<spark_wallet::KeySetType> for KeySetType {
1567    fn from(value: spark_wallet::KeySetType) -> Self {
1568        match value {
1569            spark_wallet::KeySetType::Default => KeySetType::Default,
1570            spark_wallet::KeySetType::Taproot => KeySetType::Taproot,
1571            spark_wallet::KeySetType::NativeSegwit => KeySetType::NativeSegwit,
1572            spark_wallet::KeySetType::WrappedSegwit => KeySetType::WrappedSegwit,
1573            spark_wallet::KeySetType::Legacy => KeySetType::Legacy,
1574        }
1575    }
1576}
1577
1578impl From<KeySetType> for spark_wallet::KeySetType {
1579    fn from(value: KeySetType) -> Self {
1580        match value {
1581            KeySetType::Default => spark_wallet::KeySetType::Default,
1582            KeySetType::Taproot => spark_wallet::KeySetType::Taproot,
1583            KeySetType::NativeSegwit => spark_wallet::KeySetType::NativeSegwit,
1584            KeySetType::WrappedSegwit => spark_wallet::KeySetType::WrappedSegwit,
1585            KeySetType::Legacy => spark_wallet::KeySetType::Legacy,
1586        }
1587    }
1588}
1589
1590/// Configuration for key set derivation.
1591///
1592/// This struct encapsulates the parameters needed for BIP32 key derivation.
1593#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1594#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1595pub struct KeySetConfig {
1596    /// The key set type which determines the derivation path
1597    pub key_set_type: KeySetType,
1598    /// Controls the structure of the BIP derivation path
1599    pub use_address_index: bool,
1600    /// Optional account number for key derivation
1601    pub account_number: Option<u32>,
1602}
1603
1604impl Default for KeySetConfig {
1605    fn default() -> Self {
1606        Self {
1607            key_set_type: KeySetType::Default,
1608            use_address_index: false,
1609            account_number: None,
1610        }
1611    }
1612}
1613
1614/// Response from listing fiat currencies
1615#[derive(Debug, Clone, Serialize)]
1616#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1617pub struct ListFiatCurrenciesResponse {
1618    /// The list of fiat currencies
1619    pub currencies: Vec<FiatCurrency>,
1620}
1621
1622/// Response from listing fiat rates
1623#[derive(Debug, Clone, Serialize)]
1624#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1625pub struct ListFiatRatesResponse {
1626    /// The list of fiat rates
1627    pub rates: Vec<Rate>,
1628}
1629
1630/// The operational status of a Spark service.
1631#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1632#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1633pub enum ServiceStatus {
1634    /// Service is fully operational.
1635    Operational,
1636    /// Service is experiencing degraded performance.
1637    Degraded,
1638    /// Service is partially unavailable.
1639    Partial,
1640    /// Service status is unknown.
1641    Unknown,
1642    /// Service is experiencing a major outage.
1643    Major,
1644}
1645
1646/// The status of the Spark network services relevant to the SDK.
1647#[derive(Debug, Clone, Serialize)]
1648#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1649pub struct SparkStatus {
1650    /// The worst status across all relevant services.
1651    pub status: ServiceStatus,
1652    /// The last time the status was updated, as a unix timestamp in seconds.
1653    pub last_updated: u64,
1654}
1655
1656pub(crate) enum WaitForPaymentIdentifier {
1657    PaymentId(String),
1658    PaymentRequest(String),
1659}
1660
1661#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1662pub struct GetTokensMetadataRequest {
1663    pub token_identifiers: Vec<String>,
1664}
1665
1666#[derive(Debug, Clone, Serialize)]
1667#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1668pub struct GetTokensMetadataResponse {
1669    pub tokens_metadata: Vec<TokenMetadata>,
1670}
1671
1672#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1673pub struct SignMessageRequest {
1674    pub message: String,
1675    /// If true, the signature will be encoded in compact format instead of DER format
1676    pub compact: bool,
1677}
1678
1679#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1680pub struct SignMessageResponse {
1681    pub pubkey: String,
1682    /// The DER or compact hex encoded signature
1683    pub signature: String,
1684}
1685
1686#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1687pub struct CheckMessageRequest {
1688    /// The message that was signed
1689    pub message: String,
1690    /// The public key that signed the message
1691    pub pubkey: String,
1692    /// The DER or compact hex encoded signature
1693    pub signature: String,
1694}
1695
1696#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1697pub struct CheckMessageResponse {
1698    pub is_valid: bool,
1699}
1700
1701#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1702#[derive(Debug, Clone, Serialize)]
1703pub struct UserSettings {
1704    pub spark_private_mode_enabled: bool,
1705
1706    /// The label of the currently active stable balance token, or `None` if deactivated.
1707    pub stable_balance_active_label: Option<String>,
1708}
1709
1710#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1711pub struct UpdateUserSettingsRequest {
1712    pub spark_private_mode_enabled: Option<bool>,
1713
1714    /// Update the active stable balance token. `None` means no change.
1715    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
1716    pub stable_balance_active_label: Option<StableBalanceActiveLabel>,
1717}
1718
1719#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1720pub struct ClaimHtlcPaymentRequest {
1721    pub preimage: String,
1722}
1723
1724#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1725pub struct ClaimHtlcPaymentResponse {
1726    pub payment: Payment,
1727}
1728
1729#[derive(Debug, Clone, Deserialize, Serialize)]
1730#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1731pub struct LnurlReceiveMetadata {
1732    pub nostr_zap_request: Option<String>,
1733    pub nostr_zap_receipt: Option<String>,
1734    pub sender_comment: Option<String>,
1735}
1736
1737#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1738pub struct OptimizationProgress {
1739    pub is_running: bool,
1740    pub current_round: u32,
1741    pub total_rounds: u32,
1742}
1743
1744/// A contact entry containing a name and payment identifier.
1745#[derive(Debug, Clone, Serialize, Deserialize)]
1746#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1747pub struct Contact {
1748    pub id: String,
1749    pub name: String,
1750    /// A Lightning address (user@domain).
1751    pub payment_identifier: String,
1752    pub created_at: u64,
1753    pub updated_at: u64,
1754}
1755
1756/// Request to add a new contact.
1757#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1758pub struct AddContactRequest {
1759    pub name: String,
1760    /// A Lightning address (user@domain).
1761    pub payment_identifier: String,
1762}
1763
1764/// Request to update an existing contact.
1765#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1766pub struct UpdateContactRequest {
1767    pub id: String,
1768    pub name: String,
1769    /// A Lightning address (user@domain).
1770    pub payment_identifier: String,
1771}
1772
1773/// Request to list contacts with optional pagination.
1774#[derive(Debug, Clone, Default)]
1775#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1776pub struct ListContactsRequest {
1777    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1778    pub offset: Option<u32>,
1779    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1780    pub limit: Option<u32>,
1781}
1782
1783/// The type of event that triggers a webhook notification.
1784#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1785#[allow(clippy::enum_variant_names)]
1786#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1787pub enum WebhookEventType {
1788    /// Triggered when a Lightning receive operation completes.
1789    LightningReceiveFinished,
1790    /// Triggered when a Lightning send operation completes.
1791    LightningSendFinished,
1792    /// Triggered when a cooperative exit completes.
1793    CoopExitFinished,
1794    /// Triggered when a static deposit completes.
1795    StaticDepositFinished,
1796    /// An event type not yet recognized by this version of the SDK.
1797    Unknown(String),
1798}
1799
1800/// A registered webhook entry.
1801#[derive(Debug, Clone, Serialize)]
1802#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1803pub struct Webhook {
1804    /// Unique identifier for this webhook.
1805    pub id: String,
1806    /// The URL that receives webhook notifications.
1807    pub url: String,
1808    /// The event types this webhook is subscribed to.
1809    pub event_types: Vec<WebhookEventType>,
1810}
1811
1812/// Request to register a new webhook.
1813#[derive(Debug, Clone)]
1814#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1815pub struct RegisterWebhookRequest {
1816    /// The URL that will receive webhook notifications.
1817    pub url: String,
1818    /// A secret used for HMAC-SHA256 signature verification of webhook payloads.
1819    pub secret: String,
1820    /// The event types to subscribe to.
1821    pub event_types: Vec<WebhookEventType>,
1822}
1823
1824/// Response from registering a webhook.
1825#[derive(Debug, Clone, Serialize)]
1826#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1827pub struct RegisterWebhookResponse {
1828    /// The unique identifier of the newly registered webhook.
1829    pub webhook_id: String,
1830}
1831
1832/// Request to unregister an existing webhook.
1833#[derive(Debug, Clone)]
1834#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1835pub struct UnregisterWebhookRequest {
1836    /// The unique identifier of the webhook to unregister.
1837    pub webhook_id: String,
1838}