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
1187/// Response from refunding pending conversions.
1188#[derive(Debug, Clone, Serialize, Default)]
1189#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1190pub struct RefundPendingConversionsResponse {
1191    /// Conversions successfully refunded this pass.
1192    pub refunded: u32,
1193    /// Conversions intentionally deferred (eligible but held back by a
1194    /// safety window). The next pass will retry them.
1195    pub skipped: u32,
1196    /// Conversions whose clawback did not complete this pass (rejected or
1197    /// errored; funds not returned). The next pass will retry them.
1198    pub failed: u32,
1199}
1200
1201impl std::fmt::Display for MaxFee {
1202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1203        match self {
1204            MaxFee::Fixed { amount } => write!(f, "Fixed: {amount}"),
1205            MaxFee::Rate { sat_per_vbyte } => write!(f, "Rate: {sat_per_vbyte}"),
1206            MaxFee::NetworkRecommended {
1207                leeway_sat_per_vbyte,
1208            } => write!(f, "NetworkRecommended: {leeway_sat_per_vbyte}"),
1209        }
1210    }
1211}
1212
1213#[derive(Debug, Clone)]
1214#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1215pub struct Credentials {
1216    pub username: String,
1217    pub password: String,
1218}
1219
1220/// Request to get the balance of the wallet
1221#[derive(Debug, Clone)]
1222#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1223pub struct GetInfoRequest {
1224    /// When `Some(true)`, and `background_tasks_enabled` is `true`, the call
1225    /// waits for the initial Full sync to complete before returning.
1226    ///
1227    /// When `background_tasks_enabled` is `false`, setting this to `Some(true)`
1228    /// is rejected with an invalid-input error. There is no background sync to
1229    /// wait on; call `sync_wallet` explicitly first if you need fresh state.
1230    pub ensure_synced: Option<bool>,
1231}
1232
1233/// Response containing the balance of the wallet
1234#[derive(Debug, Clone, Serialize)]
1235#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1236pub struct GetInfoResponse {
1237    /// The identity public key of the wallet as a hex string
1238    pub identity_pubkey: String,
1239    /// The balance in satoshis
1240    pub balance_sats: u64,
1241    /// The balances of the tokens in the wallet keyed by the token identifier
1242    pub token_balances: HashMap<String, TokenBalance>,
1243}
1244
1245#[derive(Debug, Clone, Serialize, Deserialize)]
1246#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1247pub struct TokenBalance {
1248    pub balance: u128,
1249    pub token_metadata: TokenMetadata,
1250}
1251
1252#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1253#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1254pub struct TokenMetadata {
1255    pub identifier: String,
1256    /// Hex representation of the issuer public key
1257    pub issuer_public_key: String,
1258    pub name: String,
1259    pub ticker: String,
1260    /// Number of decimals the token uses
1261    pub decimals: u32,
1262    pub max_supply: u128,
1263    pub is_freezable: bool,
1264}
1265
1266/// Request to sync the wallet with the Spark network
1267#[derive(Debug, Clone)]
1268#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1269pub struct SyncWalletRequest {}
1270
1271/// Response from synchronizing the wallet
1272#[derive(Debug, Clone, Serialize)]
1273#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1274pub struct SyncWalletResponse {}
1275
1276#[derive(Debug, Clone, Serialize)]
1277#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1278pub enum ReceivePaymentMethod {
1279    SparkAddress,
1280    SparkInvoice {
1281        /// Amount to receive. Denominated in sats if token identifier is empty, otherwise in the token base units
1282        amount: Option<u128>,
1283        /// The presence of this field indicates that the payment is for a token
1284        /// If empty, it is a Bitcoin payment
1285        token_identifier: Option<String>,
1286        /// The expiry time of the invoice as a unix timestamp in seconds
1287        expiry_time: Option<u64>,
1288        /// A description to embed in the invoice.
1289        description: Option<String>,
1290        /// If set, the invoice may only be fulfilled by a payer with this public key
1291        sender_public_key: Option<String>,
1292    },
1293    BitcoinAddress {
1294        /// If true, rotate to a new deposit address. Previous ones remain valid.
1295        /// If false or absent, return the existing address (creating one if none
1296        /// exists yet).
1297        new_address: Option<bool>,
1298    },
1299    Bolt11Invoice {
1300        description: String,
1301        amount_sats: Option<u64>,
1302        /// The expiry of the invoice as a duration in seconds
1303        expiry_secs: Option<u32>,
1304        /// If set, creates a HODL invoice with this payment hash (hex-encoded).
1305        /// The payer's HTLC will be held until the preimage is provided via
1306        /// `claim_htlc_payment` or the HTLC expires.
1307        payment_hash: Option<String>,
1308    },
1309}
1310
1311#[derive(Debug, Clone, Serialize)]
1312#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1313pub enum SendPaymentMethod {
1314    BitcoinAddress {
1315        address: BitcoinAddressDetails,
1316        fee_quote: SendOnchainFeeQuote,
1317    },
1318    Bolt11Invoice {
1319        invoice_details: Bolt11InvoiceDetails,
1320        spark_transfer_fee_sats: Option<u64>,
1321        lightning_fee_sats: u64,
1322    }, // should be replaced with the parsed invoice
1323    SparkAddress {
1324        address: String,
1325        /// Fee to pay for the transaction
1326        /// Denominated in sats if token identifier is empty, otherwise in the token base units
1327        fee: u128,
1328        /// The presence of this field indicates that the payment is for a token
1329        /// If empty, it is a Bitcoin payment
1330        token_identifier: Option<String>,
1331    },
1332    SparkInvoice {
1333        spark_invoice_details: SparkInvoiceDetails,
1334        /// Fee to pay for the transaction
1335        /// Denominated in sats if token identifier is empty, otherwise in the token base units
1336        fee: u128,
1337        /// The presence of this field indicates that the payment is for a token
1338        /// If empty, it is a Bitcoin payment
1339        token_identifier: Option<String>,
1340    },
1341    /// A cross-chain send via a bridge/swap provider.
1342    CrossChainAddress {
1343        /// The route selected for this cross-chain send (includes provider, chain, asset).
1344        route: CrossChainRoutePair,
1345        /// Raw destination address (e.g. `0xabc...`).
1346        recipient_address: String,
1347        /// Amount routed to the provider, in the route's source-asset units
1348        /// (Boltz invoice sats; Orchestra deposit sats/token). On the
1349        /// token-conversion path (both `FeesIncluded` and `FeesExcluded`)
1350        /// the dispatcher overrides this with the wallet-side token debit
1351        /// when the source token and destination asset form a USD-stable pair.
1352        amount_in: u128,
1353        /// `amount_in` expressed in the cross-chain (destination) asset's
1354        /// base units, via the same rate the SDK used at prepare time.
1355        asset_amount_in: u128,
1356        /// Estimated recipient amount in cross-chain asset base units.
1357        estimated_out: u128,
1358        /// Prepare-time total user-visible fee in cross-chain asset base units.
1359        /// Covers provider spread + bridge/gas + DEX slippage. On the
1360        /// token-conversion path it also rolls in the LN routing budget; on
1361        /// the direct path that budget lives separately in
1362        /// `source_transfer_fee_sats`.
1363        fee_amount: u128,
1364        /// Provider's own service fee/spread in its native denomination.
1365        service_fee_amount: u128,
1366        /// Asset which service fee is denominated in. Unset means BTC sats.
1367        service_fee_asset: Option<String>,
1368        /// Sats budget for moving the amount in from the wallet to the provider.
1369        source_transfer_fee_sats: u64,
1370        /// Fee mode the prepare ran under; the send stage matches.
1371        fee_mode: CrossChainFeeMode,
1372        /// ISO8601 timestamp after which the quote is no longer valid.
1373        expires_at: String,
1374        /// Provider-internal state, produced when preparing and consumed
1375        /// when sending.
1376        provider_context: CrossChainProviderContext,
1377    },
1378}
1379
1380#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1381#[derive(Debug, Clone, Serialize, Deserialize)]
1382pub struct SendOnchainFeeQuote {
1383    pub id: String,
1384    pub expires_at: u64,
1385    pub speed_fast: SendOnchainSpeedFeeQuote,
1386    pub speed_medium: SendOnchainSpeedFeeQuote,
1387    pub speed_slow: SendOnchainSpeedFeeQuote,
1388}
1389
1390#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1391#[derive(Debug, Clone, Serialize, Deserialize)]
1392pub struct SendOnchainSpeedFeeQuote {
1393    pub user_fee_sat: u64,
1394    pub l1_broadcast_fee_sat: u64,
1395}
1396
1397impl SendOnchainSpeedFeeQuote {
1398    pub fn total_fee_sat(&self) -> u64 {
1399        self.user_fee_sat.saturating_add(self.l1_broadcast_fee_sat)
1400    }
1401}
1402
1403#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1404pub struct ReceivePaymentRequest {
1405    pub payment_method: ReceivePaymentMethod,
1406}
1407
1408#[derive(Debug, Clone, Serialize)]
1409#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1410pub struct ReceivePaymentResponse {
1411    pub payment_request: String,
1412    /// Fee to pay to receive the payment
1413    /// Denominated in sats or token base units
1414    pub fee: u128,
1415}
1416
1417#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1418pub struct PrepareLnurlPayRequest {
1419    /// The amount to send. Denominated in satoshis, or in token base units
1420    /// when `token_identifier` is set.
1421    pub amount: u128,
1422    pub pay_request: LnurlPayRequestDetails,
1423    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1424    pub comment: Option<String>,
1425    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1426    pub validate_success_action_url: Option<bool>,
1427    /// The token identifier when sending a token amount with conversion.
1428    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1429    pub token_identifier: Option<String>,
1430    /// If provided, the payment will include a token conversion step before sending the payment
1431    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1432    pub conversion_options: Option<ConversionOptions>,
1433    /// How fees are handled. See [`FeePolicy`]. Defaults to `FeesExcluded`.
1434    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1435    pub fee_policy: Option<FeePolicy>,
1436}
1437
1438#[derive(Debug, Clone)]
1439#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1440pub struct PrepareLnurlPayResponse {
1441    /// The amount for the payment, always denominated in sats, even when a
1442    /// `token_identifier` and conversion are present.
1443    /// When a conversion is present, the token input amount is available in
1444    /// `conversion_estimate.amount_in`.
1445    pub amount_sats: u64,
1446    pub comment: Option<String>,
1447    pub pay_request: LnurlPayRequestDetails,
1448    /// The fee in satoshis. For `FeesIncluded` operations, this represents the total fee
1449    /// (including potential overpayment).
1450    pub fee_sats: u64,
1451    pub invoice_details: Bolt11InvoiceDetails,
1452    pub success_action: Option<SuccessAction>,
1453    /// When set, the payment will include a token conversion step before sending the payment
1454    pub conversion_estimate: Option<ConversionEstimate>,
1455    /// The fee policy actually applied. May differ from the request — e.g.,
1456    /// LNURL sends with `token_identifier` set + conversion are always
1457    /// `FeesIncluded` (explicit `FeesExcluded` is rejected).
1458    pub fee_policy: FeePolicy,
1459}
1460
1461#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1462pub struct LnurlPayRequest {
1463    pub prepare_response: PrepareLnurlPayResponse,
1464    /// If set, providing the same idempotency key for multiple requests will ensure that only one
1465    /// payment is made. If an idempotency key is re-used, the same payment will be returned.
1466    /// The idempotency key must be a valid UUID.
1467    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1468    pub idempotency_key: Option<String>,
1469}
1470
1471#[derive(Debug, Serialize)]
1472#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1473pub struct LnurlPayResponse {
1474    pub payment: Payment,
1475    pub success_action: Option<SuccessActionProcessed>,
1476}
1477
1478#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1479pub struct BuildUnsignedLnurlPayPackageRequest {
1480    pub prepare_response: PrepareLnurlPayResponse,
1481}
1482
1483#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1484pub struct PublishSignedLnurlPayPackageRequest {
1485    pub signed_package: SignedTransferPackage,
1486}
1487
1488#[allow(clippy::large_enum_variant)]
1489#[derive(Debug)]
1490#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1491pub enum PublishSignedLnurlPayResponse {
1492    SwapCompleted,
1493    PaymentSent { response: LnurlPayResponse },
1494}
1495
1496#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1497pub struct LnurlWithdrawRequest {
1498    /// The amount to withdraw in satoshis
1499    /// Must be within the min and max withdrawable limits
1500    pub amount_sats: u64,
1501    pub withdraw_request: LnurlWithdrawRequestDetails,
1502    /// If set, the function will return the payment if it is still pending after this
1503    /// number of seconds. If unset, the function will return immediately after
1504    /// initiating the LNURL withdraw.
1505    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1506    pub completion_timeout_secs: Option<u32>,
1507}
1508
1509#[derive(Debug, Serialize)]
1510#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1511pub struct LnurlWithdrawResponse {
1512    /// The Lightning invoice generated for the LNURL withdraw
1513    pub payment_request: String,
1514    pub payment: Option<Payment>,
1515}
1516
1517/// Represents the payment LNURL info
1518#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
1519#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1520pub struct LnurlPayInfo {
1521    pub ln_address: Option<String>,
1522    pub comment: Option<String>,
1523    pub domain: Option<String>,
1524    pub metadata: Option<String>,
1525    pub processed_success_action: Option<SuccessActionProcessed>,
1526    pub raw_success_action: Option<SuccessAction>,
1527}
1528
1529/// Represents the withdraw LNURL info
1530#[derive(Clone, Debug, Deserialize, Serialize)]
1531#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1532pub struct LnurlWithdrawInfo {
1533    pub withdraw_url: String,
1534}
1535
1536impl LnurlPayInfo {
1537    pub fn extract_description(&self) -> Option<String> {
1538        let Some(metadata) = &self.metadata else {
1539            return None;
1540        };
1541
1542        let Ok(metadata) = serde_json::from_str::<Vec<Vec<Value>>>(metadata) else {
1543            return None;
1544        };
1545
1546        for arr in metadata {
1547            if arr.len() != 2 {
1548                continue;
1549            }
1550            if let (Some(key), Some(value)) = (arr[0].as_str(), arr[1].as_str())
1551                && key == "text/plain"
1552            {
1553                return Some(value.to_string());
1554            }
1555        }
1556
1557        None
1558    }
1559}
1560
1561/// Specifies how fees are handled in a payment.
1562///
1563/// "Fees" are the wallet's sender-paid fees (Lightning routing, on-chain,
1564/// Spark transfer). They do not include provider spreads or destination-chain
1565/// costs on cross-chain routes; those are reported separately via
1566/// `estimated_out` on the prepare response and are not deterministic.
1567/// `FeePolicy` only controls the wallet's spend accounting.
1568#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1569#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1570pub enum FeePolicy {
1571    /// Fees are added on top of `amount`. Wallet's total spend is
1572    /// `amount + fees`. For direct sat sends, the recipient receives exactly
1573    /// `amount`. Default.
1574    #[default]
1575    FeesExcluded,
1576    /// Fees are deducted from `amount`. Wallet's total spend is `amount`.
1577    /// Use this to drain a balance — pass `amount = balance` and the wallet
1578    /// spends exactly that.
1579    FeesIncluded,
1580}
1581
1582#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1583#[derive(Debug, Clone, Serialize, Deserialize)]
1584pub enum OnchainConfirmationSpeed {
1585    Fast,
1586    Medium,
1587    Slow,
1588}
1589
1590/// The payment destination. Either a raw string (bolt11, spark address, BIP-21,
1591/// cross-chain URI, etc.) that is parsed internally, or a structured
1592/// cross-chain destination with explicit chain + asset selection.
1593#[derive(Debug, Clone, Serialize)]
1594#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1595pub enum PaymentRequest {
1596    /// Unparsed user input string (bolt11, spark address, BIP-21, cross-chain URI, etc.)
1597    Input { input: String },
1598    /// Cross-chain send with a selected route from `get_cross_chain_routes()`.
1599    /// Amount comes from `PrepareSendPaymentRequest.amount`, not here.
1600    CrossChain {
1601        address: String,
1602        route: CrossChainRoutePair,
1603        /// Maximum slippage tolerance in basis points (1/100 of a percent)
1604        /// for the cross-chain quote. Must be in `10..=500`. Falls back to
1605        /// [`Config::default_slippage_bps`] when `None`, which itself
1606        /// defaults to 100 bps (1%) when unset.
1607        max_slippage_bps: Option<u32>,
1608        /// Target-overpay pad in basis points applied on `FeesExcluded`
1609        /// conversion sends. Inflates the destination target before quoting
1610        /// so the recipient lands at or above the user's requested amount
1611        /// despite provider slippage. Must be in `0..=500`. Falls back to
1612        /// [`CrossChainConfig::default_target_overpay_bps`] when `None`,
1613        /// which itself defaults to 15 bps.
1614        target_overpay_bps: Option<u32>,
1615    },
1616}
1617
1618#[allow(clippy::large_enum_variant)]
1619#[derive(Debug, Clone, Serialize, Deserialize)]
1620#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1621pub enum UnsignedTransferPackage {
1622    Swap {
1623        prepare_transfer: crate::signer::ExternalPrepareTransferRequest,
1624        target_amounts: Vec<u64>,
1625        amount_sat: u64,
1626        fee_sat: u64,
1627    },
1628    Transfer {
1629        prepare_transfer: crate::signer::ExternalPrepareTransferRequest,
1630        amount_sat: u64,
1631        fee_sat: u64,
1632        target: TransferTarget,
1633    },
1634    Token {
1635        prepare_token_transaction: crate::signer::ExternalPrepareTokenTransactionRequest,
1636        token_context: Vec<u8>,
1637        token_identifier: String,
1638        amount: u128,
1639        fee: u128,
1640        /// When set, this package re-shapes the wallet's token outputs instead of
1641        /// sending a payment. Publishing it returns `SwapCompleted`: rebuild the
1642        /// original send from the same prepare response and submit again.
1643        is_swap: bool,
1644    },
1645}
1646
1647#[derive(Debug, Clone, Serialize, Deserialize)]
1648#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1649pub enum TransferTarget {
1650    Spark {
1651        address: String,
1652        spark_invoice: Option<String>,
1653    },
1654    Lightning {
1655        bolt11: String,
1656        lnurl_pay: Option<LnurlPayContext>,
1657        fee_policy: FeePolicy,
1658        completion_timeout_secs: Option<u32>,
1659    },
1660    CoopExit {
1661        address: String,
1662        fee_quote: SendOnchainFeeQuote,
1663        confirmation_speed: OnchainConfirmationSpeed,
1664    },
1665}
1666
1667#[derive(Debug, Clone, Serialize, Deserialize)]
1668#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1669pub struct LnurlPayContext {
1670    pub pay_request: LnurlPayRequestDetails,
1671    pub comment: Option<String>,
1672    pub success_action: Option<SuccessAction>,
1673}
1674
1675#[derive(Debug, Clone, Serialize, Deserialize)]
1676#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1677pub struct SignedTransferPackage {
1678    pub unsigned: UnsignedTransferPackage,
1679    pub signature: TransferSignature,
1680}
1681
1682#[derive(Debug, Clone, Serialize, Deserialize)]
1683#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1684pub enum TransferSignature {
1685    Transfer {
1686        signed: crate::signer::ExternalPreparedTransfer,
1687    },
1688    Token {
1689        signed: crate::signer::ExternalPreparedTokenTransaction,
1690    },
1691}
1692
1693#[derive(Debug, Clone)]
1694#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1695pub enum BuildTransferPackageOptions {
1696    BitcoinAddress {
1697        confirmation_speed: OnchainConfirmationSpeed,
1698    },
1699    Bolt11Invoice {
1700        prefer_spark: bool,
1701
1702        /// If set, publishing the package waits up to this many seconds for the
1703        /// payment to complete before returning it while still pending. If unset,
1704        /// publishing returns immediately after initiating the payment.
1705        completion_timeout_secs: Option<u32>,
1706    },
1707}
1708
1709#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1710pub struct BuildUnsignedTransferPackageRequest {
1711    pub prepare_response: PrepareSendPaymentResponse,
1712    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1713    pub options: Option<BuildTransferPackageOptions>,
1714}
1715
1716#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1717pub struct PrepareSendPaymentRequest {
1718    pub payment_request: PaymentRequest,
1719    /// The amount to send.
1720    /// Optional for payment requests with embedded amounts (e.g., Spark/Bolt11 invoices with amounts).
1721    /// Required for Spark addresses, Bitcoin addresses, and amountless invoices.
1722    /// Denominated in satoshis for Bitcoin payments, or token base units for token payments.
1723    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1724    pub amount: Option<u128>,
1725    /// Optional token identifier for token payments.
1726    /// Absence indicates that the payment is a Bitcoin payment.
1727    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1728    pub token_identifier: Option<String>,
1729    /// If provided, the payment will include a conversion step before sending the payment
1730    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1731    pub conversion_options: Option<ConversionOptions>,
1732    /// How fees are handled. See [`FeePolicy`]. Defaults to `FeesExcluded`.
1733    ///
1734    /// Ignored on cross-chain AMM-conversion sends (whether the conversion was
1735    /// explicitly requested or auto-injected by stable balance) — fees come
1736    /// out of the converted sats. Bolt11 and Bitcoin AMM-conversion sends
1737    /// still respect this field by sizing the conversion to cover fees. The
1738    /// prepare response's `fee_policy` reflects what was actually applied.
1739    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1740    pub fee_policy: Option<FeePolicy>,
1741}
1742
1743#[derive(Debug, Clone, Serialize)]
1744#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1745pub struct PrepareSendPaymentResponse {
1746    pub payment_method: SendPaymentMethod,
1747    /// The amount to be sent, denominated in satoshis for Bitcoin payments
1748    /// (including token-to-Bitcoin conversions), or token base units for token payments.
1749    /// When a conversion is present, the input amount is in
1750    /// `conversion_estimate.amount_in`.
1751    pub amount: u128,
1752    /// Optional token identifier for token payments.
1753    /// Absence indicates that the payment is a Bitcoin payment.
1754    pub token_identifier: Option<String>,
1755    /// When set, the payment will include a conversion step before sending the payment
1756    pub conversion_estimate: Option<ConversionEstimate>,
1757    /// The fee policy actually applied. May differ from the request — e.g.,
1758    /// cross-chain AMM-conversion sends are always `FeesIncluded`.
1759    pub fee_policy: FeePolicy,
1760}
1761
1762#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1763pub enum SendPaymentOptions {
1764    BitcoinAddress {
1765        /// Confirmation speed for the on-chain transaction.
1766        confirmation_speed: OnchainConfirmationSpeed,
1767    },
1768    Bolt11Invoice {
1769        prefer_spark: bool,
1770
1771        /// If set, the function will return the payment if it is still pending after this
1772        /// number of seconds. If unset, the function will return immediately after initiating the payment.
1773        completion_timeout_secs: Option<u32>,
1774    },
1775    SparkAddress {
1776        /// Can only be provided for Bitcoin payments. If set, a Spark HTLC transfer will be created.
1777        /// The receiver will need to provide the preimage to claim it.
1778        htlc_options: Option<SparkHtlcOptions>,
1779    },
1780}
1781
1782#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1783pub struct SparkHtlcOptions {
1784    /// The payment hash of the HTLC. The receiver will need to provide the associated preimage to claim it.
1785    pub payment_hash: String,
1786    /// The duration of the HTLC in seconds.
1787    /// After this time, the HTLC will be returned.
1788    pub expiry_duration_secs: u64,
1789}
1790
1791#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1792pub struct SendPaymentRequest {
1793    pub prepare_response: PrepareSendPaymentResponse,
1794    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1795    pub options: Option<SendPaymentOptions>,
1796    /// The optional idempotency key for all Spark based transfers (excludes token payments
1797    /// and cross-chain sends).
1798    /// If set, providing the same idempotency key for multiple requests will ensure that only one
1799    /// payment is made. If an idempotency key is re-used, the same payment will be returned.
1800    /// The idempotency key must be a valid UUID.
1801    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1802    pub idempotency_key: Option<String>,
1803}
1804
1805#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1806pub struct PublishSignedTransferPackageRequest {
1807    pub signed_package: SignedTransferPackage,
1808}
1809
1810#[allow(clippy::large_enum_variant)]
1811#[derive(Debug, Clone)]
1812#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1813pub enum PublishSignedTransferPackageResponse {
1814    SwapCompleted,
1815    PaymentSent { payment: Payment },
1816}
1817
1818#[derive(Debug, Clone, Serialize)]
1819#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1820pub struct SendPaymentResponse {
1821    pub payment: Payment,
1822}
1823
1824#[derive(Debug, Clone)]
1825#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1826pub enum PaymentDetailsFilter {
1827    Spark {
1828        /// Filter specific Spark HTLC statuses
1829        htlc_status: Option<Vec<SparkHtlcStatus>>,
1830        /// Filter conversion payments with refund information
1831        conversion_refund_needed: Option<bool>,
1832    },
1833    Token {
1834        /// Filter conversion payments with refund information
1835        conversion_refund_needed: Option<bool>,
1836        /// Filter by transaction hash
1837        tx_hash: Option<String>,
1838        /// Filter by transaction type
1839        tx_type: Option<TokenTransactionType>,
1840    },
1841    Lightning {
1842        /// Filter specific Spark HTLC statuses
1843        htlc_status: Option<Vec<SparkHtlcStatus>>,
1844    },
1845}
1846
1847/// Request to list payments with optional filters and pagination
1848#[derive(Debug, Clone, Default)]
1849#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1850pub struct ListPaymentsRequest {
1851    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1852    pub type_filter: Option<Vec<PaymentType>>,
1853    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1854    pub status_filter: Option<Vec<PaymentStatus>>,
1855    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1856    pub asset_filter: Option<AssetFilter>,
1857    /// Only include payments matching at least one of these payment details filters
1858    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1859    pub payment_details_filter: Option<Vec<PaymentDetailsFilter>>,
1860    /// Only include payments created after this timestamp (inclusive)
1861    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1862    pub from_timestamp: Option<u64>,
1863    /// Only include payments created before this timestamp (exclusive)
1864    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1865    pub to_timestamp: Option<u64>,
1866    /// Number of records to skip
1867    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1868    pub offset: Option<u32>,
1869    /// Maximum number of records to return
1870    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1871    pub limit: Option<u32>,
1872    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1873    pub sort_ascending: Option<bool>,
1874}
1875
1876/// A field of [`ListPaymentsRequest`] when listing payments filtered by asset
1877#[derive(Debug, Clone)]
1878#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1879pub enum AssetFilter {
1880    Bitcoin,
1881    Token {
1882        /// Optional token identifier to filter by
1883        token_identifier: Option<String>,
1884    },
1885}
1886
1887impl FromStr for AssetFilter {
1888    type Err = String;
1889
1890    fn from_str(s: &str) -> Result<Self, Self::Err> {
1891        Ok(match s.to_lowercase().as_str() {
1892            "bitcoin" => AssetFilter::Bitcoin,
1893            "token" => AssetFilter::Token {
1894                token_identifier: None,
1895            },
1896            str if str.starts_with("token:") => AssetFilter::Token {
1897                token_identifier: Some(
1898                    str.split_once(':')
1899                        .ok_or(format!("Invalid asset filter '{s}'"))?
1900                        .1
1901                        .to_string(),
1902                ),
1903            },
1904            _ => return Err(format!("Invalid asset filter '{s}'")),
1905        })
1906    }
1907}
1908
1909/// Response from listing payments
1910#[derive(Debug, Clone, Serialize)]
1911#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1912pub struct ListPaymentsResponse {
1913    /// The list of payments
1914    pub payments: Vec<Payment>,
1915}
1916
1917#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1918pub struct GetPaymentRequest {
1919    pub payment_id: String,
1920}
1921
1922#[derive(Debug, Clone, Serialize)]
1923#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1924pub struct GetPaymentResponse {
1925    pub payment: Payment,
1926}
1927
1928#[cfg_attr(feature = "uniffi", uniffi::export(callback_interface))]
1929pub trait Logger: Send + Sync {
1930    fn log(&self, l: LogEntry);
1931}
1932
1933#[derive(Debug, Clone, Serialize)]
1934#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1935pub struct LogEntry {
1936    pub line: String,
1937    pub level: String,
1938}
1939
1940#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1941#[derive(Debug, Clone, Serialize, Deserialize)]
1942pub struct CheckLightningAddressRequest {
1943    pub username: String,
1944}
1945
1946#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1947#[derive(Debug, Clone, Serialize, Deserialize)]
1948pub struct RegisterLightningAddressRequest {
1949    pub username: String,
1950    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1951    pub description: Option<String>,
1952}
1953
1954/// Authorization from the current owner granting a specific new owner the
1955/// right to take over a username. Produced by
1956/// [`BreezSdk::authorize_lightning_address_transfer`] and handed to the new
1957/// owner, who passes it to [`BreezSdk::claim_lightning_address_transfer`]. It
1958/// fully describes the transfer, so the new owner needs nothing else to claim.
1959#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1960#[derive(Debug, Clone, Serialize, Deserialize)]
1961pub struct TransferAuthorization {
1962    /// The username being handed over.
1963    pub username: String,
1964    /// The current owner's public key.
1965    pub pubkey: String,
1966    /// The current owner's signature authorizing the transfer.
1967    pub signature: String,
1968}
1969
1970/// Request for [`BreezSdk::authorize_lightning_address_transfer`]. Called by
1971/// the *current owner* to authorize handing their registered username over to
1972/// `transferee_pubkey`.
1973#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1974#[derive(Debug, Clone, Serialize, Deserialize)]
1975pub struct AuthorizeTransferRequest {
1976    /// The new owner's identity public key.
1977    pub transferee_pubkey: String,
1978}
1979
1980/// Request for [`BreezSdk::claim_lightning_address_transfer`]. Called by the
1981/// *new owner* to complete the takeover using the authorization produced by
1982/// the current owner.
1983#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1984#[derive(Debug, Clone, Serialize, Deserialize)]
1985pub struct ClaimTransferRequest {
1986    /// Authorization produced by the current owner via
1987    /// [`BreezSdk::authorize_lightning_address_transfer`].
1988    pub authorization: TransferAuthorization,
1989    /// Description for the address. Defaults to `"Pay to {username}@{domain}"`.
1990    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1991    pub description: Option<String>,
1992}
1993
1994#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1995#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
1996pub struct LnurlInfo {
1997    pub url: String,
1998    pub bech32: String,
1999}
2000
2001impl LnurlInfo {
2002    pub fn new(url: String) -> Self {
2003        let bech32 =
2004            breez_sdk_common::lnurl::encode_lnurl_to_bech32(&url).unwrap_or_else(|_| url.clone());
2005        Self { url, bech32 }
2006    }
2007}
2008
2009#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2010#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
2011pub struct LightningAddressInfo {
2012    pub description: String,
2013    pub lightning_address: String,
2014    pub lnurl: LnurlInfo,
2015    pub username: String,
2016}
2017
2018impl From<RecoverLnurlPayResponse> for LightningAddressInfo {
2019    fn from(resp: RecoverLnurlPayResponse) -> Self {
2020        Self {
2021            description: resp.description,
2022            lightning_address: resp.lightning_address,
2023            lnurl: LnurlInfo::new(resp.lnurl),
2024            username: resp.username,
2025        }
2026    }
2027}
2028
2029/// Response from listing fiat currencies
2030#[derive(Debug, Clone, Serialize)]
2031#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2032pub struct ListFiatCurrenciesResponse {
2033    /// The list of fiat currencies
2034    pub currencies: Vec<FiatCurrency>,
2035}
2036
2037/// Response from listing fiat rates
2038#[derive(Debug, Clone, Serialize)]
2039#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2040pub struct ListFiatRatesResponse {
2041    /// The list of fiat rates
2042    pub rates: Vec<Rate>,
2043}
2044
2045/// The operational status of a Spark service.
2046#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2047#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2048pub enum ServiceStatus {
2049    /// Service is fully operational.
2050    Operational,
2051    /// Service is experiencing degraded performance.
2052    Degraded,
2053    /// Service is partially unavailable.
2054    Partial,
2055    /// Service status is unknown.
2056    Unknown,
2057    /// Service is experiencing a major outage.
2058    Major,
2059}
2060
2061/// The status of the Spark network services relevant to the SDK.
2062#[derive(Debug, Clone, Serialize)]
2063#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2064pub struct SparkStatus {
2065    /// The worst status across all relevant services.
2066    pub status: ServiceStatus,
2067    /// The last time the status was updated, as a unix timestamp in seconds.
2068    pub last_updated: u64,
2069}
2070
2071pub(crate) enum WaitForPaymentIdentifier {
2072    PaymentId(String),
2073    LightningReceive { invoice: String, ssp_id: String },
2074}
2075
2076#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2077pub struct GetTokensMetadataRequest {
2078    pub token_identifiers: Vec<String>,
2079}
2080
2081#[derive(Debug, Clone, Serialize)]
2082#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2083pub struct GetTokensMetadataResponse {
2084    pub tokens_metadata: Vec<TokenMetadata>,
2085}
2086
2087#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2088pub struct SignMessageRequest {
2089    pub message: String,
2090    /// If true, the signature will be encoded in compact format instead of DER format
2091    pub compact: bool,
2092}
2093
2094#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2095pub struct SignMessageResponse {
2096    pub pubkey: String,
2097    /// The DER or compact hex encoded signature
2098    pub signature: String,
2099}
2100
2101#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2102pub struct CheckMessageRequest {
2103    /// The message that was signed
2104    pub message: String,
2105    /// The public key that signed the message
2106    pub pubkey: String,
2107    /// The DER or compact hex encoded signature
2108    pub signature: String,
2109}
2110
2111#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2112pub struct CheckMessageResponse {
2113    pub is_valid: bool,
2114}
2115
2116#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2117#[derive(Debug, Clone, Serialize)]
2118pub struct UserSettings {
2119    pub spark_private_mode_enabled: bool,
2120
2121    /// The label of the currently active stable balance token, or `None` if deactivated.
2122    pub stable_balance_active_label: Option<String>,
2123
2124    /// The hex encoded public key designated as this wallet's master identity
2125    /// key, or `None` if none is designated.
2126    pub spark_master_identity_public_key: Option<String>,
2127}
2128
2129/// Specifies how to update the wallet's Spark master identity public key.
2130#[derive(Debug, Clone)]
2131#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2132pub enum SparkMasterIdentityPublicKey {
2133    /// Designate the holder of this public key as the wallet's master
2134    /// identity, replacing any previously designated key. Must be hex encoded
2135    /// in the 33-byte compressed form.
2136    Set { public_key: String },
2137    /// Remove the designated master identity, leaving the owner as the only
2138    /// party able to read the wallet under private mode.
2139    Unset,
2140}
2141
2142#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2143pub struct UpdateUserSettingsRequest {
2144    pub spark_private_mode_enabled: Option<bool>,
2145
2146    /// Update the active stable balance token. `None` means no change.
2147    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
2148    pub stable_balance_active_label: Option<StableBalanceActiveLabel>,
2149
2150    /// Designate or remove the wallet's master identity, a second public key
2151    /// the Spark operators accept as a reader of this wallet's balance and
2152    /// history while `spark_private_mode_enabled` is set. The master identity
2153    /// can only read: payments still require the owner's keys. `None` means no
2154    /// change.
2155    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
2156    pub spark_master_identity_public_key: Option<SparkMasterIdentityPublicKey>,
2157}
2158
2159#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2160pub struct ClaimHtlcPaymentRequest {
2161    pub preimage: String,
2162}
2163
2164#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2165pub struct ClaimHtlcPaymentResponse {
2166    pub payment: Payment,
2167}
2168
2169#[derive(Debug, Clone, Deserialize, Serialize)]
2170#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2171pub struct LnurlReceiveMetadata {
2172    pub nostr_zap_request: Option<String>,
2173    pub nostr_zap_receipt: Option<String>,
2174    pub sender_comment: Option<String>,
2175}
2176
2177/// Mode of a manually-triggered optimization run.
2178#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
2179#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2180pub enum OptimizationMode {
2181    /// Run until no further optimization is productive.
2182    #[default]
2183    Full,
2184    /// Execute a single round and return so the caller can drive progress.
2185    SingleRound,
2186}
2187
2188/// Request for [`BreezSdk::optimize_leaves`]. Defaults to
2189/// [`OptimizationMode::Full`].
2190#[derive(Debug, Clone, Default, Deserialize, Serialize)]
2191#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2192pub struct OptimizeLeavesRequest {
2193    /// Controls how much work the call performs before returning.
2194    pub mode: OptimizationMode,
2195}
2196
2197/// Response from a [`BreezSdk::optimize_leaves`] call.
2198#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
2199#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2200pub struct OptimizeLeavesResponse {
2201    /// The outcome of the optimization run.
2202    pub outcome: OptimizationOutcome,
2203}
2204
2205/// Outcome of a [`BreezSdk::optimize_leaves`] call.
2206///
2207/// `rounds_executed` on `Completed` refers to rounds run by *this call*.
2208/// The SDK holds no cross-call state — callers driving a `SingleRound`
2209/// loop maintain their own cumulative counter if they need one.
2210///
2211/// A `Completed { rounds_executed: 0 }` outcome means the wallet was
2212/// already optimal at call time (no swap was needed).
2213///
2214/// **`SingleRound` loop pattern**: terminate on anything that isn't
2215/// `InProgress`. `Completed` covers both the final swap of a productive
2216/// run and the "already optimal" no-op case (the latter as
2217/// `rounds_executed: 0`).
2218///
2219/// ```ignore
2220/// loop {
2221///     let request = OptimizeLeavesRequest { mode: OptimizationMode::SingleRound };
2222///     match sdk.optimize_leaves(request).await?.outcome {
2223///         OptimizationOutcome::InProgress => continue,
2224///         OptimizationOutcome::Completed { .. } => break,
2225///     }
2226/// }
2227/// ```
2228#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
2229#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2230pub enum OptimizationOutcome {
2231    /// All planned optimization work was executed in this call.
2232    /// Returned by `Full` runs on success, and by `SingleRound` runs
2233    /// whose swap was the final one needed (the planner produced a
2234    /// single-swap plan with a convergence guarantee).
2235    /// `rounds_executed == 0` means the wallet was already optimal —
2236    /// no work was performed.
2237    Completed { rounds_executed: u32 },
2238    /// `SingleRound` only: a round ran but the planner could not
2239    /// guarantee it was the last. The caller should invoke
2240    /// `optimize_leaves` again.
2241    InProgress,
2242}
2243
2244/// A contact entry containing a name and payment identifier.
2245#[derive(Debug, Clone, Serialize, Deserialize)]
2246#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2247pub struct Contact {
2248    pub id: String,
2249    pub name: String,
2250    /// A Lightning address (user@domain).
2251    pub payment_identifier: String,
2252    pub created_at: u64,
2253    pub updated_at: u64,
2254}
2255
2256/// Request to add a new contact.
2257#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2258pub struct AddContactRequest {
2259    pub name: String,
2260    /// A Lightning address (user@domain).
2261    pub payment_identifier: String,
2262}
2263
2264/// Request to update an existing contact.
2265#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2266pub struct UpdateContactRequest {
2267    pub id: String,
2268    pub name: String,
2269    /// A Lightning address (user@domain).
2270    pub payment_identifier: String,
2271}
2272
2273/// Request to list contacts with optional pagination.
2274#[derive(Debug, Clone, Default)]
2275#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2276pub struct ListContactsRequest {
2277    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2278    pub offset: Option<u32>,
2279    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2280    pub limit: Option<u32>,
2281}
2282
2283/// The type of event that triggers a webhook notification.
2284#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2285#[allow(clippy::enum_variant_names)]
2286#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2287pub enum WebhookEventType {
2288    /// Triggered when a Lightning receive operation completes.
2289    LightningReceiveFinished,
2290    /// Triggered when a Lightning send operation completes.
2291    LightningSendFinished,
2292    /// Triggered when a cooperative exit completes.
2293    CoopExitFinished,
2294    /// Triggered when a static deposit completes.
2295    StaticDepositFinished,
2296    /// An event type not yet recognized by this version of the SDK.
2297    Unknown(String),
2298}
2299
2300/// A registered webhook entry.
2301#[derive(Debug, Clone, Serialize)]
2302#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2303pub struct Webhook {
2304    /// Unique identifier for this webhook.
2305    pub id: String,
2306    /// The URL that receives webhook notifications.
2307    pub url: String,
2308    /// The event types this webhook is subscribed to.
2309    pub event_types: Vec<WebhookEventType>,
2310}
2311
2312/// Request to register a new webhook.
2313#[derive(Debug, Clone)]
2314#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2315pub struct RegisterWebhookRequest {
2316    /// The URL that will receive webhook notifications.
2317    pub url: String,
2318    /// A secret used for HMAC-SHA256 signature verification of webhook payloads.
2319    pub secret: String,
2320    /// The event types to subscribe to.
2321    pub event_types: Vec<WebhookEventType>,
2322}
2323
2324/// Response from registering a webhook.
2325#[derive(Debug, Clone, Serialize)]
2326#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2327pub struct RegisterWebhookResponse {
2328    /// The unique identifier of the newly registered webhook.
2329    pub webhook_id: String,
2330}
2331
2332/// Request to unregister an existing webhook.
2333#[derive(Debug, Clone)]
2334#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2335pub struct UnregisterWebhookRequest {
2336    /// The unique identifier of the webhook to unregister.
2337    pub webhook_id: String,
2338}
2339
2340// ===========================================================================
2341// Unilateral exit
2342// ===========================================================================
2343
2344/// A funding UTXO that pays the on-chain fees of a unilateral exit.
2345#[derive(Debug, Clone, Serialize, Deserialize)]
2346#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2347pub enum CpfpInput {
2348    /// A P2WPKH (native segwit v0) UTXO controlled by `pubkey` (33-byte
2349    /// compressed, hex).
2350    P2wpkh {
2351        txid: String,
2352        vout: u32,
2353        value: u64,
2354        pubkey: String,
2355    },
2356    /// A P2TR (taproot, key-path) UTXO. `pubkey` (x-only or compressed, hex) is
2357    /// the **internal** (untweaked, BIP86 key-path) public key whose secret signs
2358    /// the input, not the tweaked on-chain output key. The SDK applies the BIP86
2359    /// taproot tweak itself to derive the funding scriptPubKey, so passing the
2360    /// already-tweaked output key here produces a scriptPubKey that does not match
2361    /// the UTXO and the built transaction is rejected at broadcast.
2362    P2tr {
2363        txid: String,
2364        vout: u32,
2365        value: u64,
2366        pubkey: String,
2367    },
2368    /// Any witness-program script, signed via a custom `CpfpSigner`. Legacy
2369    /// (non-SegWit) scripts are rejected. `signed_input_weight` (weight units)
2370    /// is an upper bound on the input's signed weight, so the fee stays exact,
2371    /// or slightly conservative if the real signature is shorter.
2372    Custom {
2373        txid: String,
2374        vout: u32,
2375        value: u64,
2376        script_pubkey_hex: String,
2377        signed_input_weight: u64,
2378    },
2379}
2380
2381/// The kind of UTXO that will fund an exit's fees.
2382#[derive(Debug, Clone, Serialize, Deserialize)]
2383#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2384pub enum CpfpFundingKind {
2385    /// Fees paid from P2WPKH (native segwit v0) UTXOs.
2386    P2wpkh,
2387    /// Fees paid from P2TR (taproot, key-path) UTXOs.
2388    P2tr,
2389    /// Fees paid from a custom witness-program script (legacy scripts are
2390    /// rejected). `script_pubkey_hex` (the funding scriptPubKey) sizes the
2391    /// fan-out output and dust; `signed_input_weight` (weight units) is an upper
2392    /// bound on the input's signed weight, so the quote stays exact or slightly
2393    /// conservative.
2394    Custom {
2395        script_pubkey_hex: String,
2396        signed_input_weight: u64,
2397    },
2398}
2399
2400/// Which leaves to exit.
2401#[derive(Debug, Clone, Serialize, Deserialize)]
2402#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2403pub enum ExitLeafSelection {
2404    /// Exit every leaf whose value exceeds its own marginal exit cost (its tree
2405    /// and refund CPFP fees plus its sweep input). This per-leaf test does not
2406    /// include the shared fan-out fee, so funding many leaves from a single UTXO
2407    /// adds `fanout_fee_sat` on top: compare `recoverable_value_sat` with
2408    /// `total_fee_sat`, or fund one UTXO per branch to avoid the fan-out. Leaves
2409    /// that fail the per-leaf test are skipped.
2410    Auto,
2411    /// Exit exactly these leaves, regardless of profitability.
2412    Specific { leaf_ids: Vec<String> },
2413}
2414
2415/// The role of a transaction in the exit path.
2416#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2417#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2418pub enum UnilateralExitTxKind {
2419    /// Splits the caller's funding into one output per branch. Present only
2420    /// when the funding couldn't be matched one-to-one to branches.
2421    FanOut,
2422    /// A tree node transaction (root, intermediate, or leaf node).
2423    Node,
2424    /// A leaf's refund transaction.
2425    Refund,
2426    /// The final transaction sweeping all refund outputs to the destination.
2427    Sweep,
2428}
2429
2430/// Whether a transaction in the exit path is already on-chain.
2431#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2432#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2433pub enum ConfirmationStatus {
2434    /// This transaction is confirmed in a block. It needs no action.
2435    Confirmed,
2436    /// This transaction is not yet confirmed. Mempool state is not consulted.
2437    Unconfirmed,
2438    /// The on-chain status could not be determined (the chain service errored).
2439    /// Broadcasting may fail if a conflicting transaction already landed.
2440    Unverified,
2441}
2442
2443/// One transaction in the unilateral exit path, with everything needed to
2444/// order and broadcast it.
2445#[derive(Debug, Clone, Serialize, Deserialize)]
2446#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2447pub struct UnilateralExitTransaction {
2448    pub kind: UnilateralExitTxKind,
2449    /// The tree node this transaction belongs to. Unset for the fan-out and the
2450    /// sweep.
2451    pub node_id: Option<String>,
2452    pub txid: String,
2453    pub tx_hex: String,
2454    /// The signed CPFP child to broadcast alongside `tx_hex` as a package.
2455    /// Unset for the fan-out and the sweep (no anchor to bump) and for a
2456    /// `Confirmed` step (its CPFP is already on-chain).
2457    pub cpfp_tx_hex: Option<String>,
2458    /// Relative CSV timelock, in blocks, that must mature on the spent input
2459    /// before this transaction can confirm. Unset when there is no timelock.
2460    pub csv_timelock_blocks: Option<u32>,
2461    /// Txids of other entries in this list that must be confirmed before this
2462    /// one can be broadcast.
2463    pub depends_on: Vec<String>,
2464    pub status: ConfirmationStatus,
2465}
2466
2467/// A leaf selected for exit, with its value.
2468#[derive(Debug, Clone, Serialize, Deserialize)]
2469#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2470pub struct UnilateralExitLeaf {
2471    pub leaf_id: String,
2472    /// The leaf's value in satoshis.
2473    pub value: u64,
2474}
2475
2476/// Request for `prepare_unilateral_exit`, the exit quote.
2477#[derive(Debug, Clone)]
2478#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2479pub struct PrepareUnilateralExitRequest {
2480    /// Target fee rate in sat/vByte, applied to every CPFP child, the fan-out,
2481    /// and the sweep.
2482    pub fee_rate_sat_per_vbyte: u64,
2483    pub funding_kind: CpfpFundingKind,
2484    /// The Bitcoin address the swept funds are sent to.
2485    pub destination: String,
2486    pub selection: ExitLeafSelection,
2487}
2488
2489/// How much to fund one branch of the exit to avoid a fan-out.
2490#[derive(Debug, Clone, Serialize, Deserialize)]
2491#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2492pub struct PerBranchFunding {
2493    /// The leaf whose branch this funds.
2494    pub leaf_id: String,
2495    /// Fund a UTXO of at least this many satoshis for this branch.
2496    pub funding_sat: u64,
2497}
2498
2499/// Response from `prepare_unilateral_exit`: which leaves would exit, the exact
2500/// fee at the requested rate, and how much to fund.
2501#[derive(Debug, Clone, Serialize, Deserialize)]
2502#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2503pub struct PrepareUnilateralExitResponse {
2504    pub leaves: Vec<UnilateralExitLeaf>,
2505    /// Total value of the selected leaves, in satoshis.
2506    pub recoverable_value_sat: u64,
2507    /// Total on-chain fee when funding with a single UTXO (fanned out across
2508    /// branches), in satoshis. Exact for the given funding kind; nodes the
2509    /// operators report on-chain are assumed already paid, so a partially-exited
2510    /// tree quotes a lower fee than a fresh one.
2511    pub total_fee_sat: u64,
2512    /// The part of `total_fee_sat` paid for the fan-out transaction. Funding one
2513    /// UTXO per branch (`per_branch_funding`) avoids it. Zero for a single
2514    /// branch (no fan-out).
2515    pub fanout_fee_sat: u64,
2516    /// Fund a single UTXO of at least this many satoshis to exit with a fan-out.
2517    pub single_utxo_funding_sat: u64,
2518    /// To skip the fan-out, fund one UTXO per branch of at least the given
2519    /// amount (one entry per selected leaf).
2520    pub per_branch_funding: Vec<PerBranchFunding>,
2521    /// The fee rate this quote was computed at, in sat/vByte.
2522    pub fee_rate_sat_per_vbyte: u64,
2523    pub destination: String,
2524}
2525
2526/// Request for `unilateral_exit`: a `prepare_unilateral_exit` quote plus the
2527/// funding UTXOs that pay its fees. The signer is passed separately (it is not a
2528/// plain data value).
2529#[derive(Debug, Clone)]
2530#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2531pub struct UnilateralExitRequest {
2532    /// The quote returned by `prepare_unilateral_exit`, naming the leaves to exit.
2533    pub prepared: PrepareUnilateralExitResponse,
2534    /// The funding UTXOs that pay the exit's on-chain fees, meeting the quote's
2535    /// `single_utxo_funding_sat` (one UTXO) or `per_branch_funding` (one per branch).
2536    pub funding_inputs: Vec<CpfpInput>,
2537}
2538
2539/// Result of `unilateral_exit`: a cost summary plus the complete, signed exit
2540/// path.
2541#[derive(Debug, Clone, Serialize)]
2542#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2543pub struct UnilateralExitResponse {
2544    /// Total value of the selected leaves, in satoshis.
2545    pub recoverable_value_sat: u64,
2546    /// The actual total on-chain fee the returned transactions pay at the
2547    /// requested rate, in satoshis. A resumed or partially-confirmed exit pays
2548    /// less because already-confirmed steps are not rebuilt.
2549    pub total_fee_sat: u64,
2550    pub leaves: Vec<UnilateralExitLeaf>,
2551    /// The full signed transaction set, in valid topological (broadcast) order
2552    /// with shared ancestors appearing once and the sweep last.
2553    pub transactions: Vec<UnilateralExitTransaction>,
2554}