Skip to main content

breez_sdk_spark/models/
mod.rs

1pub(crate) mod adaptors;
2pub mod payment_observer;
3pub use payment_observer::*;
4pub mod token_allowance;
5pub use token_allowance::*;
6
7// Re-export public conversion types from the conversion module
8pub use crate::token_conversion::{
9    AmountAdjustmentReason, ConversionEstimate, ConversionInfo, ConversionOptions,
10    ConversionPurpose, ConversionStatus, ConversionType, FetchConversionLimitsRequest,
11    FetchConversionLimitsResponse, SwapDegradation,
12};
13
14use core::fmt;
15use lnurl_models::RecoverLnurlPayResponse;
16use serde::{Deserialize, Serialize};
17use serde_json::Value;
18use spark_wallet::bech32m_decode_token_id;
19use std::{
20    collections::{HashMap, HashSet},
21    fmt::Display,
22    str::FromStr,
23};
24
25use crate::{
26    BitcoinAddressDetails, BitcoinNetwork, Bolt11InvoiceDetails, ExternalInputParser, FiatCurrency,
27    LnurlPayRequestDetails, LnurlWithdrawRequestDetails, Rate, SdkError, SparkInvoiceDetails,
28    SuccessAction, SuccessActionProcessed,
29    chain::FeeMarket,
30    cross_chain::{CrossChainFeeMode, CrossChainProviderContext, CrossChainRoutePair},
31    error::{CooperativeRecoveryError, DepositClaimError},
32};
33
34/// A list of external input parsers that are used by default.
35/// To opt-out, set `use_default_external_input_parsers` in [Config] to false.
36pub const DEFAULT_EXTERNAL_INPUT_PARSERS: &[(&str, &str, &str)] = &[
37    (
38        "picknpay",
39        "(.*)(za.co.electrum.picknpay)(.*)",
40        "https://cryptoqr.net/.well-known/lnurlp/<input>",
41    ),
42    (
43        "bootleggers",
44        r"(.*)(wigroup\.co|yoyogroup\.co)(.*)",
45        "https://cryptoqr.net/.well-known/lnurlw/<input>",
46    ),
47];
48
49/// Represents the seed for wallet generation, either as a mnemonic phrase with an optional
50/// passphrase or as raw entropy bytes.
51#[derive(Clone)]
52#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
53pub enum Seed {
54    /// A BIP-39 mnemonic phrase with an optional passphrase.
55    Mnemonic {
56        /// The mnemonic phrase. 12 or 24 words.
57        mnemonic: String,
58        /// An optional passphrase for the mnemonic.
59        passphrase: Option<String>,
60    },
61    /// Raw entropy bytes.
62    Entropy(Vec<u8>),
63}
64
65impl fmt::Debug for Seed {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        match self {
68            Seed::Mnemonic { passphrase, .. } => f
69                .debug_struct("Mnemonic")
70                .field("mnemonic", &"<redacted>")
71                .field("passphrase", &passphrase.as_ref().map(|_| "<redacted>"))
72                .finish(),
73            Seed::Entropy(_) => f.debug_tuple("Entropy").field(&"<redacted>").finish(),
74        }
75    }
76}
77
78impl Seed {
79    pub fn to_bytes(&self) -> Result<Vec<u8>, SdkError> {
80        match self {
81            Seed::Mnemonic {
82                mnemonic,
83                passphrase,
84            } => {
85                let mnemonic = bip39::Mnemonic::parse(mnemonic)
86                    .map_err(|e| SdkError::Generic(e.to_string()))?;
87
88                Ok(mnemonic
89                    .to_seed(passphrase.as_deref().unwrap_or(""))
90                    .to_vec())
91            }
92            Seed::Entropy(entropy) => Ok(entropy.clone()),
93        }
94    }
95}
96
97#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
98pub struct ConnectRequest {
99    pub config: Config,
100    pub seed: Seed,
101    pub storage_dir: String,
102}
103
104/// Request object for connecting to the Spark network using an external signer.
105///
106/// This allows using a custom signer implementation instead of providing a seed directly.
107#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
108#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
109pub struct ConnectWithSignerRequest {
110    pub config: Config,
111    /// External signer for non-Spark SDK signing (LNURL-auth, sync, message
112    /// signing, ECIES).
113    pub breez_signer: std::sync::Arc<dyn crate::signer::ExternalBreezSigner>,
114    /// External high-level Spark signer for the Spark wallet flows.
115    pub spark_signer: std::sync::Arc<dyn crate::signer::ExternalSparkSigner>,
116    pub storage_dir: String,
117}
118
119/// Request object for connecting to the Spark network using a signing-only
120/// external signer.
121///
122/// Use this instead of [`ConnectWithSignerRequest`] for a signer that can't
123/// perform the SDK's local ECIES/HMAC operations (for example a
124/// policy-restricted enclave). The SDK keeps session tokens in plaintext and
125/// disables the features that rely on ECIES/HMAC.
126#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
127#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
128pub struct ConnectWithSigningOnlySignerRequest {
129    pub config: Config,
130    /// Signing-only external signer for non-Spark SDK signing.
131    pub breez_signer: std::sync::Arc<dyn crate::signer::ExternalSigningSigner>,
132    /// External high-level Spark signer for the Spark wallet flows.
133    pub spark_signer: std::sync::Arc<dyn crate::signer::ExternalSparkSigner>,
134    pub storage_dir: String,
135}
136
137/// The type of payment
138#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
139#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
140pub enum PaymentType {
141    /// Payment sent from this wallet
142    Send,
143    /// Payment received to this wallet
144    Receive,
145}
146
147impl fmt::Display for PaymentType {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        match self {
150            PaymentType::Send => write!(f, "send"),
151            PaymentType::Receive => write!(f, "receive"),
152        }
153    }
154}
155
156impl FromStr for PaymentType {
157    type Err = String;
158
159    fn from_str(s: &str) -> Result<Self, Self::Err> {
160        Ok(match s.to_lowercase().as_str() {
161            "receive" => PaymentType::Receive,
162            "send" => PaymentType::Send,
163            _ => return Err(format!("invalid payment type '{s}'")),
164        })
165    }
166}
167
168/// The status of a payment
169#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
170#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
171pub enum PaymentStatus {
172    /// Payment is completed successfully
173    Completed,
174    /// Payment is in progress
175    Pending,
176    /// Payment has failed
177    Failed,
178}
179
180impl PaymentStatus {
181    /// Returns true if the payment status is final (either Completed or Failed)
182    pub fn is_final(&self) -> bool {
183        matches!(self, PaymentStatus::Completed | PaymentStatus::Failed)
184    }
185}
186
187impl fmt::Display for PaymentStatus {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        match self {
190            PaymentStatus::Completed => write!(f, "completed"),
191            PaymentStatus::Pending => write!(f, "pending"),
192            PaymentStatus::Failed => write!(f, "failed"),
193        }
194    }
195}
196
197impl FromStr for PaymentStatus {
198    type Err = String;
199
200    fn from_str(s: &str) -> Result<Self, Self::Err> {
201        Ok(match s.to_lowercase().as_str() {
202            "completed" => PaymentStatus::Completed,
203            "pending" => PaymentStatus::Pending,
204            "failed" => PaymentStatus::Failed,
205            _ => return Err(format!("Invalid payment status '{s}'")),
206        })
207    }
208}
209
210#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
211#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
212pub enum PaymentMethod {
213    Lightning,
214    Spark,
215    Token,
216    Deposit,
217    Withdraw,
218    Unknown,
219}
220
221impl Display for PaymentMethod {
222    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223        match self {
224            PaymentMethod::Lightning => write!(f, "lightning"),
225            PaymentMethod::Spark => write!(f, "spark"),
226            PaymentMethod::Token => write!(f, "token"),
227            PaymentMethod::Deposit => write!(f, "deposit"),
228            PaymentMethod::Withdraw => write!(f, "withdraw"),
229            PaymentMethod::Unknown => write!(f, "unknown"),
230        }
231    }
232}
233
234impl FromStr for PaymentMethod {
235    type Err = ();
236
237    fn from_str(s: &str) -> Result<Self, Self::Err> {
238        match s {
239            "lightning" => Ok(PaymentMethod::Lightning),
240            "spark" => Ok(PaymentMethod::Spark),
241            "token" => Ok(PaymentMethod::Token),
242            "deposit" => Ok(PaymentMethod::Deposit),
243            "withdraw" => Ok(PaymentMethod::Withdraw),
244            "unknown" => Ok(PaymentMethod::Unknown),
245            _ => Err(()),
246        }
247    }
248}
249
250/// Represents a payment (sent or received)
251#[derive(Debug, Clone, Serialize, Deserialize)]
252#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
253pub struct Payment {
254    /// Unique identifier for the payment
255    pub id: String,
256    /// Type of payment (send or receive)
257    pub payment_type: PaymentType,
258    /// Status of the payment
259    pub status: PaymentStatus,
260    /// Amount in satoshis or token base units
261    pub amount: u128,
262    /// Fee paid in satoshis or token base units
263    pub fees: u128,
264    /// Timestamp of when the payment was created
265    pub timestamp: u64,
266    /// Method of payment. Sometimes the payment details is empty so this field
267    /// is used to determine the payment method.
268    pub method: PaymentMethod,
269    /// Details of the payment
270    pub details: Option<PaymentDetails>,
271    /// If set, this payment involved a conversion before the payment
272    pub conversion_details: Option<ConversionDetails>,
273}
274
275impl Payment {
276    /// Returns `true` if this payment is a child of a conversion operation.
277    ///
278    /// An AMM conversion (stable balance, convert-on-send) settles as its own
279    /// payments: sats out to the pool, tokens back in. Those legs are internal
280    /// plumbing, so they carry [`ConversionInfo::Amm`]. A cross-chain
281    /// conversion has no such legs: it annotates the payment the user made or
282    /// received, which is never a child.
283    pub fn is_conversion_child(&self) -> bool {
284        matches!(
285            &self.details,
286            Some(
287                PaymentDetails::Spark {
288                    conversion_info: Some(ConversionInfo::Amm { .. }),
289                    ..
290                } | PaymentDetails::Token {
291                    conversion_info: Some(ConversionInfo::Amm { .. }),
292                    ..
293                }
294            )
295        )
296    }
297}
298
299/// Outlines the steps involved in one or more conversions on a payment.
300///
301/// Built progressively: `status` is available immediately from payment metadata,
302/// while `conversions` are enriched later from child payments and conversion info.
303#[derive(Debug, Clone, Serialize, Deserialize)]
304#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
305pub struct ConversionDetails {
306    /// Overall status of the conversion. On a send, it follows the cross-chain
307    /// step until that step finishes.
308    pub status: ConversionStatus,
309    /// Ordered list of conversion steps. For sends: [AMM, cross-chain].
310    /// For receives: [cross-chain, AMM]. Rebuilt on retrieval, not persisted.
311    #[serde(default)]
312    pub conversions: Vec<Conversion>,
313}
314
315/// The provider that performed a conversion.
316#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
317#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
318pub enum ConversionProvider {
319    /// AMM (Flashnet pool) conversion between token and BTC on Spark
320    Amm,
321    /// Orchestra cross-chain conversion
322    Orchestra,
323    /// Boltz reverse-swap cross-chain conversion
324    Boltz,
325}
326
327/// The chain or network that a [`ConversionSide`] lives on.
328#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
329#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
330pub enum ConversionChain {
331    /// Spark layer-2 network.
332    Spark,
333    /// Bitcoin Lightning Network.
334    Lightning,
335    /// An external chain reached via a cross-chain provider.
336    External {
337        /// Human-readable chain name (e.g. `"base"`, `"solana"`, `"arbitrum"`).
338        name: String,
339        /// Stable chain identifier (e.g. EVM `chainId` as a decimal string,
340        /// or a chain-native identifier). `None` when the provider does not
341        /// expose one for this route.
342        chain_id: Option<String>,
343    },
344}
345
346/// The asset on a [`ConversionSide`] — groups the ticker, stable identifier,
347/// and decimals that always travel together.
348#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
349#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
350pub struct ConversionAsset {
351    /// Ticker (e.g. `"BTC"`, `"USDB"`, `"USDC"`, `"USDT"`). Tickers alone
352    /// are ambiguous across chains — pair with [`Self::identifier`] for a
353    /// hard match.
354    pub ticker: String,
355    /// Stable identifier: a Spark token identifier for Spark tokens, or a
356    /// contract/mint address for cross-chain assets. `None` for BTC/sats.
357    pub identifier: Option<String>,
358    /// Number of decimals for the asset.
359    /// `0` for BTC/sats sides (amount is already in the smallest unit,
360    /// so no scaling is needed); non-zero for token assets (e.g. `6` for
361    /// USDC/USDT/USDB).
362    pub decimals: u32,
363}
364
365/// One side (source or destination) of a conversion.
366#[derive(Debug, Clone, Serialize, Deserialize)]
367#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
368pub struct ConversionSide {
369    /// The chain or network for this side.
370    pub chain: ConversionChain,
371    /// The asset being converted on this side.
372    pub asset: ConversionAsset,
373    /// Amount in base units (satoshis or token base units)
374    pub amount: u128,
375    /// Fee in the same base units
376    pub fee: u128,
377}
378
379/// A single conversion in a payment's conversion chain.
380#[derive(Debug, Clone, Serialize, Deserialize)]
381#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
382pub struct Conversion {
383    /// The provider that performed this conversion
384    pub provider: ConversionProvider,
385    /// Status of this specific conversion step
386    pub status: ConversionStatus,
387    /// Source side of the conversion
388    pub from: ConversionSide,
389    /// Destination side of the conversion
390    pub to: ConversionSide,
391    /// Reason the conversion amount was adjusted, if applicable (AMM only)
392    #[serde(default)]
393    pub amount_adjustment: Option<AmountAdjustmentReason>,
394}
395
396#[cfg(feature = "uniffi")]
397uniffi::custom_type!(u128, String, {
398    remote,
399    try_lift: |val| val.parse::<u128>().map_err(uniffi::deps::anyhow::Error::msg),
400    lower: |obj| obj.to_string(),
401});
402
403// TODO: fix large enum variant lint - may be done by boxing lnurl_pay_info but that requires
404//  some changes to the wasm bindgen macro
405#[allow(clippy::large_enum_variant)]
406#[derive(Debug, Clone, Serialize, Deserialize)]
407#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
408pub enum PaymentDetails {
409    Spark {
410        /// The invoice details if the payment fulfilled a spark invoice
411        invoice_details: Option<SparkInvoicePaymentDetails>,
412        /// The HTLC transfer details if the payment fulfilled an HTLC transfer
413        htlc_details: Option<SparkHtlcDetails>,
414        /// The information for a conversion
415        conversion_info: Option<ConversionInfo>,
416    },
417    Token {
418        metadata: TokenMetadata,
419        tx_hash: String,
420        tx_type: TokenTransactionType,
421        /// The invoice details if the payment fulfilled a spark invoice
422        invoice_details: Option<SparkInvoicePaymentDetails>,
423        /// The information for a conversion
424        conversion_info: Option<ConversionInfo>,
425    },
426    Lightning {
427        /// Represents the invoice description
428        description: Option<String>,
429        /// Represents the Bolt11/Bolt12 invoice associated with a payment
430        /// In the case of a Send payment, this is the invoice paid by the user
431        /// In the case of a Receive payment, this is the invoice paid to the user
432        invoice: String,
433
434        /// The invoice destination/payee pubkey
435        destination_pubkey: String,
436
437        /// The HTLC transfer details
438        htlc_details: SparkHtlcDetails,
439
440        /// Lnurl payment information if this was an lnurl payment.
441        lnurl_pay_info: Option<LnurlPayInfo>,
442
443        /// Lnurl withdrawal information if this was an lnurl payment.
444        lnurl_withdraw_info: Option<LnurlWithdrawInfo>,
445
446        /// Lnurl receive information if this was a received lnurl payment.
447        lnurl_receive_metadata: Option<LnurlReceiveMetadata>,
448
449        /// The information for a conversion — populated when this Lightning
450        /// payment is the source leg of a cross-chain conversion (e.g. a
451        /// Boltz reverse swap paying a hold invoice).
452        conversion_info: Option<ConversionInfo>,
453    },
454    Withdraw {
455        tx_id: String,
456        /// The index of the withdrawal transaction's output that pays the
457        /// destination address.
458        vout: u32,
459        destination_address: String,
460    },
461    Deposit {
462        tx_id: String,
463        vout: u32,
464    },
465}
466
467#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
468#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
469pub enum TokenTransactionType {
470    Transfer,
471    Mint,
472    Burn,
473}
474
475impl fmt::Display for TokenTransactionType {
476    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
477        match self {
478            TokenTransactionType::Transfer => write!(f, "transfer"),
479            TokenTransactionType::Mint => write!(f, "mint"),
480            TokenTransactionType::Burn => write!(f, "burn"),
481        }
482    }
483}
484
485impl FromStr for TokenTransactionType {
486    type Err = String;
487
488    fn from_str(s: &str) -> Result<Self, Self::Err> {
489        match s.to_lowercase().as_str() {
490            "transfer" => Ok(TokenTransactionType::Transfer),
491            "mint" => Ok(TokenTransactionType::Mint),
492            "burn" => Ok(TokenTransactionType::Burn),
493            _ => Err(format!("Invalid token transaction type '{s}'")),
494        }
495    }
496}
497
498#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
499#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
500pub struct SparkInvoicePaymentDetails {
501    /// Represents the spark invoice description
502    pub description: Option<String>,
503    /// The raw spark invoice string
504    pub invoice: String,
505}
506
507#[derive(Clone, Serialize, Deserialize, PartialEq)]
508#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
509pub struct SparkHtlcDetails {
510    /// The payment hash of the HTLC
511    pub payment_hash: String,
512    /// The preimage of the HTLC. Empty until receiver has released it.
513    pub preimage: Option<String>,
514    /// The expiry time of the HTLC as a unix timestamp in seconds
515    pub expiry_time: u64,
516    /// The HTLC status
517    pub status: SparkHtlcStatus,
518}
519
520impl fmt::Debug for SparkHtlcDetails {
521    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
522        f.debug_struct("SparkHtlcDetails")
523            .field("payment_hash", &self.payment_hash)
524            .field("preimage", &self.preimage.as_ref().map(|_| "<redacted>"))
525            .field("expiry_time", &self.expiry_time)
526            .field("status", &self.status)
527            .finish()
528    }
529}
530
531#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
532#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
533pub enum SparkHtlcStatus {
534    /// The HTLC is waiting for the preimage to be shared by the receiver
535    WaitingForPreimage,
536    /// The HTLC preimage has been shared and the transfer can be or has been claimed by the receiver
537    PreimageShared,
538    /// The HTLC has been returned to the sender due to expiry
539    Returned,
540}
541
542impl fmt::Display for SparkHtlcStatus {
543    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
544        match self {
545            SparkHtlcStatus::WaitingForPreimage => write!(f, "WaitingForPreimage"),
546            SparkHtlcStatus::PreimageShared => write!(f, "PreimageShared"),
547            SparkHtlcStatus::Returned => write!(f, "Returned"),
548        }
549    }
550}
551
552impl FromStr for SparkHtlcStatus {
553    type Err = String;
554
555    fn from_str(s: &str) -> Result<Self, Self::Err> {
556        match s {
557            "WaitingForPreimage" => Ok(SparkHtlcStatus::WaitingForPreimage),
558            "PreimageShared" => Ok(SparkHtlcStatus::PreimageShared),
559            "Returned" => Ok(SparkHtlcStatus::Returned),
560            _ => Err("Invalid Spark HTLC status".to_string()),
561        }
562    }
563}
564
565#[derive(Debug, Clone, Copy, PartialEq, Eq)]
566#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
567pub enum Network {
568    Mainnet,
569    Regtest,
570    Signet,
571}
572
573impl std::fmt::Display for Network {
574    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
575        match self {
576            Network::Mainnet => write!(f, "Mainnet"),
577            Network::Regtest => write!(f, "Regtest"),
578            Network::Signet => write!(f, "Signet"),
579        }
580    }
581}
582
583impl From<Network> for BitcoinNetwork {
584    fn from(network: Network) -> Self {
585        match network {
586            Network::Mainnet => BitcoinNetwork::Bitcoin,
587            Network::Regtest => BitcoinNetwork::Regtest,
588            Network::Signet => BitcoinNetwork::Signet,
589        }
590    }
591}
592
593impl From<Network> for breez_sdk_common::network::BitcoinNetwork {
594    fn from(network: Network) -> Self {
595        match network {
596            Network::Mainnet => breez_sdk_common::network::BitcoinNetwork::Bitcoin,
597            Network::Regtest => breez_sdk_common::network::BitcoinNetwork::Regtest,
598            Network::Signet => breez_sdk_common::network::BitcoinNetwork::Signet,
599        }
600    }
601}
602
603impl From<Network> for bitcoin::Network {
604    fn from(network: Network) -> Self {
605        match network {
606            Network::Mainnet => bitcoin::Network::Bitcoin,
607            Network::Regtest => bitcoin::Network::Regtest,
608            Network::Signet => bitcoin::Network::Signet,
609        }
610    }
611}
612
613impl FromStr for Network {
614    type Err = String;
615
616    fn from_str(s: &str) -> Result<Self, Self::Err> {
617        match s {
618            "mainnet" => Ok(Network::Mainnet),
619            "regtest" => Ok(Network::Regtest),
620            "signet" => Ok(Network::Signet),
621            _ => Err("Invalid network".to_string()),
622        }
623    }
624}
625
626/// A SOCKS5 proxy carrying the connections the SDK opens.
627///
628/// Hostnames are resolved by the proxy rather than locally, so a DNS query
629/// never discloses which host is being reached. A connection that cannot be
630/// established through the proxy fails: the SDK never falls back to a direct
631/// one.
632///
633/// Not supported on WASM, where the browser owns connection setup and exposes
634/// no proxy control. In Node, route the SDK by installing a proxy dispatcher
635/// on the global `fetch` instead.
636#[derive(Debug, Clone, PartialEq, Eq)]
637#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
638pub struct ProxyConfig {
639    /// Proxy host. An IP address, or a name resolvable locally: reaching the
640    /// proxy is the one lookup that cannot itself go through the proxy.
641    pub host: String,
642    pub port: u16,
643    /// Username for SOCKS5 username/password authentication. Authentication is
644    /// only offered when both this and `password` are set.
645    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
646    pub username: Option<String>,
647    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
648    pub password: Option<String>,
649}
650
651impl From<&ProxyConfig> for platform_utils::ProxyConfig {
652    fn from(config: &ProxyConfig) -> Self {
653        Self {
654            host: config.host.clone(),
655            port: config.port,
656            username: config.username.clone(),
657            password: config.password.clone(),
658        }
659    }
660}
661
662impl ProxyConfig {
663    /// A validated HTTP client honouring `proxy`, for the components built
664    /// outside the SDK's shared context. Everything inside it takes the
665    /// context's pooled client instead and never sees a proxy.
666    pub(crate) fn http_client(
667        proxy: Option<&Self>,
668        user_agent: Option<&str>,
669    ) -> Result<std::sync::Arc<dyn platform_utils::HttpClient>, SdkError> {
670        if let Some(proxy) = proxy {
671            proxy.validate()?;
672        }
673        platform_utils::create_http_client_with_proxy(
674            user_agent,
675            proxy.map(platform_utils::ProxyConfig::from).as_ref(),
676        )
677        .map_err(|e| SdkError::InvalidInput(format!("Failed to build proxied HTTP client: {e}")))
678    }
679
680    /// Rejects a proxy the SDK cannot honour end to end. Accepting one it can
681    /// only partly apply would leave some traffic going direct, which is worse
682    /// than refusing outright.
683    pub(crate) fn validate(&self) -> Result<(), SdkError> {
684        if cfg!(all(target_family = "wasm", target_os = "unknown")) {
685            return Err(SdkError::InvalidInput(
686                "A SOCKS5 proxy is not supported on WASM: the browser owns connection setup and \
687                 exposes no proxy control. In Node, install a proxy dispatcher on the global \
688                 fetch instead."
689                    .to_string(),
690            ));
691        }
692        if self.host.is_empty() {
693            return Err(SdkError::InvalidInput(
694                "Proxy host must not be empty".to_string(),
695            ));
696        }
697        if self.port == 0 {
698            return Err(SdkError::InvalidInput(
699                "Proxy port must not be 0".to_string(),
700            ));
701        }
702        if self.username.is_some() != self.password.is_some() {
703            return Err(SdkError::InvalidInput(
704                "Proxy username and password must be set together".to_string(),
705            ));
706        }
707        // The host ends up in a URL authority, so it has to survive being put
708        // there unchanged. Parsing is not enough on its own: a host carrying
709        // `@` parses, but as userinfo, which silently turns part of it into a
710        // username and moves the address. Checked here rather than at client
711        // build time so the error names the config that caused it.
712        let address = platform_utils::ProxyConfig::from(self).address();
713        let intact = url::Url::parse(&format!("socks5h://{address}")).is_ok_and(|url| {
714            url.username().is_empty()
715                && url.password().is_none()
716                && url.port() == Some(self.port)
717                && url.path().is_empty()
718                && url.query().is_none()
719                && url.fragment().is_none()
720        });
721        if !intact {
722            return Err(SdkError::InvalidInput(format!(
723                "Proxy host '{}' cannot be used in a proxy URL",
724                self.host
725            )));
726        }
727        Ok(())
728    }
729}
730
731#[derive(Debug, Clone)]
732#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
733#[allow(clippy::struct_excessive_bools)]
734pub struct Config {
735    pub api_key: Option<String>,
736    pub network: Network,
737    pub sync_interval_secs: u32,
738
739    /// The maximum fee that can be paid to claim an on-chain deposit. It also caps
740    /// the provider's spread for crediting a deposit before it matures, so raising
741    /// it is what allows deposits to be claimed early. Unset disables claiming
742    /// rather than allowing any fee.
743    pub max_deposit_claim_fee: Option<MaxFee>,
744
745    /// The domain used for receiving through lnurl-pay and lightning address.
746    pub lnurl_domain: Option<String>,
747
748    /// When this is set to `true` we will prefer to use spark payments over
749    /// lightning when sending and receiving. This has the benefit of lower fees
750    /// but is at the cost of privacy.
751    pub prefer_spark_over_lightning: bool,
752
753    /// Whether the data needed to exit a payment unilaterally, without the Spark
754    /// operators, is collected automatically as funds arrive. Collection runs in
755    /// the background, after an operation rather than during it. A leaf the
756    /// operators cannot complete stays un-exitable until a later attempt
757    /// succeeds.
758    ///
759    /// Turn it off when collecting behind every operation costs more than it is
760    /// worth, on a busy wallet holding many leaves. `sync_wallet` collects
761    /// regardless of this flag, and waits for the pass before returning, so an
762    /// explicit sync on a cadence of your choosing is how the data is kept
763    /// current with the automatic collection off.
764    ///
765    /// Only that automatic collection is governed, so this has no effect at all
766    /// where none runs: with `background_tasks_enabled` off there is no
767    /// background collector, and every sync is an explicit one.
768    ///
769    /// Default value is true.
770    pub exit_chain_auto_fetch_enabled: bool,
771
772    /// Whether the SDK pre-signs the transactions that protect funds on chain at
773    /// a range of fees, so the protection holds when on-chain fees are high.
774    ///
775    /// Unset turns it on. It is not supported with a remote signer, such as
776    /// Turnkey: unset keeps it off there, and true fails to connect.
777    pub watchtower_fee_ladder_enabled: Option<bool>,
778
779    /// A set of external input parsers that are used by [`BreezSdk::parse`](crate::sdk::BreezSdk::parse) when the input
780    /// is not recognized. See [`ExternalInputParser`] for more details on how to configure
781    /// external parsing.
782    pub external_input_parsers: Option<Vec<ExternalInputParser>>,
783    /// The SDK includes some default external input parsers
784    /// ([`DEFAULT_EXTERNAL_INPUT_PARSERS`]).
785    /// Set this to false in order to prevent their use.
786    pub use_default_external_input_parsers: bool,
787
788    /// Url to use for the real-time sync server. Defaults to the Breez real-time sync server.
789    pub real_time_sync_server_url: Option<String>,
790
791    /// Whether the Spark private mode is enabled by default.
792    ///
793    /// If set to true, the Spark private mode will be enabled on the first
794    /// initialization of the SDK. If set to false, no changes will be made
795    /// to the Spark private mode.
796    ///
797    /// This default is only auto-applied when `background_tasks_enabled` is
798    /// `true`. When `background_tasks_enabled` is `false`, the SDK does not
799    /// touch the Spark private mode on startup; call `update_user_settings`
800    /// with `spark_private_mode_enabled` set as needed on a one-time setup
801    /// pass.
802    pub private_enabled_default: bool,
803
804    /// Configuration for leaf optimization.
805    ///
806    /// Leaf optimization controls the denominations of leaves that are held in the wallet.
807    /// Fewer, bigger leaves allow for more funds to be exited unilaterally.
808    /// More leaves allow payments to be made without needing a swap, reducing payment latency.
809    pub leaf_optimization_config: LeafOptimizationConfig,
810
811    /// Configuration for token-output optimization.
812    ///
813    /// Token-output optimization controls automatic consolidation of a token's
814    /// available outputs. Keeping the output set small reduces transaction size,
815    /// while keeping enough distinct outputs preserves concurrency for parallel
816    /// sends.
817    pub token_optimization_config: TokenOptimizationConfig,
818
819    /// Configuration for automatic conversion of Bitcoin to stable tokens.
820    ///
821    /// When set, received sats will be automatically converted to the specified token
822    /// once the balance exceeds the threshold.
823    pub stable_balance_config: Option<StableBalanceConfig>,
824
825    /// Maximum number of concurrent transfer claims.
826    ///
827    /// Default is 4. Increase for server environments with high incoming payment volume.
828    pub max_concurrent_claims: u32,
829
830    /// Optional custom Spark environment configuration.
831    ///
832    /// When set, overrides the default Spark operator pool, service provider,
833    /// threshold, and token settings. Use this to connect to alternative Spark
834    /// deployments (e.g. dev/staging environments).
835    pub spark_config: Option<SparkConfig>,
836
837    /// Master switch for per-instance background services.
838    ///
839    /// When `true` (default), the SDK runs its standard background work:
840    /// periodic sync, lightning-address recovery, private-mode initialization,
841    /// the leaf and token-output optimizers, the Spark server-event
842    /// subscription, and the real-time sync client (when
843    /// `real_time_sync_server_url` is set).
844    ///
845    /// When `false`, **no background service is started**, regardless of any
846    /// other setting on this config. This is intended for multi-tenant server
847    /// deployments where the host application orchestrates sync and claims
848    /// explicitly and receives events via webhooks. Use
849    /// `default_server_config` to get this preset.
850    ///
851    /// Explicit operations (`sync_wallet`, `claim_deposit`,
852    /// `list_unclaimed_deposits`, `refund_deposit`,
853    /// `refund_pending_conversions`, leaf/token optimization, etc.) work
854    /// regardless of this flag.
855    ///
856    /// When `false`, the SDK rejects builds where fields whose backing
857    /// service is gated off are still in their active shape:
858    /// `stable_balance_config` must be `None`, `real_time_sync_server_url`
859    /// must be `None`, and `optimization_config.auto_enabled` must be `false`.
860    /// `default_server_config` already sets these compatible values.
861    pub background_tasks_enabled: bool,
862
863    /// Routes the connections the SDK opens through a SOCKS5 proxy.
864    ///
865    /// Covers HTTP and gRPC alike, and resolves hostnames at the proxy so no
866    /// DNS query leaks the destination. `None` (default) connects directly.
867    ///
868    /// When an [`SdkContext`](crate::SdkContext) is supplied to the builder,
869    /// its proxy must match this one: the context owns the shared clients, so
870    /// a disagreement would mean part of the traffic bypassed the proxy.
871    pub proxy: Option<ProxyConfig>,
872
873    /// Enables cross-chain payments: sending and receiving USDC/USDT on other
874    /// chains. Set it to a default `CrossChainConfig`, or one with your own
875    /// defaults, to turn the feature on. Unset (the default) leaves it off, and
876    /// the cross-chain calls fail with a `CrossChainDisabled` error.
877    ///
878    /// Off by default because the providers run background work such as web
879    /// sockets. Only supported on mainnet.
880    pub cross_chain_config: Option<CrossChainConfig>,
881}
882
883/// Configuration for cross-chain payments. Setting it on the config turns the
884/// feature on.
885#[derive(Debug, Clone, Default)]
886#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
887pub struct CrossChainConfig {
888    /// Default maximum slippage in basis points used when
889    /// [`PaymentRequest::CrossChain::max_slippage_bps`] is not set on the
890    /// prepare request. Must be in 10 to 500. Falls back to 100 bps (1%)
891    /// when this field is unset.
892    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
893    pub default_slippage_bps: Option<u32>,
894    /// Default target-overpay pad in basis points applied to the user's
895    /// destination amount on `FeesExcluded` conversion sends. Bumps the
896    /// target upward before quoting so the recipient lands at or above the
897    /// requested amount despite provider slippage. Must be in 0 to 500.
898    /// Falls back to 15 bps when unset.
899    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
900    pub default_target_overpay_bps: Option<u32>,
901}
902
903impl CrossChainConfig {
904    pub(crate) fn validate(&self) -> Result<(), SdkError> {
905        if let Some(bps) = self.default_slippage_bps
906            && !(crate::cross_chain::MIN_CROSS_CHAIN_SLIPPAGE_BPS
907                ..=crate::cross_chain::MAX_CROSS_CHAIN_SLIPPAGE_BPS)
908                .contains(&bps)
909        {
910            return Err(SdkError::InvalidInput(format!(
911                "Default cross-chain slippage must be between {} and {} basis points, but got {bps}.",
912                crate::cross_chain::MIN_CROSS_CHAIN_SLIPPAGE_BPS,
913                crate::cross_chain::MAX_CROSS_CHAIN_SLIPPAGE_BPS,
914            )));
915        }
916        if let Some(bps) = self.default_target_overpay_bps
917            && !(crate::cross_chain::MIN_TARGET_OVERPAY_BPS
918                ..=crate::cross_chain::MAX_TARGET_OVERPAY_BPS)
919                .contains(&bps)
920        {
921            return Err(SdkError::InvalidInput(format!(
922                "Default cross-chain target-overpay must be between {} and {} basis points, but got {bps}.",
923                crate::cross_chain::MIN_TARGET_OVERPAY_BPS,
924                crate::cross_chain::MAX_TARGET_OVERPAY_BPS,
925            )));
926        }
927        Ok(())
928    }
929}
930
931/// Configuration for leaf optimization.
932#[derive(Debug, Clone)]
933#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
934pub struct LeafOptimizationConfig {
935    /// Whether automatic leaf optimization is enabled.
936    ///
937    /// If set to true, the SDK will automatically optimize the leaf set when it changes.
938    /// Otherwise, the manual optimization API must be used to optimize the leaf set.
939    ///
940    /// Default value is true.
941    pub auto_enabled: bool,
942    /// The desired multiplicity for the leaf set.
943    ///
944    /// Setting this to 0 will optimize for maximizing unilateral exit.
945    /// Higher values will optimize for minimizing transfer swaps, with higher values
946    /// being more aggressive and allowing better TPS rates.
947    ///
948    /// For end-user wallets, values of 1-5 are recommended. Values above 5 are
949    /// intended for high-throughput server environments and are not recommended
950    /// for end-user wallets due to significantly higher unilateral exit costs.
951    ///
952    /// Default value is 1.
953    pub multiplicity: u8,
954}
955
956/// Configuration for token-output optimization.
957#[derive(Debug, Clone)]
958#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
959pub struct TokenOptimizationConfig {
960    /// Whether automatic token-output consolidation is enabled.
961    ///
962    /// If set to true, the SDK will periodically consolidate a token's outputs
963    /// once their count exceeds [`Self::min_outputs_threshold`]. Otherwise, no
964    /// automatic consolidation is performed.
965    ///
966    /// Default value is true.
967    pub auto_enabled: bool,
968    /// Number of token outputs to produce when token-output auto-consolidation
969    /// fires.
970    ///
971    /// Instead of collapsing a token's outputs into a single output (which
972    /// serializes subsequent payments), the SDK splits the consolidated balance
973    /// across this many outputs of roughly equal value. Higher values preserve
974    /// concurrency for parallel sends at the cost of a slightly larger output
975    /// set.
976    ///
977    /// Must be >= 1 and strictly less than [`Self::min_outputs_threshold`].
978    ///
979    /// Default value is 5.
980    pub target_output_count: u32,
981    /// Output count that triggers per-token auto-consolidation.
982    ///
983    /// Auto-consolidation triggers for a token when its available output count
984    /// strictly exceeds this threshold.
985    ///
986    /// Must be greater than 1.
987    ///
988    /// Default value is 50.
989    pub min_outputs_threshold: u32,
990}
991
992/// A stable token that can be used for automatic balance conversion.
993#[derive(Debug, Clone)]
994#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
995pub struct StableBalanceToken {
996    /// Integrator-defined display label for the token, e.g. "USD".
997    ///
998    /// This is a short, human-readable name set by the integrator for display purposes.
999    /// It is **not** a canonical Spark token ticker — it has no protocol-level meaning.
1000    /// Labels must be unique within the [`StableBalanceConfig::tokens`] list.
1001    pub label: String,
1002
1003    /// The full token identifier string used for conversions.
1004    pub token_identifier: String,
1005}
1006
1007/// Configuration for automatic conversion of Bitcoin to stable tokens.
1008///
1009/// When configured, the SDK automatically monitors the Bitcoin balance after each
1010/// wallet sync. Once the balance reaches the configured threshold, the SDK converts
1011/// the whole Bitcoin balance to the active stable token.
1012///
1013/// When the balance is held in a stable token, Bitcoin payments can still be sent.
1014/// The SDK automatically detects when there's not enough Bitcoin balance to cover a
1015/// payment and auto-populates the token-to-Bitcoin conversion options to facilitate
1016/// the payment.
1017///
1018/// The active token can be changed at runtime via [`UpdateUserSettingsRequest`].
1019#[derive(Debug, Clone)]
1020#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1021pub struct StableBalanceConfig {
1022    /// Available tokens that can be used for stable balance.
1023    pub tokens: Vec<StableBalanceToken>,
1024
1025    /// The label of the token to activate by default.
1026    ///
1027    /// If `None`, stable balance starts deactivated. The user can activate it
1028    /// at runtime via [`UpdateUserSettingsRequest`]. If a user setting is cached
1029    /// locally, it takes precedence over this default.
1030    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
1031    pub default_active_label: Option<String>,
1032
1033    /// The minimum sats balance that triggers auto-conversion.
1034    ///
1035    /// If not provided, uses the minimum from conversion limits.
1036    /// If provided but less than the conversion limit minimum, the limit minimum is used.
1037    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
1038    pub threshold_sats: Option<u64>,
1039
1040    /// Maximum slippage in basis points (1/100 of a percent).
1041    ///
1042    /// Defaults to 10 bps (0.1%) if not set.
1043    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
1044    pub max_slippage_bps: Option<u32>,
1045}
1046
1047/// Specifies how to update the active stable balance token.
1048#[derive(Debug, Clone)]
1049#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1050pub enum StableBalanceActiveLabel {
1051    /// Activate stable balance with the given label.
1052    Set { label: String },
1053    /// Deactivate stable balance.
1054    Unset,
1055}
1056
1057/// Configuration for a custom Spark environment.
1058///
1059/// When set on [`Config`], overrides the default Spark operator pool,
1060/// service provider, threshold, and token settings. This allows connecting
1061/// to alternative Spark deployments (e.g. dev/staging environments).
1062#[derive(Debug, Clone, Deserialize)]
1063#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1064pub struct SparkConfig {
1065    /// Hex-encoded identifier of the coordinator operator.
1066    pub coordinator_identifier: String,
1067    /// The FROST signing threshold (e.g. 2 of 3).
1068    pub threshold: u32,
1069    /// The set of signing operators.
1070    pub signing_operators: Vec<SparkSigningOperator>,
1071    /// Service provider (SSP) configuration.
1072    pub ssp_config: SparkSspConfig,
1073    /// Expected bond amount in sats for token withdrawals.
1074    pub expected_withdraw_bond_sats: u64,
1075    /// Expected relative block locktime for token withdrawals.
1076    pub expected_withdraw_relative_block_locktime: u64,
1077    /// Cap on the inputs a single token transaction may spend. A send needing
1078    /// more first consolidates the wallet's token outputs. Unset uses the SDK
1079    /// default (500).
1080    pub max_token_transaction_inputs: Option<u32>,
1081}
1082
1083impl From<&SparkConfig> for spark_wallet::SparkDeployment {
1084    fn from(config: &SparkConfig) -> Self {
1085        Self {
1086            coordinator_identifier: config.coordinator_identifier.clone(),
1087            threshold: config.threshold,
1088            signing_operators: config
1089                .signing_operators
1090                .iter()
1091                .map(|operator| spark_wallet::SparkDeploymentOperator {
1092                    id: operator.id,
1093                    identifier: operator.identifier.clone(),
1094                    address: operator.address.clone(),
1095                    identity_public_key: operator.identity_public_key.clone(),
1096                    ca_cert_pem: operator.ca_cert_pem.clone(),
1097                })
1098                .collect(),
1099            ssp_config: spark_wallet::SparkDeploymentSsp {
1100                base_url: config.ssp_config.base_url.clone(),
1101                identity_public_key: config.ssp_config.identity_public_key.clone(),
1102                schema_endpoint: config.ssp_config.schema_endpoint.clone(),
1103            },
1104            expected_withdraw_bond_sats: config.expected_withdraw_bond_sats,
1105            expected_withdraw_relative_block_locktime: config
1106                .expected_withdraw_relative_block_locktime,
1107            max_token_transaction_inputs: config.max_token_transaction_inputs,
1108        }
1109    }
1110}
1111
1112/// A Spark signing operator.
1113#[derive(Debug, Clone, Deserialize)]
1114#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1115pub struct SparkSigningOperator {
1116    /// Sequential operator ID (0-indexed).
1117    pub id: u32,
1118    /// Hex-encoded 32-byte FROST identifier.
1119    pub identifier: String,
1120    /// gRPC address of the operator (e.g. `https://0.spark.lightspark.com`).
1121    pub address: String,
1122    /// Hex-encoded compressed public key of the operator.
1123    pub identity_public_key: String,
1124    /// Optional PEM-encoded CA certificate for TLS verification.
1125    /// When set, the SDK uses this CA to verify the operator's TLS certificate
1126    /// instead of the system/default roots. Useful for local development with
1127    /// self-signed certificates.
1128    pub ca_cert_pem: Option<String>,
1129}
1130
1131/// Configuration for the Spark Service Provider (SSP).
1132#[derive(Debug, Clone, Deserialize)]
1133#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1134pub struct SparkSspConfig {
1135    /// Base URL of the SSP GraphQL API.
1136    pub base_url: String,
1137    /// Hex-encoded compressed public key of the SSP.
1138    pub identity_public_key: String,
1139    /// Optional GraphQL schema endpoint path (e.g. "graphql/spark/rc").
1140    /// Defaults to the hardcoded schema endpoint if not set.
1141    pub schema_endpoint: Option<String>,
1142}
1143
1144impl Config {
1145    /// Validates the configuration.
1146    ///
1147    /// Returns an error if any configuration values are invalid.
1148    pub fn validate(&self) -> Result<(), SdkError> {
1149        if self.network == Network::Signet && self.spark_config.is_none() {
1150            return Err(SdkError::InvalidInput(
1151                "Signet requires an explicit spark_config with signing operators and an SSP"
1152                    .to_string(),
1153            ));
1154        }
1155        if self.max_concurrent_claims == 0 {
1156            return Err(SdkError::InvalidInput(
1157                "max_concurrent_claims must be greater than 0".to_string(),
1158            ));
1159        }
1160
1161        if let Some(sb) = &self.stable_balance_config {
1162            if sb.tokens.is_empty() {
1163                return Err(SdkError::InvalidInput(
1164                    "tokens must not be empty".to_string(),
1165                ));
1166            }
1167
1168            let mut seen_labels = HashSet::new();
1169            let mut seen_identifiers = HashSet::new();
1170            for token in &sb.tokens {
1171                if token.label.is_empty() {
1172                    return Err(SdkError::InvalidInput(
1173                        "token label must not be empty".to_string(),
1174                    ));
1175                }
1176                if token.token_identifier.is_empty() {
1177                    return Err(SdkError::InvalidInput(
1178                        "token_identifier must not be empty".to_string(),
1179                    ));
1180                }
1181                if bech32m_decode_token_id(&token.token_identifier, Some(self.network.into()))
1182                    .is_err()
1183                {
1184                    return Err(SdkError::InvalidInput(format!(
1185                        "token_identifier must be a bech32m token identifier for this network: {}",
1186                        token.token_identifier
1187                    )));
1188                }
1189                if !seen_labels.insert(&token.label) {
1190                    return Err(SdkError::InvalidInput(format!(
1191                        "tokens contains duplicate label: {}",
1192                        token.label
1193                    )));
1194                }
1195                if !seen_identifiers.insert(&token.token_identifier) {
1196                    return Err(SdkError::InvalidInput(format!(
1197                        "tokens contains duplicate token_identifier: {}",
1198                        token.token_identifier
1199                    )));
1200                }
1201            }
1202
1203            if let Some(bps) = sb.max_slippage_bps
1204                && bps > 10000
1205            {
1206                return Err(SdkError::InvalidInput(
1207                    "max_slippage_bps must be <= 10000".to_string(),
1208                ));
1209            }
1210
1211            if let Some(default_label) = &sb.default_active_label
1212                && !seen_labels.contains(default_label)
1213            {
1214                return Err(SdkError::InvalidInput(format!(
1215                    "default_active_label '{default_label}' not found in tokens list"
1216                )));
1217            }
1218        }
1219
1220        self.validate_token_optimization()?;
1221        self.proxy.as_ref().map_or(Ok(()), ProxyConfig::validate)?;
1222
1223        if let Some(cc) = &self.cross_chain_config {
1224            if self.network != Network::Mainnet {
1225                return Err(SdkError::InvalidInput(format!(
1226                    "Cross-chain sends are only available on Mainnet, not on {}.",
1227                    self.network,
1228                )));
1229            }
1230            cc.validate()?;
1231        }
1232
1233        Ok(())
1234    }
1235
1236    fn validate_token_optimization(&self) -> Result<(), SdkError> {
1237        let token_opt = &self.token_optimization_config;
1238        if token_opt.min_outputs_threshold <= 1 {
1239            return Err(SdkError::InvalidInput(
1240                "token optimization minimum outputs threshold must be greater than 1".to_string(),
1241            ));
1242        }
1243        if token_opt.target_output_count < 1 {
1244            return Err(SdkError::InvalidInput(
1245                "token optimization target output count must be at least 1".to_string(),
1246            ));
1247        }
1248        if token_opt.target_output_count >= token_opt.min_outputs_threshold {
1249            return Err(SdkError::InvalidInput(
1250                "token optimization target output count must be less than the minimum outputs threshold".to_string(),
1251            ));
1252        }
1253        Ok(())
1254    }
1255
1256    pub(crate) fn get_all_external_input_parsers(&self) -> Vec<ExternalInputParser> {
1257        let mut external_input_parsers = Vec::new();
1258        if self.use_default_external_input_parsers {
1259            let default_parsers = DEFAULT_EXTERNAL_INPUT_PARSERS
1260                .iter()
1261                .map(|(id, regex, url)| ExternalInputParser {
1262                    provider_id: (*id).to_string(),
1263                    input_regex: (*regex).to_string(),
1264                    parser_url: (*url).to_string(),
1265                })
1266                .collect::<Vec<_>>();
1267            external_input_parsers.extend(default_parsers);
1268        }
1269        external_input_parsers.extend(self.external_input_parsers.clone().unwrap_or_default());
1270
1271        external_input_parsers
1272    }
1273}
1274
1275/// The most a deposit claim may cost: an on-chain part plus an optional share
1276/// of the deposit amount.
1277#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1278#[serde(from = "StoredMaxFee", into = "StoredMaxFee")]
1279#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1280pub struct MaxFee {
1281    pub onchain: MaxOnchainFee,
1282    /// A share of the deposit amount added on top of the on-chain part, in parts
1283    /// per million (100 ppm is 0.01%), to allow for a provider fee that grows
1284    /// with the deposit. It applies to every claim, early ones included. Unset
1285    /// adds nothing.
1286    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
1287    pub proportional_ppm: Option<u32>,
1288}
1289
1290/// The on-chain part of a [`MaxFee`].
1291#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1292#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1293pub enum MaxOnchainFee {
1294    /// A fixed amount in sats.
1295    Fixed { amount: u64 },
1296    /// A fee rate in sats per vbyte.
1297    Rate { sat_per_vbyte: u64 },
1298    /// The fastest recommended fee rate at the time of the claim plus a leeway in
1299    /// sats per vbyte.
1300    NetworkRecommended { leeway_sat_per_vbyte: u64 },
1301}
1302
1303/// How a [`MaxFee`] is persisted: one variant per on-chain kind, each carrying the
1304/// proportional part. Ceilings stored before the split are in this shape, so
1305/// keeping it lets them load as they are.
1306#[derive(Serialize, Deserialize)]
1307enum StoredMaxFee {
1308    Fixed {
1309        amount: u64,
1310        #[serde(default, skip_serializing_if = "Option::is_none")]
1311        proportional_ppm: Option<u32>,
1312    },
1313    Rate {
1314        sat_per_vbyte: u64,
1315        #[serde(default, skip_serializing_if = "Option::is_none")]
1316        proportional_ppm: Option<u32>,
1317    },
1318    NetworkRecommended {
1319        leeway_sat_per_vbyte: u64,
1320        #[serde(default, skip_serializing_if = "Option::is_none")]
1321        proportional_ppm: Option<u32>,
1322    },
1323}
1324
1325impl From<StoredMaxFee> for MaxFee {
1326    fn from(stored: StoredMaxFee) -> Self {
1327        let (onchain, proportional_ppm) = match stored {
1328            StoredMaxFee::Fixed {
1329                amount,
1330                proportional_ppm,
1331            } => (MaxOnchainFee::Fixed { amount }, proportional_ppm),
1332            StoredMaxFee::Rate {
1333                sat_per_vbyte,
1334                proportional_ppm,
1335            } => (MaxOnchainFee::Rate { sat_per_vbyte }, proportional_ppm),
1336            StoredMaxFee::NetworkRecommended {
1337                leeway_sat_per_vbyte,
1338                proportional_ppm,
1339            } => (
1340                MaxOnchainFee::NetworkRecommended {
1341                    leeway_sat_per_vbyte,
1342                },
1343                proportional_ppm,
1344            ),
1345        };
1346        MaxFee {
1347            onchain,
1348            proportional_ppm,
1349        }
1350    }
1351}
1352
1353impl From<MaxFee> for StoredMaxFee {
1354    fn from(max_fee: MaxFee) -> Self {
1355        let proportional_ppm = max_fee.proportional_ppm;
1356        match max_fee.onchain {
1357            MaxOnchainFee::Fixed { amount } => StoredMaxFee::Fixed {
1358                amount,
1359                proportional_ppm,
1360            },
1361            MaxOnchainFee::Rate { sat_per_vbyte } => StoredMaxFee::Rate {
1362                sat_per_vbyte,
1363                proportional_ppm,
1364            },
1365            MaxOnchainFee::NetworkRecommended {
1366                leeway_sat_per_vbyte,
1367            } => StoredMaxFee::NetworkRecommended {
1368                leeway_sat_per_vbyte,
1369                proportional_ppm,
1370            },
1371        }
1372    }
1373}
1374
1375impl MaxFee {
1376    /// The share of the deposit amount allowed on top of the on-chain part, in
1377    /// parts per million.
1378    pub(crate) fn proportional_ppm(&self) -> u32 {
1379        self.proportional_ppm.unwrap_or(0)
1380    }
1381
1382    /// The on-chain part of the ceiling, priced at the current fee market.
1383    pub(crate) async fn to_fee(&self, fee_market: &FeeMarket<'_>) -> Result<Fee, SdkError> {
1384        match &self.onchain {
1385            MaxOnchainFee::Fixed { amount } => Ok(Fee::Fixed { amount: *amount }),
1386            MaxOnchainFee::Rate { sat_per_vbyte } => Ok(Fee::Rate {
1387                sat_per_vbyte: *sat_per_vbyte,
1388            }),
1389            MaxOnchainFee::NetworkRecommended {
1390                leeway_sat_per_vbyte,
1391            } => {
1392                let recommended_fees = fee_market.recommended_fees().await?;
1393                let max_fee_rate = recommended_fees
1394                    .fastest_fee
1395                    .saturating_add(*leeway_sat_per_vbyte);
1396                Ok(Fee::Rate {
1397                    sat_per_vbyte: max_fee_rate,
1398                })
1399            }
1400        }
1401    }
1402}
1403
1404#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1405#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1406pub enum Fee {
1407    // Fixed fee amount in sats
1408    Fixed { amount: u64 },
1409    // Relative fee rate in satoshis per vbyte
1410    Rate { sat_per_vbyte: u64 },
1411}
1412
1413impl Fee {
1414    pub fn to_sats(&self, vbytes: u64) -> u64 {
1415        match self {
1416            Fee::Fixed { amount } => *amount,
1417            Fee::Rate { sat_per_vbyte } => sat_per_vbyte.saturating_mul(vbytes),
1418        }
1419    }
1420}
1421
1422/// State of an instant claim attempt on a deposit.
1423#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1424#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1425pub enum InstantClaimStatus {
1426    /// The early claim was declined and the deposit falls through to the claim at
1427    /// maturity. `max_fee_sats` is the ceiling that declined it, unset when the
1428    /// decline was for a reason no ceiling will fix. `confirmations` is the depth
1429    /// it was declined at.
1430    Declined {
1431        max_fee_sats: Option<u64>,
1432        #[serde(default)]
1433        confirmations: u32,
1434    },
1435    /// An instant claim was submitted and is settling. Carries the SSP claim id.
1436    Submitted { claim_id: String },
1437    /// A claim has taken the deposit: either its credit arrived here, or the
1438    /// provider reports the deposit as already claimed. It stays listed until
1439    /// the provider spends the output, so treat it as settled rather than as
1440    /// awaiting action.
1441    Claimed,
1442}
1443
1444/// State of the deposit refund broadcast.
1445#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1446#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1447pub enum RefundState {
1448    /// The refund is signed and stored but has not been seen on the network.
1449    /// `last_error` carries the reason the most recent broadcast was refused,
1450    /// unset while none has been refused. A refund whose fee is under the
1451    /// network's current minimum stays here until it is re-created at a higher
1452    /// fee.
1453    BroadcastPending { last_error: Option<String> },
1454    /// The refund has been accepted by the network and is waiting to confirm.
1455    Broadcast,
1456}
1457
1458#[derive(Debug, Clone, Serialize, Deserialize)]
1459#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1460pub struct DepositInfo {
1461    /// Transaction id of the on-chain output the deposit came from.
1462    pub txid: String,
1463    /// Index of that output within its transaction.
1464    pub vout: u32,
1465    /// Deposit value in satoshis.
1466    pub amount_sats: u64,
1467    /// Whether the deposit has enough confirmations to be claimed.
1468    pub is_mature: bool,
1469    /// Raw refund transaction, once one has been created.
1470    pub refund_tx: Option<String>,
1471    /// Transaction id of the refund, once one has been created.
1472    pub refund_tx_id: Option<String>,
1473    /// How far the refund has got towards the network. Unset when no refund has
1474    /// been created, and on refunds created before this field existed.
1475    pub refund_state: Option<RefundState>,
1476    /// Why the last claim attempt failed. Unset while none has failed.
1477    pub claim_error: Option<DepositClaimError>,
1478    /// Unset when no instant claim has been attempted.
1479    pub instant_claim_status: Option<InstantClaimStatus>,
1480    /// The fee ceiling standing for this deposit alone. It caps what may be paid
1481    /// to claim the deposit ahead of maturity. The claim at maturity runs under
1482    /// whichever is larger, this or the configured max deposit claim fee, so a
1483    /// ceiling set below that one does not hold the deposit back from it. Unset
1484    /// means the configured ceiling applies to both.
1485    pub max_claim_fee: Option<MaxFee>,
1486}
1487
1488#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1489pub struct ClaimDepositRequest {
1490    pub txid: String,
1491    pub vout: u32,
1492    /// Caps what the claim may cost, and is recorded on the deposit so later
1493    /// automatic attempts are held to it too.
1494    ///
1495    /// Raising it above the quoted spread is what lets a deposit be claimed ahead
1496    /// of maturity without further input. Lowering it below the spread keeps the
1497    /// deposit from being claimed early, and does not hold back its claim at
1498    /// maturity: that one runs under whichever is larger, this or the configured
1499    /// max deposit claim fee.
1500    ///
1501    /// Unset claims under the configured max deposit claim fee and clears any
1502    /// ceiling previously recorded on the deposit.
1503    ///
1504    /// The ceiling is recorded before the claim is attempted, so it stands even
1505    /// when the attempt is then declined for exceeding it.
1506    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
1507    pub max_fee: Option<MaxFee>,
1508}
1509
1510/// Why a claim was deferred rather than made.
1511#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
1512#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1513pub enum ClaimDeferredReason {
1514    /// Claiming ahead of maturity costs more than the fee ceiling allows. Unlike a
1515    /// depth that has not arrived, this does not clear on its own: the deposit is
1516    /// claimed at maturity unless the ceiling is raised.
1517    ///
1518    /// `required_fee_sats` is what the early claim would have cost, so a caller can
1519    /// offer it at that price rather than quoting again.
1520    MaxFeeExceeded {
1521        /// What the provider asked to credit the deposit early.
1522        required_fee_sats: u64,
1523        /// The ceiling it was held to.
1524        max_fee_sats: u64,
1525    },
1526    /// The provider offers no early claim at the deposit's current depth, either
1527    /// because it is too shallow for any plan or because none was offered for it.
1528    /// Depth is the usual cause, and it resolves itself: the next confirmation may
1529    /// well bring an early claim within reach.
1530    NoEarlyClaimAvailable,
1531    /// The provider refused the early claim or could not be reached, so nothing was
1532    /// submitted. Unlike a depth that has not arrived, waiting for a confirmation
1533    /// does not address this, and the deposit is claimed at maturity unless a later
1534    /// call succeeds. `message` is what the provider or the transport reported.
1535    ProviderDeclined { message: String },
1536}
1537
1538/// What became of a claim.
1539#[allow(clippy::large_enum_variant)]
1540#[derive(Debug, Clone, Serialize)]
1541#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1542pub enum ClaimDepositOutcome {
1543    /// Claimed at maturity and settled, carrying the payment it produced.
1544    Settled { payment: Payment },
1545    /// Claimed ahead of maturity and submitted. The transfer settles
1546    /// asynchronously, so no payment is returned yet: watch for it via events or
1547    /// `list_payments`.
1548    Submitted,
1549    /// Nothing was claimed yet, and no further call is needed: any fee ceiling this
1550    /// one asked for stands on the deposit, and the SDK keeps claiming it on its
1551    /// own. An early claim is attempted again as the deposit gains confirmations,
1552    /// so one declined for being too shallow is often claimed early a block or two
1553    /// later. Failing that, the deposit is claimed at maturity.
1554    ///
1555    /// An ordinary outcome rather than a failure. `reason` says which of the two to
1556    /// expect: a depth that has not arrived yet, or a cost the ceiling will not
1557    /// cover.
1558    Deferred { reason: ClaimDeferredReason },
1559}
1560
1561#[derive(Debug, Clone, Serialize)]
1562#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1563pub struct ClaimDepositResponse {
1564    /// What the call did. Which outcome occurs follows from the deposit's maturity
1565    /// and the fee ceiling, not from anything the caller asks for, so handle all
1566    /// three on every claim.
1567    pub outcome: ClaimDepositOutcome,
1568}
1569
1570#[derive(Debug, Clone, Serialize)]
1571#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1572pub struct FetchClaimDepositQuoteRequest {
1573    pub txid: String,
1574    pub vout: u32,
1575}
1576
1577/// What one way of claiming a deposit costs.
1578#[derive(Debug, Clone, Serialize)]
1579#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1580pub struct ClaimDepositQuote {
1581    /// The depth this becomes claimable at, as a total confirmation count on the
1582    /// deposit tx and not a number still to wait. A deposit already at or past
1583    /// this depth can be claimed.
1584    pub confirmations_required: u32,
1585    /// What reaches the balance.
1586    pub credit_amount_sats: u64,
1587    /// The deposit value less the credit.
1588    pub fee_sats: u64,
1589    /// The most this claim may cost for this deposit, in sats, under the max claim
1590    /// fee that applies to it. The SDK claims automatically when `fee_sats` is
1591    /// within it. Unset when automatic claiming is off.
1592    pub max_fee_sats: Option<u64>,
1593    /// The provider would not quote this yet, so the fee is derived from current
1594    /// on-chain fees and the real one may differ.
1595    pub is_estimate: bool,
1596}
1597
1598#[derive(Debug, Clone, Serialize)]
1599#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1600pub struct FetchClaimDepositQuoteResponse {
1601    pub amount_sats: u64,
1602    /// Confirmations the deposit has now, 0 while unconfirmed.
1603    pub confirmations: u32,
1604    /// Claiming ahead of maturity, for a spread. Absent when the provider offers
1605    /// no such option for this deposit, and when claiming early would not actually
1606    /// be earlier: a deposit that has already matured, or a plan crediting no
1607    /// sooner than maturity would, is only ever the more expensive way to wait.
1608    ///
1609    /// Also absent when the provider could not be reached for a quote, which is not
1610    /// distinguished here from having nothing to offer: both mean there is no early
1611    /// claim to show right now, and the one worth retrying is the transient one.
1612    ///
1613    /// Priced regardless of the max claim fee, so it can be offered: its
1614    /// `fee_sats` within its `max_fee_sats` means the SDK claims it on its own.
1615    /// Claiming it by hand needs a max fee of at least its `fee_sats`.
1616    pub instant: Option<ClaimDepositQuote>,
1617    /// Claiming once the deposit matures.
1618    pub mature: ClaimDepositQuote,
1619}
1620
1621#[derive(Debug, Clone, Serialize)]
1622#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1623pub struct RefundDepositRequest {
1624    pub txid: String,
1625    pub vout: u32,
1626    pub destination_address: String,
1627    pub fee: Fee,
1628}
1629
1630#[derive(Debug, Clone, Serialize)]
1631#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1632pub struct RefundDepositResponse {
1633    pub tx_id: String,
1634    pub tx_hex: String,
1635}
1636
1637#[derive(Debug, Clone, Serialize)]
1638#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1639pub struct ListUnclaimedDepositsRequest {}
1640
1641#[derive(Debug, Clone, Serialize)]
1642#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1643pub struct ListUnclaimedDepositsResponse {
1644    pub deposits: Vec<DepositInfo>,
1645}
1646
1647/// The available providers for buying Bitcoin
1648/// Request to buy Bitcoin using an external provider.
1649///
1650/// Each variant carries only the parameters relevant to that provider.
1651#[derive(Debug, Clone)]
1652#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1653pub enum BuyBitcoinRequest {
1654    /// `MoonPay`: Fiat purchase via credit card, Apple Pay, etc.
1655    Moonpay {
1656        /// How `MoonPay` delivers the purchase. Defaults to Bitcoin with no
1657        /// amount.
1658        delivery: Option<MoonpayDelivery>,
1659        /// Custom redirect URL after purchase completion.
1660        redirect_url: Option<String>,
1661    },
1662    /// `CashApp`: Pay via the Lightning Network.
1663    /// Returns a `cash.app` deep link to a bolt11 invoice. A conversion
1664    /// provider delivers the payment to the wallet over Spark, and the payer
1665    /// pays its fee on top of the amount. Only available on mainnet.
1666    ///
1667    /// The amount is required. With an amountless invoice, Cash App only
1668    /// lets the payer fund from their existing Cash App BTC balance. With
1669    /// a fixed-amount invoice, Cash App opens up funding via fiat balance
1670    /// and debit card.
1671    CashApp {
1672        /// Amount in satoshis the wallet receives. Must be non-zero.
1673        amount_sats: u64,
1674    },
1675}
1676
1677impl Default for BuyBitcoinRequest {
1678    fn default() -> Self {
1679        Self::Moonpay {
1680            delivery: None,
1681            redirect_url: None,
1682        }
1683    }
1684}
1685
1686/// How a `MoonPay` purchase is delivered.
1687#[derive(Debug, Clone)]
1688#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1689pub enum MoonpayDelivery {
1690    /// Bitcoin to an on-chain deposit address.
1691    Bitcoin {
1692        /// Amount in satoshis to prefill the checkout with.
1693        amount_sat: Option<u64>,
1694    },
1695    /// USDC on Solana, converted on arrival into the active stable balance
1696    /// token if set, otherwise Bitcoin. Requires `cross_chain_config`.
1697    CrossChain {
1698        /// USD amount to prefill the checkout with, in 6-decimal base units
1699        /// (`1_000_000` is $1).
1700        amount: u128,
1701        /// Whether the conversion fee is added on top of `amount`
1702        /// (`FeesExcluded`, the default) or deducted from it (`FeesIncluded`).
1703        fee_mode: Option<crate::cross_chain::CrossChainFeeMode>,
1704    },
1705}
1706
1707impl Default for MoonpayDelivery {
1708    fn default() -> Self {
1709        Self::Bitcoin { amount_sat: None }
1710    }
1711}
1712
1713/// Response containing a URL to complete the Bitcoin purchase
1714#[derive(Debug, Clone, Serialize)]
1715#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1716pub struct BuyBitcoinResponse {
1717    /// The URL to open in a browser to complete the purchase
1718    pub url: String,
1719    /// The conversion quote, populated for cross-chain delivery and for
1720    /// `CashApp`, whose purchase is delivered through a conversion provider.
1721    pub cross_chain_info: Option<crate::cross_chain::CrossChainReceiveInfo>,
1722}
1723
1724/// Response from refunding pending conversions.
1725#[derive(Debug, Clone, Serialize, Default)]
1726#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1727pub struct RefundPendingConversionsResponse {
1728    /// Conversions successfully refunded this pass.
1729    pub refunded: u32,
1730    /// Conversions not clawed back this pass: held back by a safety window, or
1731    /// found to have executed after all. Only the former are retried.
1732    pub skipped: u32,
1733    /// Conversions whose clawback did not complete this pass (rejected or
1734    /// errored; funds not returned). The next pass will retry them.
1735    pub failed: u32,
1736}
1737
1738/// **Deprecated.** Use `CreateBridgeRequest` with the bridging client's
1739/// `bridge_from_cash_app`.
1740///
1741/// Request for a payment link that sends USDC/USDT to an external-chain
1742/// recipient, funded by Cash App over Lightning.
1743///
1744/// The user pays the returned URL, and the cross-chain provider delivers the
1745/// stablecoin to `address`. No funds move through the Spark wallet. Only
1746/// available on mainnet.
1747#[derive(Debug, Clone)]
1748#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1749pub struct PreparePaymentLinkRequest {
1750    /// Recipient address on the destination chain (e.g. an EVM `0x...` address).
1751    pub address: String,
1752    /// The destination route from calling `get_cross_chain_routes()` with the
1753    /// `CrossChainRouteFilter::PaymentLink` filter. Selects the destination chain
1754    /// + asset (e.g. USDC on Base).
1755    pub route: CrossChainRoutePair,
1756    /// Amount in the destination asset's base units, per the route's
1757    /// `decimals`. These routes deliver USD-pegged stablecoins, so at parity
1758    /// this is the USD value: `1_000_000` is 1 USDC (6 decimals), about $1.
1759    ///
1760    /// With the default fee policy the recipient receives this net amount.
1761    /// With `FeesIncluded` it is the amount the payer deposits.
1762    pub amount: u128,
1763    /// Whether fees are added on top of `amount` (`FeesExcluded`, the default)
1764    /// or deducted from it (`FeesIncluded`).
1765    pub fee_policy: Option<FeePolicy>,
1766    /// Maximum slippage tolerance in basis points. Falls back to the SDK
1767    /// default when unset.
1768    pub max_slippage_bps: Option<u32>,
1769}
1770
1771/// **Deprecated.** Use `CreateBridgeResponse`, from the bridging client's
1772/// `bridge_from_cash_app`.
1773///
1774/// Response to a [`PreparePaymentLinkRequest`]. Mirrors `BuyBitcoinResponse`
1775/// (a payable `url`) plus the quote so the caller can display the expected
1776/// delivery and fees.
1777#[derive(Debug, Clone, Serialize)]
1778#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1779pub struct PreparePaymentLinkResponse {
1780    /// The URL to open in a browser; paying it delivers the stablecoin.
1781    pub url: String,
1782    /// Sats the payer deposits through the fiat rail.
1783    pub amount_sats: u64,
1784    /// Estimated amount delivered to the recipient, in `asset` base units.
1785    pub estimated_out: u128,
1786    /// The destination stablecoin symbol (e.g. `USDC`). `estimated_out` is
1787    /// denominated in it.
1788    pub asset: String,
1789    /// Provider service fee, including the partner fee when one is set, in
1790    /// `service_fee_asset` base units.
1791    pub service_fee_amount: u128,
1792    /// Denomination of `service_fee_amount`. `None` means sats: Boltz
1793    /// denominates its fee in sats, Orchestra in the stablecoin.
1794    pub service_fee_asset: Option<String>,
1795    /// Decimals of `service_fee_asset`, for formatting `service_fee_amount`.
1796    /// Unset when the fee is in sats or the provider did not report them.
1797    pub service_fee_asset_decimals: Option<u32>,
1798    /// RFC3339 timestamp after which the quote is no longer valid.
1799    pub expires_at: String,
1800}
1801
1802/// **Deprecated.** Use `CreateBridgeRequest` with the bridging client's
1803/// `bridge_from_cash_app`.
1804///
1805/// Request to bridge from Cash App to USDC/USDT on an external chain.
1806///
1807/// The payer pays the returned URL in Cash App over Lightning, and the
1808/// cross-chain provider delivers the stablecoin to `address`. No funds move
1809/// through the Spark wallet. Only available on mainnet.
1810#[derive(Debug, Clone)]
1811#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1812pub struct BridgeFromCashAppRequest {
1813    /// Recipient address on the destination chain (e.g. an EVM `0x...` address).
1814    pub address: String,
1815    /// The destination route from calling `get_cross_chain_routes()` with the
1816    /// `CrossChainRouteFilter::Send` filter and its `delivery_method` set to
1817    /// `Lightning`. Selects the destination chain + asset (e.g. USDC on Base).
1818    pub route: CrossChainRoutePair,
1819    /// Amount in the destination asset's base units, per the route's
1820    /// `decimals`. These routes deliver USD-pegged stablecoins, so at parity
1821    /// this is the USD value: `1_000_000` is 1 USDC (6 decimals), about $1.
1822    ///
1823    /// With the default fee policy the recipient receives this net amount.
1824    /// With `FeesIncluded` it is the amount the payer deposits.
1825    pub amount: u128,
1826    /// Whether fees are added on top of `amount` (`FeesExcluded`, the default)
1827    /// or deducted from it (`FeesIncluded`).
1828    pub fee_policy: Option<FeePolicy>,
1829    /// Maximum slippage tolerance in basis points. Falls back to the SDK
1830    /// default when unset.
1831    pub max_slippage_bps: Option<u32>,
1832}
1833
1834/// **Deprecated.** Use `CreateBridgeResponse`, from the bridging client's
1835/// `bridge_from_cash_app`.
1836///
1837/// Response to a [`BridgeFromCashAppRequest`]: the URL to pay plus the quote,
1838/// so the caller can display the expected delivery and fees.
1839#[derive(Debug, Clone, Serialize)]
1840#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1841pub struct BridgeFromCashAppResponse {
1842    /// The `cash.app` URL to open. Paying it delivers the stablecoin.
1843    pub url: String,
1844    /// Sats the payer deposits through Cash App.
1845    pub amount_sats: u64,
1846    /// Estimated amount delivered to the recipient, in `asset` base units.
1847    pub estimated_out: u128,
1848    /// The destination stablecoin symbol (e.g. `USDC`). `estimated_out` is
1849    /// denominated in it.
1850    pub asset: String,
1851    /// Provider service fee, including the partner fee when one is set, in
1852    /// `service_fee_asset` base units.
1853    pub service_fee_amount: u128,
1854    /// Denomination of `service_fee_amount`. Unset means sats.
1855    pub service_fee_asset: Option<String>,
1856    /// Decimals of `service_fee_asset`, for formatting `service_fee_amount`.
1857    /// Unset when the fee is in sats or the provider did not report them.
1858    pub service_fee_asset_decimals: Option<u32>,
1859    /// Quote expiry as a Unix timestamp in seconds.
1860    pub expires_at: u64,
1861}
1862
1863impl From<PreparePaymentLinkRequest> for BridgeFromCashAppRequest {
1864    fn from(request: PreparePaymentLinkRequest) -> Self {
1865        BridgeFromCashAppRequest {
1866            address: request.address,
1867            route: request.route,
1868            amount: request.amount,
1869            fee_policy: request.fee_policy,
1870            max_slippage_bps: request.max_slippage_bps,
1871        }
1872    }
1873}
1874
1875impl From<BridgeFromCashAppResponse> for PreparePaymentLinkResponse {
1876    fn from(response: BridgeFromCashAppResponse) -> Self {
1877        PreparePaymentLinkResponse {
1878            url: response.url,
1879            amount_sats: response.amount_sats,
1880            estimated_out: response.estimated_out,
1881            asset: response.asset,
1882            service_fee_amount: response.service_fee_amount,
1883            service_fee_asset: response.service_fee_asset,
1884            service_fee_asset_decimals: response.service_fee_asset_decimals,
1885            expires_at: i64::try_from(response.expires_at)
1886                .ok()
1887                .and_then(|secs| chrono::DateTime::from_timestamp(secs, 0))
1888                .map_or_else(
1889                    || response.expires_at.to_string(),
1890                    |dt| dt.to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
1891                ),
1892        }
1893    }
1894}
1895
1896/// **Deprecated.** Use `CreateBridgeRequest` with the bridging client's
1897/// `bridge_to_cash_app`.
1898///
1899/// Request to bridge USDC/USDT from an external chain to a Cash App user.
1900///
1901/// The payer deposits the stablecoin to the returned address, and the
1902/// cross-chain provider pays the recipient in Bitcoin over Lightning. No funds
1903/// move through the Spark wallet. Only available on mainnet.
1904#[derive(Debug, Clone)]
1905#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1906pub struct BridgeToCashAppRequest {
1907    /// The Cash App user, as a username (`alice` or `$alice`) or a Cash App
1908    /// Lightning address (`alice@cash.app`).
1909    pub recipient: String,
1910    /// The source route from calling `get_cross_chain_routes()` with the
1911    /// `CrossChainRouteFilter::Receive` filter and its `delivery_method` set to
1912    /// `Lightning`. Selects the source chain + asset (e.g. USDC on Base).
1913    pub route: CrossChainRoutePair,
1914    /// Amount in the source asset's base units, per the route's `decimals`.
1915    /// These routes carry USD-pegged stablecoins, so at parity this is the USD
1916    /// value: `1_000_000` is 1 USDC (6 decimals), about $1.
1917    ///
1918    /// With the default fee policy the recipient receives about this value in
1919    /// Bitcoin. With `FeesIncluded` it is the amount the payer deposits.
1920    pub amount: u128,
1921    /// Whether fees are added on top of `amount` (`FeesExcluded`, the default)
1922    /// or deducted from it (`FeesIncluded`).
1923    pub fee_policy: Option<FeePolicy>,
1924    /// The payer's address on the source chain. The deposit is refunded here
1925    /// if the recipient can't be paid.
1926    pub refund_address: String,
1927    /// Maximum slippage tolerance in basis points. Falls back to the SDK
1928    /// default when unset.
1929    pub max_slippage_bps: Option<u32>,
1930}
1931
1932/// **Deprecated.** Use `CreateBridgeResponse`, from the bridging client's
1933/// `bridge_to_cash_app`.
1934///
1935/// Response to a [`BridgeToCashAppRequest`].
1936#[derive(Debug, Clone, Serialize)]
1937#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1938pub struct BridgeToCashAppResponse {
1939    /// What the payer pays: an EIP-681 URI on EVM chains, otherwise the bare
1940    /// deposit address.
1941    pub payment_request: String,
1942    /// The deposit and the expected delivery. `expected_received_amount` is
1943    /// in sats.
1944    pub info: crate::cross_chain::CrossChainReceiveInfo,
1945}
1946
1947impl std::fmt::Display for MaxFee {
1948    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1949        match &self.onchain {
1950            MaxOnchainFee::Fixed { amount } => write!(f, "Fixed: {amount}")?,
1951            MaxOnchainFee::Rate { sat_per_vbyte } => write!(f, "Rate: {sat_per_vbyte}")?,
1952            MaxOnchainFee::NetworkRecommended {
1953                leeway_sat_per_vbyte,
1954            } => write!(f, "NetworkRecommended: {leeway_sat_per_vbyte}")?,
1955        }
1956        match self.proportional_ppm() {
1957            0 => Ok(()),
1958            ppm => write!(f, " + {ppm} ppm"),
1959        }
1960    }
1961}
1962
1963#[derive(Debug, Clone)]
1964#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1965pub struct Credentials {
1966    pub username: String,
1967    pub password: String,
1968}
1969
1970/// Request to get the balance of the wallet
1971#[derive(Debug, Clone)]
1972#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1973pub struct GetInfoRequest {
1974    /// When `Some(true)`, and `background_tasks_enabled` is `true`, the call
1975    /// waits for the initial Full sync to complete before returning. The sync
1976    /// can fail to reach the network: `last_sync_error` in the response says
1977    /// whether it did.
1978    ///
1979    /// When `background_tasks_enabled` is `false`, setting this to `Some(true)`
1980    /// is rejected with an invalid-input error. There is no background sync to
1981    /// wait on; call `sync_wallet` explicitly first if you need fresh state.
1982    pub ensure_synced: Option<bool>,
1983}
1984
1985/// Response containing the balance of the wallet
1986#[derive(Debug, Clone, Serialize)]
1987#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
1988pub struct GetInfoResponse {
1989    /// The identity public key of the wallet as a hex string
1990    pub identity_pubkey: String,
1991    /// The balance in satoshis
1992    pub balance_sats: u64,
1993    /// The balances of the tokens in the wallet keyed by the token identifier
1994    pub token_balances: HashMap<String, TokenBalance>,
1995    /// Funds that left `balance_sats` and wait to be recovered on-chain with
1996    /// `recover_funds`. The SDK on this device takes them out of this total
1997    /// once it has seen their recovery in a block, at the next sync at the
1998    /// latest. It looks for that in `prepare_recover_funds`, `recover_funds`
1999    /// and `check_recover_funds`, and during syncs at most once per leaf. The
2000    /// total includes funds too small to be worth recovering at the fee rate
2001    /// you choose.
2002    pub recoverable_funds_sats: u64,
2003    /// Why the latest sync could not refresh the balances, which may then be
2004    /// out of date. Unset once a sync succeeds. It is not persisted, so it is
2005    /// also unset after each `connect` until the first sync ends.
2006    pub last_sync_error: Option<SyncError>,
2007    /// When a sync last refreshed the balances, in seconds since the Unix
2008    /// epoch. Unset until a sync succeeds with this storage.
2009    pub last_synced_at: Option<u64>,
2010}
2011
2012/// Why a sync could not refresh the balances. Each message is for logs, not
2013/// for display.
2014#[derive(Debug, Clone, PartialEq, Serialize)]
2015#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2016pub enum SyncError {
2017    /// The network could not be reached, or is temporarily too busy to answer.
2018    Network { message: String },
2019    /// Any other failure.
2020    Generic { message: String },
2021}
2022
2023#[derive(Debug, Clone, Serialize, Deserialize)]
2024#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2025pub struct TokenBalance {
2026    pub balance: u128,
2027    pub token_metadata: TokenMetadata,
2028}
2029
2030#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2031#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2032pub struct TokenMetadata {
2033    pub identifier: String,
2034    /// Hex representation of the issuer public key
2035    pub issuer_public_key: String,
2036    pub name: String,
2037    pub ticker: String,
2038    /// Number of decimals the token uses
2039    pub decimals: u32,
2040    pub max_supply: u128,
2041    pub is_freezable: bool,
2042}
2043
2044/// Request to sync the wallet with the Spark network
2045#[derive(Debug, Clone)]
2046#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2047pub struct SyncWalletRequest {}
2048
2049/// Response from synchronizing the wallet
2050#[derive(Debug, Clone, Serialize)]
2051#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2052pub struct SyncWalletResponse {
2053    /// Why the sync could not refresh the balances, which may then be out of
2054    /// date. Unset when it succeeded.
2055    pub last_sync_error: Option<SyncError>,
2056    /// When a sync last refreshed the balances, in seconds since the Unix
2057    /// epoch. Unset until a sync succeeds with this storage.
2058    pub last_synced_at: Option<u64>,
2059}
2060
2061#[derive(Clone, Serialize)]
2062#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2063pub enum ReceivePaymentMethod {
2064    SparkAddress,
2065    SparkInvoice {
2066        /// Amount to receive. Denominated in sats if token identifier is empty, otherwise in the token base units
2067        amount: Option<u128>,
2068        /// The presence of this field indicates that the payment is for a token
2069        /// If empty, it is a Bitcoin payment
2070        token_identifier: Option<String>,
2071        /// The expiry time of the invoice as a unix timestamp in seconds
2072        expiry_time: Option<u64>,
2073        /// A description to embed in the invoice.
2074        description: Option<String>,
2075        /// If set, the invoice may only be fulfilled by a payer with this public key
2076        sender_public_key: Option<String>,
2077    },
2078    BitcoinAddress {
2079        /// If true, rotate to a new deposit address. Previous ones remain valid.
2080        /// If false or absent, return the existing address (creating one if none
2081        /// exists yet).
2082        new_address: Option<bool>,
2083    },
2084    Bolt11Invoice {
2085        description: String,
2086        amount_sats: Option<u64>,
2087        /// The expiry of the invoice as a duration in seconds
2088        expiry_secs: Option<u32>,
2089        /// If set, creates a HODL invoice with this payment hash (hex-encoded).
2090        /// The payer's HTLC will be held until the preimage is provided via
2091        /// `claim_htlc_payment` or the HTLC expires.
2092        payment_hash: Option<String>,
2093        /// Spark identity public key that will receive the payment.
2094        /// If absent, the connected wallet's identity public key is used.
2095        receiver_identity_public_key: Option<String>,
2096        /// If set, creates an invoice for the hash of this preimage (hex-encoded)
2097        /// instead of one the SDK generates. The SSP settles the payment without
2098        /// the receiver, and the payer learns the preimage. Use a random value
2099        /// below the secp256k1 group order, and a new one for each invoice. The
2100        /// invoice never includes a Spark address. Set at most one of this and
2101        /// `payment_hash`.
2102        preimage: Option<String>,
2103    },
2104    CrossChain {
2105        /// The selected cross-chain route in the receive direction.
2106        route: crate::cross_chain::CrossChainRoutePair,
2107        /// The amount, in the source asset's base units (`route.decimals`).
2108        /// USD-stable sources are at parity, so `1 USD = 10^route.decimals`
2109        /// (e.g. `1_000_000` for 6-decimal USDC/USDT, `10^18` for 18-decimal
2110        /// BSC USDC).
2111        ///
2112        /// - `FeesExcluded` (default): what the receiver ends up with, sized
2113        ///   as if `amount` source units were converted to the Spark-side
2114        ///   destination at parity (USDB) or the live BTC/USD rate (Bitcoin).
2115        /// - `FeesIncluded`: what the sender deposits. The receiver ends up
2116        ///   with that amount minus provider fees.
2117        amount: u128,
2118        /// Spark-side asset the receiver wants delivered. When absent, the
2119        /// SDK auto-selects: the wallet's active stable-balance token if
2120        /// the route supports it, otherwise Bitcoin (sats). When set, the
2121        /// value must appear in the route's `accepted_assets`.
2122        destination: Option<crate::cross_chain::SparkAsset>,
2123        /// How `amount` should be interpreted. When absent, defaults to
2124        /// `FeesExcluded`.
2125        fee_mode: Option<crate::cross_chain::CrossChainFeeMode>,
2126        /// Maximum slippage in basis points. When absent, the SDK default
2127        /// (100 bps) is used.
2128        max_slippage_bps: Option<u32>,
2129        /// Per-request override for the overpay buffer applied to the
2130        /// sender's deposit when `fee_mode == FeesExcluded`. Range 0 to 500.
2131        /// When absent, falls back to `CrossChainConfig::default_target_overpay_bps`
2132        /// then the built-in default (15 bps). Ignored when `fee_mode`
2133        /// is `FeesIncluded`.
2134        target_overpay_bps: Option<u32>,
2135    },
2136}
2137
2138impl fmt::Debug for ReceivePaymentMethod {
2139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2140        match self {
2141            ReceivePaymentMethod::SparkAddress => f.write_str("SparkAddress"),
2142            ReceivePaymentMethod::SparkInvoice {
2143                amount,
2144                token_identifier,
2145                expiry_time,
2146                description,
2147                sender_public_key,
2148            } => f
2149                .debug_struct("SparkInvoice")
2150                .field("amount", amount)
2151                .field("token_identifier", token_identifier)
2152                .field("expiry_time", expiry_time)
2153                .field("description", description)
2154                .field("sender_public_key", sender_public_key)
2155                .finish(),
2156            ReceivePaymentMethod::BitcoinAddress { new_address } => f
2157                .debug_struct("BitcoinAddress")
2158                .field("new_address", new_address)
2159                .finish(),
2160            ReceivePaymentMethod::Bolt11Invoice {
2161                description,
2162                amount_sats,
2163                expiry_secs,
2164                payment_hash,
2165                receiver_identity_public_key,
2166                preimage,
2167            } => f
2168                .debug_struct("Bolt11Invoice")
2169                .field("description", description)
2170                .field("amount_sats", amount_sats)
2171                .field("expiry_secs", expiry_secs)
2172                .field("payment_hash", payment_hash)
2173                .field("receiver_identity_public_key", receiver_identity_public_key)
2174                .field("preimage", &preimage.as_ref().map(|_| "<redacted>"))
2175                .finish(),
2176            ReceivePaymentMethod::CrossChain {
2177                route,
2178                amount,
2179                destination,
2180                fee_mode,
2181                max_slippage_bps,
2182                target_overpay_bps,
2183            } => f
2184                .debug_struct("CrossChain")
2185                .field("route", route)
2186                .field("amount", amount)
2187                .field("destination", destination)
2188                .field("fee_mode", fee_mode)
2189                .field("max_slippage_bps", max_slippage_bps)
2190                .field("target_overpay_bps", target_overpay_bps)
2191                .finish(),
2192        }
2193    }
2194}
2195
2196#[derive(Debug, Clone, Serialize)]
2197#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2198pub enum SendPaymentMethod {
2199    BitcoinAddress {
2200        address: BitcoinAddressDetails,
2201        fee_quote: SendOnchainFeeQuote,
2202    },
2203    Bolt11Invoice {
2204        invoice_details: Bolt11InvoiceDetails,
2205        spark_transfer_fee_sats: Option<u64>,
2206        lightning_fee_sats: u64,
2207    }, // should be replaced with the parsed invoice
2208    SparkAddress {
2209        address: String,
2210        /// Fee to pay for the transaction
2211        /// Denominated in sats if token identifier is empty, otherwise in the token base units
2212        fee: u128,
2213        /// The presence of this field indicates that the payment is for a token
2214        /// If empty, it is a Bitcoin payment
2215        token_identifier: Option<String>,
2216    },
2217    SparkInvoice {
2218        spark_invoice_details: SparkInvoiceDetails,
2219        /// Fee to pay for the transaction
2220        /// Denominated in sats if token identifier is empty, otherwise in the token base units
2221        fee: u128,
2222        /// The presence of this field indicates that the payment is for a token
2223        /// If empty, it is a Bitcoin payment
2224        token_identifier: Option<String>,
2225    },
2226    /// A cross-chain send via a bridge/swap provider.
2227    CrossChainAddress {
2228        /// The route selected for this cross-chain send (includes provider, chain, asset).
2229        route: CrossChainRoutePair,
2230        /// Raw destination address (e.g. `0xabc...`).
2231        recipient_address: String,
2232        /// Amount routed to the provider, in the route's source-asset units
2233        /// (Boltz invoice sats; Orchestra deposit sats/token). On the
2234        /// token-conversion path (both `FeesIncluded` and `FeesExcluded`)
2235        /// the dispatcher overrides this with the wallet-side token debit
2236        /// when the source token and destination asset form a USD-stable pair.
2237        amount_in: u128,
2238        /// `amount_in` expressed in the cross-chain (destination) asset's
2239        /// base units, via the same rate the SDK used at prepare time.
2240        asset_amount_in: u128,
2241        /// Estimated recipient amount in cross-chain asset base units.
2242        estimated_out: u128,
2243        /// Prepare-time total user-visible fee in cross-chain asset base units.
2244        /// Covers provider spread + bridge/gas + DEX slippage. On the
2245        /// token-conversion path it also rolls in the LN routing budget; on
2246        /// the direct path that budget lives separately in
2247        /// `source_transfer_fee_sats`.
2248        fee_amount: u128,
2249        /// Provider service fee, including the partner fee when one is set, in
2250        /// `service_fee_asset` base units.
2251        service_fee_amount: u128,
2252        /// Asset which service fee is denominated in. Unset means BTC sats.
2253        service_fee_asset: Option<String>,
2254        /// Decimals of `service_fee_asset`, for formatting `service_fee_amount`.
2255        /// Unset when the fee is in sats or the provider did not report them.
2256        service_fee_asset_decimals: Option<u32>,
2257        /// Sats budget for moving the amount in from the wallet to the provider.
2258        source_transfer_fee_sats: u64,
2259        /// Fee mode the prepare ran under; the send stage matches.
2260        fee_mode: CrossChainFeeMode,
2261        /// ISO8601 timestamp after which the quote is no longer valid.
2262        expires_at: String,
2263        /// Provider-internal state, produced when preparing and consumed
2264        /// when sending.
2265        provider_context: CrossChainProviderContext,
2266    },
2267}
2268
2269#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2270#[derive(Debug, Clone, Serialize, Deserialize)]
2271pub struct SendOnchainFeeQuote {
2272    /// Identifies the quote to the provider when the payment is sent. Empty on
2273    /// an estimate, which no provider has issued.
2274    pub id: String,
2275    /// When the quote stops being honoured, as a Unix timestamp in seconds.
2276    /// Zero on an estimate.
2277    pub expires_at: u64,
2278    pub speed_fast: SendOnchainSpeedFeeQuote,
2279    pub speed_medium: SendOnchainSpeedFeeQuote,
2280    pub speed_slow: SendOnchainSpeedFeeQuote,
2281    /// Set when the wallet holds no bitcoin and a token conversion will fund the
2282    /// send, because the provider will not quote without funds to price against.
2283    /// The estimate is an upper bound: the payment quotes for real once the
2284    /// conversion lands, and fails rather than spending more than this.
2285    pub is_estimate: bool,
2286}
2287
2288#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2289#[derive(Debug, Clone, Serialize, Deserialize)]
2290pub struct SendOnchainSpeedFeeQuote {
2291    pub user_fee_sat: u64,
2292    pub l1_broadcast_fee_sat: u64,
2293}
2294
2295impl SendOnchainSpeedFeeQuote {
2296    pub fn total_fee_sat(&self) -> u64 {
2297        self.user_fee_sat.saturating_add(self.l1_broadcast_fee_sat)
2298    }
2299}
2300
2301#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2302pub struct ReceivePaymentRequest {
2303    pub payment_method: ReceivePaymentMethod,
2304}
2305
2306#[derive(Debug, Clone, Serialize)]
2307#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2308pub struct ReceivePaymentResponse {
2309    pub payment_request: String,
2310    /// Fee to pay to receive the payment
2311    /// Denominated in sats or token base units
2312    pub fee: u128,
2313    /// Optional information populated only for cross-chain receives.
2314    pub cross_chain_info: Option<crate::cross_chain::CrossChainReceiveInfo>,
2315}
2316
2317#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2318pub struct PrepareLnurlPayRequest {
2319    /// The amount to send. Denominated in satoshis, or in token base units
2320    /// when `token_identifier` is set.
2321    pub amount: u128,
2322    pub pay_request: LnurlPayRequestDetails,
2323    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2324    pub comment: Option<String>,
2325    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2326    pub validate_success_action_url: Option<bool>,
2327    /// The token identifier when sending a token amount with conversion.
2328    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2329    pub token_identifier: Option<String>,
2330    /// If provided, the payment will include a token conversion step before sending the payment
2331    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2332    pub conversion_options: Option<ConversionOptions>,
2333    /// How fees are handled. See [`FeePolicy`]. Defaults to `FeesExcluded`.
2334    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2335    pub fee_policy: Option<FeePolicy>,
2336}
2337
2338#[derive(Debug, Clone)]
2339#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2340pub struct PrepareLnurlPayResponse {
2341    /// The amount for the payment, always denominated in sats, even when a
2342    /// `token_identifier` and conversion are present.
2343    /// When a conversion is present, the token input amount is available in
2344    /// `conversion_estimate.amount_in`.
2345    pub amount_sats: u64,
2346    pub comment: Option<String>,
2347    pub pay_request: LnurlPayRequestDetails,
2348    /// The fee in satoshis. For `FeesIncluded` operations, this represents the total fee
2349    /// (including potential overpayment).
2350    pub fee_sats: u64,
2351    pub invoice_details: Bolt11InvoiceDetails,
2352    pub success_action: Option<SuccessAction>,
2353    /// When set, the payment will include a token conversion step before sending the payment
2354    pub conversion_estimate: Option<ConversionEstimate>,
2355    /// The fee policy actually applied. May differ from the request — e.g.,
2356    /// LNURL sends with `token_identifier` set + conversion are always
2357    /// `FeesIncluded` (explicit `FeesExcluded` is rejected).
2358    pub fee_policy: FeePolicy,
2359}
2360
2361#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2362pub struct LnurlPayRequest {
2363    pub prepare_response: PrepareLnurlPayResponse,
2364    /// If set, providing the same idempotency key for multiple requests will ensure that only one
2365    /// payment is made. If an idempotency key is re-used, the same payment will be returned.
2366    /// The idempotency key must be a valid UUID.
2367    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2368    pub idempotency_key: Option<String>,
2369}
2370
2371#[derive(Debug, Serialize)]
2372#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2373pub struct LnurlPayResponse {
2374    pub payment: Payment,
2375    pub success_action: Option<SuccessActionProcessed>,
2376}
2377
2378#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2379pub struct BuildUnsignedLnurlPayPackageRequest {
2380    pub prepare_response: PrepareLnurlPayResponse,
2381}
2382
2383#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2384pub struct PublishSignedLnurlPayPackageRequest {
2385    pub signed_package: SignedTransferPackage,
2386}
2387
2388#[allow(clippy::large_enum_variant)]
2389#[derive(Debug)]
2390#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2391pub enum PublishSignedLnurlPayResponse {
2392    SwapCompleted,
2393    PaymentSent { response: LnurlPayResponse },
2394}
2395
2396#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2397pub struct LnurlWithdrawRequest {
2398    /// The amount to withdraw in satoshis
2399    /// Must be within the min and max withdrawable limits
2400    pub amount_sats: u64,
2401    pub withdraw_request: LnurlWithdrawRequestDetails,
2402    /// If set, the function will return the payment if it is still pending after this
2403    /// number of seconds. If unset, the function will return immediately after
2404    /// initiating the LNURL withdraw.
2405    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2406    pub completion_timeout_secs: Option<u32>,
2407}
2408
2409#[derive(Debug, Serialize)]
2410#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2411pub struct LnurlWithdrawResponse {
2412    /// The Lightning invoice generated for the LNURL withdraw
2413    pub payment_request: String,
2414    pub payment: Option<Payment>,
2415}
2416
2417/// Represents the payment LNURL info
2418#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
2419#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2420pub struct LnurlPayInfo {
2421    pub ln_address: Option<String>,
2422    pub comment: Option<String>,
2423    pub domain: Option<String>,
2424    pub metadata: Option<String>,
2425    pub processed_success_action: Option<SuccessActionProcessed>,
2426    pub raw_success_action: Option<SuccessAction>,
2427}
2428
2429/// Represents the withdraw LNURL info
2430#[derive(Clone, Debug, Deserialize, Serialize)]
2431#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2432pub struct LnurlWithdrawInfo {
2433    pub withdraw_url: String,
2434}
2435
2436impl LnurlPayInfo {
2437    pub fn extract_description(&self) -> Option<String> {
2438        let Some(metadata) = &self.metadata else {
2439            return None;
2440        };
2441
2442        let Ok(metadata) = serde_json::from_str::<Vec<Vec<Value>>>(metadata) else {
2443            return None;
2444        };
2445
2446        for arr in metadata {
2447            if arr.len() != 2 {
2448                continue;
2449            }
2450            if let (Some(key), Some(value)) = (arr[0].as_str(), arr[1].as_str())
2451                && key == "text/plain"
2452            {
2453                return Some(value.to_string());
2454            }
2455        }
2456
2457        None
2458    }
2459}
2460
2461/// Specifies how fees are handled in a payment.
2462///
2463/// "Fees" are the wallet's sender-paid fees (Lightning routing, on-chain,
2464/// Spark transfer). They do not include provider spreads or destination-chain
2465/// costs on cross-chain routes; those are reported separately via
2466/// `estimated_out` on the prepare response and are not deterministic.
2467/// `FeePolicy` only controls the wallet's spend accounting.
2468#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
2469#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2470pub enum FeePolicy {
2471    /// Fees are added on top of `amount`. Wallet's total spend is
2472    /// `amount + fees`. For direct sat sends, the recipient receives exactly
2473    /// `amount`. Default.
2474    #[default]
2475    FeesExcluded,
2476    /// Fees are deducted from `amount`. Wallet's total spend is `amount`.
2477    /// Use this to drain a balance — pass `amount = balance` and the wallet
2478    /// spends exactly that.
2479    FeesIncluded,
2480}
2481
2482#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2483#[derive(Debug, Clone, Serialize, Deserialize)]
2484pub enum OnchainConfirmationSpeed {
2485    Fast,
2486    Medium,
2487    Slow,
2488}
2489
2490/// The payment destination. Either a raw string (bolt11, spark address, BIP-21,
2491/// cross-chain URI, etc.) that is parsed internally, or a structured
2492/// cross-chain destination with explicit chain + asset selection.
2493#[derive(Debug, Clone, Serialize)]
2494#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2495pub enum PaymentRequest {
2496    /// Unparsed user input string (bolt11, spark address, BIP-21, cross-chain URI, etc.)
2497    Input { input: String },
2498    /// Cross-chain send with a selected route from `get_cross_chain_routes()`.
2499    /// Amount comes from `PrepareSendPaymentRequest.amount`, not here.
2500    CrossChain {
2501        address: String,
2502        route: CrossChainRoutePair,
2503        /// Maximum slippage tolerance in basis points (1/100 of a percent)
2504        /// for the cross-chain quote. Must be in 10 to 500. Falls back to
2505        /// [`Config::default_slippage_bps`] when unset, which itself
2506        /// defaults to 100 bps (1%).
2507        max_slippage_bps: Option<u32>,
2508        /// Target-overpay pad in basis points applied on `FeesExcluded`
2509        /// conversion sends. Inflates the destination target before quoting
2510        /// so the recipient lands at or above the user's requested amount
2511        /// despite provider slippage. Must be in 0 to 500. Falls back to
2512        /// [`CrossChainConfig::default_target_overpay_bps`] when unset,
2513        /// which itself defaults to 15 bps.
2514        target_overpay_bps: Option<u32>,
2515    },
2516}
2517
2518#[allow(clippy::large_enum_variant)]
2519#[derive(Debug, Clone, Serialize, Deserialize)]
2520#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2521pub enum UnsignedTransferPackage {
2522    Swap {
2523        prepare_transfer: crate::signer::ExternalPrepareTransferRequest,
2524        target_amounts: Vec<u64>,
2525        amount_sat: u64,
2526        fee_sat: u64,
2527    },
2528    Transfer {
2529        prepare_transfer: crate::signer::ExternalPrepareTransferRequest,
2530        amount_sat: u64,
2531        fee_sat: u64,
2532        target: TransferTarget,
2533    },
2534    Token {
2535        prepare_token_transaction: crate::signer::ExternalPrepareTokenTransactionRequest,
2536        token_context: Vec<u8>,
2537        token_identifier: String,
2538        amount: u128,
2539        fee: u128,
2540        /// When set, this package re-shapes the wallet's token outputs instead of
2541        /// sending a payment. Publishing it returns `SwapCompleted`: rebuild the
2542        /// original send from the same prepare response and submit again.
2543        is_swap: bool,
2544    },
2545    /// One token transaction paying several recipients. Publishing it returns
2546    /// `PaymentsSent` with one payment per recipient.
2547    TokenBatch {
2548        prepare_token_transaction: crate::signer::ExternalPrepareTokenTransactionRequest,
2549        token_context: Vec<u8>,
2550        /// What the batch debits, per token. A batch spanning tokens has no
2551        /// single amount to report.
2552        totals: Vec<BatchTotal>,
2553        /// When set, this package re-shapes the wallet's token outputs instead of
2554        /// sending a payment. Publishing it returns `SwapCompleted`: rebuild the
2555        /// original send from the same prepare response and submit again.
2556        is_swap: bool,
2557    },
2558    /// A pull from a token allowance, signed through its `prepare_token_transaction` like a Token package.
2559    TokenPull {
2560        prepare_token_transaction: crate::signer::ExternalPrepareTokenTransactionRequest,
2561        pull_context: Vec<u8>,
2562        payer_address: String,
2563        token_identifier: String,
2564        receivers: Vec<PullReceiver>,
2565        amount: u128,
2566        /// When the pull stops being publishable, in Unix seconds.
2567        expiry_time: u64,
2568    },
2569}
2570
2571#[derive(Debug, Clone, Serialize, Deserialize)]
2572#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2573pub enum TransferTarget {
2574    Spark {
2575        address: String,
2576        spark_invoice: Option<String>,
2577    },
2578    Lightning {
2579        bolt11: String,
2580        lnurl_pay: Option<LnurlPayContext>,
2581        fee_policy: FeePolicy,
2582        completion_timeout_secs: Option<u32>,
2583    },
2584    CoopExit {
2585        address: String,
2586        fee_quote: SendOnchainFeeQuote,
2587        confirmation_speed: OnchainConfirmationSpeed,
2588    },
2589}
2590
2591#[derive(Debug, Clone, Serialize, Deserialize)]
2592#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2593pub struct LnurlPayContext {
2594    pub pay_request: LnurlPayRequestDetails,
2595    pub comment: Option<String>,
2596    pub success_action: Option<SuccessAction>,
2597}
2598
2599#[derive(Debug, Clone, Serialize, Deserialize)]
2600#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2601pub struct SignedTransferPackage {
2602    pub unsigned: UnsignedTransferPackage,
2603    pub signature: TransferSignature,
2604}
2605
2606#[derive(Debug, Clone, Serialize, Deserialize)]
2607#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2608pub enum TransferSignature {
2609    Transfer {
2610        signed: crate::signer::ExternalPreparedTransfer,
2611    },
2612    Token {
2613        signed: crate::signer::ExternalPreparedTokenTransaction,
2614    },
2615}
2616
2617#[derive(Debug, Clone)]
2618#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2619pub enum BuildTransferPackageOptions {
2620    BitcoinAddress {
2621        confirmation_speed: OnchainConfirmationSpeed,
2622    },
2623    Bolt11Invoice {
2624        prefer_spark: bool,
2625
2626        /// If set, publishing the package waits up to this many seconds for the
2627        /// payment to complete before returning it while still pending. If unset,
2628        /// publishing returns immediately after initiating the payment.
2629        completion_timeout_secs: Option<u32>,
2630    },
2631}
2632
2633#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2634pub struct BuildUnsignedTransferPackageRequest {
2635    pub prepare_response: PrepareSendPaymentResponse,
2636    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2637    pub options: Option<BuildTransferPackageOptions>,
2638}
2639
2640#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2641pub struct PrepareSendPaymentRequest {
2642    pub payment_request: PaymentRequest,
2643    /// The amount to send.
2644    /// Optional for payment requests with embedded amounts (e.g., Spark/Bolt11 invoices with amounts).
2645    /// Required for Spark addresses, Bitcoin addresses, and amountless invoices.
2646    /// Denominated in satoshis for Bitcoin payments, or token base units for token payments.
2647    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2648    pub amount: Option<u128>,
2649    /// Optional token identifier for token payments.
2650    /// Absence indicates that the payment is a Bitcoin payment.
2651    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2652    pub token_identifier: Option<String>,
2653    /// If provided, the payment will include a conversion step before sending the payment
2654    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2655    pub conversion_options: Option<ConversionOptions>,
2656    /// How fees are handled. See [`FeePolicy`]. Defaults to `FeesExcluded`.
2657    ///
2658    /// Ignored on cross-chain AMM-conversion sends (whether the conversion was
2659    /// explicitly requested or auto-injected by stable balance) — fees come
2660    /// out of the converted sats. Bolt11 and Bitcoin AMM-conversion sends
2661    /// still respect this field by sizing the conversion to cover fees. The
2662    /// prepare response's `fee_policy` reflects what was actually applied.
2663    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2664    pub fee_policy: Option<FeePolicy>,
2665}
2666
2667#[derive(Debug, Clone, Serialize)]
2668#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2669pub struct PrepareSendPaymentResponse {
2670    pub payment_method: SendPaymentMethod,
2671    /// The amount to be sent, denominated in satoshis for Bitcoin payments
2672    /// (including token-to-Bitcoin conversions), or token base units for token payments.
2673    /// When a conversion is present, the input amount is in
2674    /// `conversion_estimate.amount_in`.
2675    pub amount: u128,
2676    /// Optional token identifier for token payments.
2677    /// Absence indicates that the payment is a Bitcoin payment.
2678    pub token_identifier: Option<String>,
2679    /// When set, the payment will include a conversion step before sending the payment
2680    pub conversion_estimate: Option<ConversionEstimate>,
2681    /// The fee policy actually applied. May differ from the request — e.g.,
2682    /// cross-chain AMM-conversion sends are always `FeesIncluded`.
2683    pub fee_policy: FeePolicy,
2684}
2685
2686#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2687pub enum SendPaymentOptions {
2688    BitcoinAddress {
2689        /// Confirmation speed for the on-chain transaction.
2690        confirmation_speed: OnchainConfirmationSpeed,
2691    },
2692    Bolt11Invoice {
2693        prefer_spark: bool,
2694
2695        /// If set, the function will return the payment if it is still pending after this
2696        /// number of seconds. If unset, the function will return immediately after initiating the payment.
2697        completion_timeout_secs: Option<u32>,
2698    },
2699    SparkAddress {
2700        /// Can only be provided for Bitcoin payments. If set, a Spark HTLC transfer will be created.
2701        /// The receiver will need to provide the preimage to claim it.
2702        htlc_options: Option<SparkHtlcOptions>,
2703    },
2704}
2705
2706#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2707pub struct SparkHtlcOptions {
2708    /// The payment hash of the HTLC. The receiver will need to provide the associated preimage to claim it.
2709    pub payment_hash: String,
2710    /// The duration of the HTLC in seconds.
2711    /// After this time, the HTLC will be returned.
2712    pub expiry_duration_secs: u64,
2713}
2714
2715#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2716pub struct SendPaymentRequest {
2717    pub prepare_response: PrepareSendPaymentResponse,
2718    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2719    pub options: Option<SendPaymentOptions>,
2720    /// The optional idempotency key for all Spark based transfers (excludes token payments
2721    /// and cross-chain sends).
2722    /// If set, providing the same idempotency key for multiple requests will ensure that only one
2723    /// payment is made. If an idempotency key is re-used, the same payment will be returned.
2724    /// The idempotency key must be a valid UUID.
2725    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2726    pub idempotency_key: Option<String>,
2727}
2728
2729/// A single payee in a batch send.
2730#[derive(Debug, Clone)]
2731#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2732pub struct BatchRecipient {
2733    /// Spark address or Spark invoice identifying the payee.
2734    pub payment_request: String,
2735    /// Amount to send, in the base units of the asset being sent. Required
2736    /// unless `payment_request` is an invoice that carries its own amount.
2737    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2738    pub amount: Option<u128>,
2739    /// Token to send. Unset means sats, which a batch cannot send yet, so a
2740    /// plain address needs this set. An invoice that names a token does not.
2741    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2742    pub token_identifier: Option<String>,
2743}
2744
2745#[derive(Debug, Clone)]
2746#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2747pub struct PrepareSendBatchRequest {
2748    /// The payees, all paid by one transaction. They may span several tokens,
2749    /// and may mix Spark addresses with Spark invoices. Once a Spark invoice is
2750    /// among them, every recipient must be paid in the same token.
2751    pub recipients: Vec<BatchRecipient>,
2752}
2753
2754/// Where a batch recipient is paid, once prepare has decoded its payment request.
2755#[derive(Debug, Clone, Serialize, Deserialize)]
2756#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2757pub enum BatchDestination {
2758    SparkAddress {
2759        address: String,
2760    },
2761    SparkInvoice {
2762        invoice_details: SparkInvoiceDetails,
2763    },
2764}
2765
2766/// A recipient after prepare has resolved the asset and amount it is owed.
2767#[derive(Debug, Clone, Serialize, Deserialize)]
2768#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2769pub struct ResolvedBatchRecipient {
2770    pub destination: BatchDestination,
2771    /// Amount in the base units of the asset this recipient is paid in.
2772    pub amount: u128,
2773    /// The token this recipient is paid in. Unset means sats, which a batch
2774    /// cannot send yet.
2775    pub token_identifier: Option<String>,
2776}
2777
2778/// What a batch debits for one asset.
2779#[derive(Debug, Clone, Serialize, Deserialize)]
2780#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2781pub struct BatchTotal {
2782    /// The token debited. Unset means sats, which a batch cannot send yet.
2783    pub token_identifier: Option<String>,
2784    /// Amount in the asset's base units.
2785    pub amount: u128,
2786}
2787
2788#[derive(Debug, Clone, Serialize, Deserialize)]
2789#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2790pub struct PrepareSendBatchResponse {
2791    /// The payees in the order they were requested, which is the order their
2792    /// payments come back in.
2793    pub recipients: Vec<ResolvedBatchRecipient>,
2794    /// What the batch debits, one entry per distinct asset.
2795    pub totals: Vec<BatchTotal>,
2796}
2797
2798#[derive(Debug, Clone)]
2799#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2800pub struct SendBatchRequest {
2801    pub prepare_response: PrepareSendBatchResponse,
2802}
2803
2804#[derive(Debug, Clone)]
2805#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2806pub struct SendBatchResponse {
2807    /// One payment per recipient, in recipient order, all sharing a transaction
2808    /// hash.
2809    pub payments: Vec<Payment>,
2810}
2811
2812#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2813pub struct BuildUnsignedBatchPackageRequest {
2814    pub prepare_response: PrepareSendBatchResponse,
2815}
2816
2817#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2818pub struct PublishSignedTransferPackageRequest {
2819    pub signed_package: SignedTransferPackage,
2820}
2821
2822#[allow(clippy::large_enum_variant)]
2823#[derive(Debug, Clone)]
2824#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2825pub enum PublishSignedTransferPackageResponse {
2826    SwapCompleted,
2827    PaymentSent {
2828        payment: Payment,
2829    },
2830    /// Returned for a batch package: one payment per recipient, in recipient
2831    /// order.
2832    PaymentsSent {
2833        payments: Vec<Payment>,
2834    },
2835}
2836
2837#[derive(Debug, Clone, Serialize)]
2838#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2839pub struct SendPaymentResponse {
2840    pub payment: Payment,
2841}
2842
2843#[derive(Debug, Clone)]
2844#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2845pub enum PaymentDetailsFilter {
2846    Spark {
2847        /// Filter specific Spark HTLC statuses
2848        htlc_status: Option<Vec<SparkHtlcStatus>>,
2849        /// Filter conversion payments with refund information
2850        conversion_refund_needed: Option<bool>,
2851    },
2852    Token {
2853        /// Filter conversion payments with refund information
2854        conversion_refund_needed: Option<bool>,
2855        /// Filter by transaction hash
2856        tx_hash: Option<String>,
2857        /// Filter by transaction type
2858        tx_type: Option<TokenTransactionType>,
2859    },
2860    Lightning {
2861        /// Filter specific Spark HTLC statuses
2862        htlc_status: Option<Vec<SparkHtlcStatus>>,
2863    },
2864}
2865
2866/// Request to list payments with optional filters and pagination
2867#[derive(Debug, Clone, Default)]
2868#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2869pub struct ListPaymentsRequest {
2870    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2871    pub type_filter: Option<Vec<PaymentType>>,
2872    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2873    pub status_filter: Option<Vec<PaymentStatus>>,
2874    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2875    pub asset_filter: Option<AssetFilter>,
2876    /// Only include payments matching at least one of these payment details filters
2877    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2878    pub payment_details_filter: Option<Vec<PaymentDetailsFilter>>,
2879    /// Only include payments created after this timestamp (inclusive)
2880    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2881    pub from_timestamp: Option<u64>,
2882    /// Only include payments created before this timestamp (exclusive)
2883    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2884    pub to_timestamp: Option<u64>,
2885    /// Number of records to skip
2886    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2887    pub offset: Option<u32>,
2888    /// Maximum number of records to return
2889    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2890    pub limit: Option<u32>,
2891    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2892    pub sort_ascending: Option<bool>,
2893}
2894
2895/// A field of [`ListPaymentsRequest`] when listing payments filtered by asset
2896#[derive(Debug, Clone)]
2897#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
2898pub enum AssetFilter {
2899    Bitcoin,
2900    Token {
2901        /// Optional token identifier to filter by
2902        token_identifier: Option<String>,
2903    },
2904}
2905
2906impl FromStr for AssetFilter {
2907    type Err = String;
2908
2909    fn from_str(s: &str) -> Result<Self, Self::Err> {
2910        Ok(match s.to_lowercase().as_str() {
2911            "bitcoin" => AssetFilter::Bitcoin,
2912            "token" => AssetFilter::Token {
2913                token_identifier: None,
2914            },
2915            str if str.starts_with("token:") => AssetFilter::Token {
2916                token_identifier: Some(
2917                    str.split_once(':')
2918                        .ok_or(format!("Invalid asset filter '{s}'"))?
2919                        .1
2920                        .to_string(),
2921                ),
2922            },
2923            _ => return Err(format!("Invalid asset filter '{s}'")),
2924        })
2925    }
2926}
2927
2928/// Response from listing payments
2929#[derive(Debug, Clone, Serialize)]
2930#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2931pub struct ListPaymentsResponse {
2932    /// The list of payments
2933    pub payments: Vec<Payment>,
2934}
2935
2936#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2937pub struct GetPaymentRequest {
2938    pub payment_id: String,
2939}
2940
2941#[derive(Debug, Clone, Serialize)]
2942#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2943pub struct GetPaymentResponse {
2944    pub payment: Payment,
2945}
2946
2947#[cfg_attr(feature = "uniffi", uniffi::export(callback_interface))]
2948pub trait Logger: Send + Sync {
2949    fn log(&self, l: LogEntry);
2950}
2951
2952#[derive(Debug, Clone, Serialize)]
2953#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2954pub struct LogEntry {
2955    pub line: String,
2956    pub level: String,
2957    /// Identity public key of the SDK instance that emitted the line, telling
2958    /// apart the logs of several instances in one process. Unset for lines not
2959    /// emitted by an instance.
2960    pub instance_id: Option<String>,
2961}
2962
2963#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2964#[derive(Debug, Clone, Serialize, Deserialize)]
2965pub struct CheckLightningAddressRequest {
2966    pub username: String,
2967}
2968
2969#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2970#[derive(Debug, Clone, Serialize, Deserialize)]
2971pub struct RegisterLightningAddressRequest {
2972    pub username: String,
2973    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
2974    pub description: Option<String>,
2975}
2976
2977/// Authorization from the current owner granting a specific new owner the
2978/// right to take over a username. Produced by
2979/// [`BreezSdk::authorize_lightning_address_transfer`] and handed to the new
2980/// owner, who passes it to [`BreezSdk::claim_lightning_address_transfer`]. It
2981/// fully describes the transfer, so the new owner needs nothing else to claim.
2982#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2983#[derive(Debug, Clone, Serialize, Deserialize)]
2984pub struct TransferAuthorization {
2985    /// The username being handed over.
2986    pub username: String,
2987    /// The current owner's public key.
2988    pub pubkey: String,
2989    /// The current owner's signature authorizing the transfer.
2990    pub signature: String,
2991    /// The lightning-address domain the authorization is for, taken from the
2992    /// address being handed over. The signed message names this domain, so an
2993    /// authorization made for one server does not verify at another.
2994    pub domain: String,
2995    /// When the authorization was produced, in seconds since the Unix epoch.
2996    /// Covered by the signature, and valid for 10 minutes: the transferee has
2997    /// to claim within that window or the current owner authorizes again.
2998    pub timestamp: u64,
2999}
3000
3001/// Request for [`BreezSdk::authorize_lightning_address_transfer`]. Called by
3002/// the *current owner* to authorize handing their registered username over to
3003/// `transferee_pubkey`.
3004#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3005#[derive(Debug, Clone, Serialize, Deserialize)]
3006pub struct AuthorizeTransferRequest {
3007    /// The new owner's identity public key.
3008    pub transferee_pubkey: String,
3009}
3010
3011/// Request for [`BreezSdk::claim_lightning_address_transfer`]. Called by the
3012/// *new owner* to complete the takeover using the authorization produced by
3013/// the current owner.
3014#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3015#[derive(Debug, Clone, Serialize, Deserialize)]
3016pub struct ClaimTransferRequest {
3017    /// Authorization produced by the current owner via
3018    /// [`BreezSdk::authorize_lightning_address_transfer`].
3019    pub authorization: TransferAuthorization,
3020    /// Description for the address. Defaults to `"Pay to {username}@{domain}"`.
3021    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
3022    pub description: Option<String>,
3023}
3024
3025#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3026#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
3027pub struct LnurlInfo {
3028    pub url: String,
3029    pub bech32: String,
3030}
3031
3032impl LnurlInfo {
3033    pub fn new(url: String) -> Self {
3034        let bech32 =
3035            breez_sdk_common::lnurl::encode_lnurl_to_bech32(&url).unwrap_or_else(|_| url.clone());
3036        Self { url, bech32 }
3037    }
3038}
3039
3040#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3041#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
3042pub struct LightningAddressInfo {
3043    pub description: String,
3044    pub lightning_address: String,
3045    pub lnurl: LnurlInfo,
3046    pub username: String,
3047}
3048
3049impl From<RecoverLnurlPayResponse> for LightningAddressInfo {
3050    fn from(resp: RecoverLnurlPayResponse) -> Self {
3051        Self {
3052            description: resp.description,
3053            lightning_address: resp.lightning_address,
3054            lnurl: LnurlInfo::new(resp.lnurl),
3055            username: resp.username,
3056        }
3057    }
3058}
3059
3060/// Response from listing fiat currencies
3061#[derive(Debug, Clone, Serialize)]
3062#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3063pub struct ListFiatCurrenciesResponse {
3064    /// The list of fiat currencies
3065    pub currencies: Vec<FiatCurrency>,
3066}
3067
3068/// Response from listing fiat rates
3069#[derive(Debug, Clone, Serialize)]
3070#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3071pub struct ListFiatRatesResponse {
3072    /// The list of fiat rates
3073    pub rates: Vec<Rate>,
3074}
3075
3076/// The operational status of a Spark service.
3077#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3078#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
3079pub enum ServiceStatus {
3080    /// Service is fully operational.
3081    Operational,
3082    /// Service is experiencing degraded performance.
3083    Degraded,
3084    /// Service is partially unavailable.
3085    Partial,
3086    /// Service status is unknown.
3087    Unknown,
3088    /// Service is experiencing a major outage.
3089    Major,
3090}
3091
3092/// The status of the Spark network services relevant to the SDK.
3093#[derive(Debug, Clone, Serialize)]
3094#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3095pub struct SparkStatus {
3096    /// The worst status across all relevant services.
3097    pub status: ServiceStatus,
3098    /// The last time the status was updated, as a unix timestamp in seconds.
3099    pub last_updated: u64,
3100}
3101
3102pub(crate) enum WaitForPaymentIdentifier {
3103    PaymentId(String),
3104    LightningReceive { invoice: String, ssp_id: String },
3105}
3106
3107#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3108pub struct GetTokensMetadataRequest {
3109    pub token_identifiers: Vec<String>,
3110}
3111
3112#[derive(Debug, Clone, Serialize)]
3113#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3114pub struct GetTokensMetadataResponse {
3115    pub tokens_metadata: Vec<TokenMetadata>,
3116}
3117
3118#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3119pub struct SignMessageRequest {
3120    pub message: String,
3121    /// If true, the signature will be encoded in compact format instead of DER format
3122    pub compact: bool,
3123}
3124
3125#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3126pub struct SignMessageResponse {
3127    pub pubkey: String,
3128    /// The DER or compact hex encoded signature
3129    pub signature: String,
3130}
3131
3132#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3133pub struct CheckMessageRequest {
3134    /// The message that was signed
3135    pub message: String,
3136    /// The public key that signed the message
3137    pub pubkey: String,
3138    /// The DER or compact hex encoded signature
3139    pub signature: String,
3140}
3141
3142#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3143pub struct CheckMessageResponse {
3144    pub is_valid: bool,
3145}
3146
3147#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3148#[derive(Debug, Clone, Serialize)]
3149pub struct UserSettings {
3150    pub spark_private_mode_enabled: bool,
3151
3152    /// The label of the currently active stable balance token, or `None` if deactivated.
3153    pub stable_balance_active_label: Option<String>,
3154
3155    /// The hex encoded public key designated as this wallet's master identity
3156    /// key, or `None` if none is designated.
3157    pub spark_master_identity_public_key: Option<String>,
3158}
3159
3160/// Specifies how to update the wallet's Spark master identity public key.
3161#[derive(Debug, Clone)]
3162#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
3163pub enum SparkMasterIdentityPublicKey {
3164    /// Designate the holder of this public key as the wallet's master
3165    /// identity, replacing any previously designated key. Must be hex encoded
3166    /// in the 33-byte compressed form.
3167    Set { public_key: String },
3168    /// Remove the designated master identity, leaving the owner as the only
3169    /// party able to read the wallet under private mode.
3170    Unset,
3171}
3172
3173#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3174pub struct UpdateUserSettingsRequest {
3175    pub spark_private_mode_enabled: Option<bool>,
3176
3177    /// Update the active stable balance token. `None` means no change.
3178    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
3179    pub stable_balance_active_label: Option<StableBalanceActiveLabel>,
3180
3181    /// Designate or remove the wallet's master identity, a second public key
3182    /// the Spark operators accept as a reader of this wallet's balance and
3183    /// history while `spark_private_mode_enabled` is set. The master identity
3184    /// can only read: payments still require the owner's keys. `None` means no
3185    /// change.
3186    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
3187    pub spark_master_identity_public_key: Option<SparkMasterIdentityPublicKey>,
3188}
3189
3190#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3191pub struct ClaimHtlcPaymentRequest {
3192    pub preimage: String,
3193}
3194
3195#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3196pub struct ClaimHtlcPaymentResponse {
3197    pub payment: Payment,
3198}
3199
3200#[derive(Debug, Clone, Deserialize, Serialize)]
3201#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3202pub struct LnurlReceiveMetadata {
3203    pub nostr_zap_request: Option<String>,
3204    pub nostr_zap_receipt: Option<String>,
3205    pub sender_comment: Option<String>,
3206    /// The Lightning address the payment was received on. Unset for some payments received
3207    /// before the LNURL server recorded addresses.
3208    pub lightning_address: Option<String>,
3209}
3210
3211/// Mode of a manually-triggered optimization run.
3212#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
3213#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
3214pub enum OptimizationMode {
3215    /// Run until no further optimization is productive.
3216    #[default]
3217    Full,
3218    /// Execute a single round and return so the caller can drive progress.
3219    SingleRound,
3220}
3221
3222/// Request for [`BreezSdk::optimize_leaves`]. Defaults to
3223/// [`OptimizationMode::Full`].
3224#[derive(Debug, Clone, Default, Deserialize, Serialize)]
3225#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3226pub struct OptimizeLeavesRequest {
3227    /// Controls how much work the call performs before returning.
3228    pub mode: OptimizationMode,
3229}
3230
3231/// Response from a [`BreezSdk::optimize_leaves`] call.
3232#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
3233#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3234pub struct OptimizeLeavesResponse {
3235    /// The outcome of the optimization run.
3236    pub outcome: OptimizationOutcome,
3237}
3238
3239/// Outcome of a [`BreezSdk::optimize_leaves`] call.
3240///
3241/// `rounds_executed` on `Completed` refers to rounds run by *this call*.
3242/// The SDK holds no cross-call state — callers driving a `SingleRound`
3243/// loop maintain their own cumulative counter if they need one.
3244///
3245/// A `Completed { rounds_executed: 0 }` outcome means the wallet was
3246/// already optimal at call time (no swap was needed).
3247///
3248/// **`SingleRound` loop pattern**: terminate on anything that isn't
3249/// `InProgress`. `Completed` covers both the final swap of a productive
3250/// run and the "already optimal" no-op case (the latter as
3251/// `rounds_executed: 0`).
3252///
3253/// ```ignore
3254/// loop {
3255///     let request = OptimizeLeavesRequest { mode: OptimizationMode::SingleRound };
3256///     match sdk.optimize_leaves(request).await?.outcome {
3257///         OptimizationOutcome::InProgress => continue,
3258///         OptimizationOutcome::Completed { .. } => break,
3259///     }
3260/// }
3261/// ```
3262#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
3263#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
3264pub enum OptimizationOutcome {
3265    /// All planned optimization work was executed in this call.
3266    /// Returned by `Full` runs on success, and by `SingleRound` runs
3267    /// whose swap was the final one needed (the planner produced a
3268    /// single-swap plan with a convergence guarantee).
3269    /// `rounds_executed == 0` means the wallet was already optimal —
3270    /// no work was performed.
3271    Completed { rounds_executed: u32 },
3272    /// `SingleRound` only: a round ran but the planner could not
3273    /// guarantee it was the last. The caller should invoke
3274    /// `optimize_leaves` again.
3275    InProgress,
3276}
3277
3278/// A contact entry containing a name and payment identifier.
3279#[derive(Debug, Clone, Serialize, Deserialize)]
3280#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3281pub struct Contact {
3282    pub id: String,
3283    pub name: String,
3284    /// A Lightning address (user@domain).
3285    pub payment_identifier: String,
3286    pub created_at: u64,
3287    pub updated_at: u64,
3288}
3289
3290/// Request to add a new contact.
3291#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3292pub struct AddContactRequest {
3293    pub name: String,
3294    /// A Lightning address (user@domain).
3295    pub payment_identifier: String,
3296}
3297
3298/// Request to update an existing contact.
3299#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3300pub struct UpdateContactRequest {
3301    pub id: String,
3302    pub name: String,
3303    /// A Lightning address (user@domain).
3304    pub payment_identifier: String,
3305}
3306
3307/// Request to list contacts with optional pagination.
3308#[derive(Debug, Clone, Default)]
3309#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3310pub struct ListContactsRequest {
3311    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
3312    pub offset: Option<u32>,
3313    #[cfg_attr(feature = "uniffi", uniffi(default=None))]
3314    pub limit: Option<u32>,
3315}
3316
3317/// The type of event that triggers a webhook notification.
3318#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
3319#[allow(clippy::enum_variant_names)]
3320#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
3321pub enum WebhookEventType {
3322    /// Triggered when a Lightning receive operation completes.
3323    LightningReceiveFinished,
3324    /// Triggered when a Lightning send operation completes.
3325    LightningSendFinished,
3326    /// Triggered when a cooperative exit completes.
3327    CoopExitFinished,
3328    /// Triggered when a static deposit completes.
3329    StaticDepositFinished,
3330    /// An event type not yet recognized by this version of the SDK.
3331    Unknown(String),
3332}
3333
3334/// A registered webhook entry.
3335#[derive(Debug, Clone, Serialize)]
3336#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3337pub struct Webhook {
3338    /// Unique identifier for this webhook.
3339    pub id: String,
3340    /// The URL that receives webhook notifications.
3341    pub url: String,
3342    /// The event types this webhook is subscribed to.
3343    pub event_types: Vec<WebhookEventType>,
3344}
3345
3346/// Request to register a new webhook.
3347#[derive(Debug, Clone)]
3348#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3349pub struct RegisterWebhookRequest {
3350    /// The URL that will receive webhook notifications.
3351    pub url: String,
3352    /// A secret used for HMAC-SHA256 signature verification of webhook payloads.
3353    pub secret: String,
3354    /// The event types to subscribe to.
3355    pub event_types: Vec<WebhookEventType>,
3356}
3357
3358/// Response from registering a webhook.
3359#[derive(Debug, Clone, Serialize)]
3360#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3361pub struct RegisterWebhookResponse {
3362    /// The unique identifier of the newly registered webhook.
3363    pub webhook_id: String,
3364}
3365
3366/// Request to unregister an existing webhook.
3367#[derive(Debug, Clone)]
3368#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3369pub struct UnregisterWebhookRequest {
3370    /// The unique identifier of the webhook to unregister.
3371    pub webhook_id: String,
3372}
3373
3374// ===========================================================================
3375// Unilateral exit
3376// ===========================================================================
3377
3378/// A funding UTXO that pays the on-chain fees of a unilateral exit.
3379#[derive(Debug, Clone, Serialize, Deserialize)]
3380#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
3381pub enum CpfpInput {
3382    /// A P2WPKH (native segwit v0) UTXO controlled by `pubkey` (33-byte
3383    /// compressed, hex).
3384    P2wpkh {
3385        txid: String,
3386        vout: u32,
3387        value_sats: u64,
3388        pubkey: String,
3389    },
3390    /// A P2TR (taproot, key-path) UTXO. `pubkey` (x-only or compressed, hex) is
3391    /// the **internal** (untweaked, BIP86 key-path) public key whose secret signs
3392    /// the input, not the tweaked on-chain output key. The SDK applies the BIP86
3393    /// taproot tweak itself to derive the funding scriptPubKey, so passing the
3394    /// already-tweaked output key here produces a scriptPubKey that does not match
3395    /// the UTXO and the built transaction is rejected at broadcast.
3396    P2tr {
3397        txid: String,
3398        vout: u32,
3399        value_sats: u64,
3400        pubkey: String,
3401    },
3402    /// Any witness-program script, signed via a custom `CpfpSigner`. Legacy
3403    /// (non-SegWit) scripts are rejected. `signed_input_weight` (weight units)
3404    /// is an upper bound on the input's signed weight, so the fee stays exact,
3405    /// or slightly conservative if the real signature is shorter.
3406    Custom {
3407        txid: String,
3408        vout: u32,
3409        value_sats: u64,
3410        script_pubkey_hex: String,
3411        signed_input_weight: u64,
3412    },
3413}
3414
3415/// The kind of UTXO that will fund an exit's fees.
3416#[derive(Debug, Clone, Serialize, Deserialize)]
3417#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
3418pub enum CpfpFundingKind {
3419    /// Fees paid from P2WPKH (native segwit v0) UTXOs.
3420    P2wpkh,
3421    /// Fees paid from P2TR (taproot, key-path) UTXOs.
3422    P2tr,
3423    /// Fees paid from a custom witness-program script (legacy scripts are
3424    /// rejected). `script_pubkey_hex` (the funding scriptPubKey) sizes the
3425    /// fan-out output and dust; `signed_input_weight` (weight units) is an upper
3426    /// bound on the input's signed weight, so the quote stays exact or slightly
3427    /// conservative.
3428    Custom {
3429        script_pubkey_hex: String,
3430        signed_input_weight: u64,
3431    },
3432}
3433
3434/// How funding UTXOs pay a unilateral exit's CPFP children.
3435#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
3436#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
3437pub enum CpfpFundingStrategy {
3438    /// One UTXO per branch. Each child after the first spends the change of the
3439    /// one before it, so a child's inputs depend on the fee rate of every child
3440    /// above it.
3441    #[default]
3442    PerBranch,
3443    /// One UTXO per fee-bumped transaction. No child spends another's output,
3444    /// so each can be signed at any fee rate without knowing the others. Each
3445    /// child's change stays on the funding script rather than being swept.
3446    PerNode,
3447}
3448
3449/// Which leaves to recover. `All` and `RecoverableOnly` take a leaf only when
3450/// what is left of its funds exceeds what recovering it costs. That cost leaves
3451/// out the shared fan-out fee, which you also pay when you fund several
3452/// `funding.per_branch` entries from one UTXO: compare `recoverable_value_sats`
3453/// with `total_fee_sats`, or fund one UTXO per entry to avoid the fan-out.
3454#[derive(Debug, Clone, Serialize, Deserialize)]
3455#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
3456pub enum ExitLeafSelection {
3457    /// Every leaf, including the ones still in the balance. Only for when the
3458    /// operators are unreachable or refuse to serve the wallet. Its cooperative
3459    /// leaves still need the operators.
3460    All,
3461    /// The leaves that left the balance and whose recovery has not confirmed.
3462    RecoverableOnly,
3463    /// Exactly these leaves, whether or not they are worth it. A leaf whose
3464    /// recovery finished is left out, and so is a cooperative leaf too small to
3465    /// pay its fee, or one whose funds were not found.
3466    Specific { leaf_ids: Vec<String> },
3467}
3468
3469/// Where a transaction of a recovery stands: on-chain, ready to send, or
3470/// waiting for something.
3471#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3472#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
3473pub enum ExitTransactionStatus {
3474    /// Confirmed in a block, at `block_height` where the chain service reported
3475    /// one. It needs no action.
3476    ///
3477    /// A relative `csv_timelock_blocks` counts from the height of the
3478    /// transaction it spends, so this is what tells you when a child of this one
3479    /// can go out, without fetching it again.
3480    Confirmed { block_height: Option<u32> },
3481    /// Not on-chain, and nothing is holding it back. Broadcast it, with its
3482    /// `cpfp_tx_hex` where it has one.
3483    Ready,
3484    /// A transaction in `depends_on` has yet to confirm. A relative timelock
3485    /// only starts counting once it does.
3486    WaitingForDependencies,
3487    /// Every input is confirmed, but a relative timelock has yet to mature.
3488    /// `spendable_at_height` is the first block that can include this
3489    /// transaction, and is unset when the height it counts from could not be
3490    /// read from the chain.
3491    WaitingForTimelock { spendable_at_height: Option<u32> },
3492    /// The on-chain status could not be determined (the chain service errored),
3493    /// which also leaves what it is waiting for unknown. A cooperative recovery
3494    /// has this status when the SDK did not see the output it spends in a
3495    /// block, or could not check whether an earlier recovery spent that
3496    /// output. Broadcasting may fail if a conflicting transaction already
3497    /// landed.
3498    Unverified,
3499}
3500
3501/// How much one funding UTXO must hold to avoid a fan-out: for a branch of the
3502/// exit, or under `CpfpFundingStrategy::PerNode` for one transaction of it.
3503#[derive(Debug, Clone, Serialize, Deserialize)]
3504#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3505pub struct PerBranchFunding {
3506    /// The leaf whose branch this funds.
3507    pub leaf_id: String,
3508    /// Fund a UTXO of at least this many satoshis.
3509    pub funding_sats: u64,
3510}
3511
3512/// What the chain has already done to an exit's leaves, as
3513/// `prepare_recover_funds` found it. `recover_funds` builds only the steps it
3514/// does not cover.
3515#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3516#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3517pub struct ExitChainState {
3518    /// Nodes whose transaction is on-chain.
3519    pub confirmed_nodes: Vec<ConfirmedExitNode>,
3520    /// Leaves whose refund reached the chain.
3521    pub refunds: Vec<ExitRefund>,
3522    /// Leaves whose lineage was taken on-chain by a transaction the exit cannot
3523    /// continue from. Nothing further can be driven for them.
3524    pub stopped_leaf_ids: Vec<String>,
3525    /// Nodes a chain lookup could not read, so their state is unknown rather
3526    /// than absent. Transactions depending on them come back
3527    /// `ExitTransactionStatus::Unverified`.
3528    pub unverified_node_ids: Vec<String>,
3529    /// Nodes taken to be on-chain on the operators' word, the chain itself being
3530    /// unreadable. Their spend is invisible, so anything built over them risks
3531    /// double-spending an output that is already gone.
3532    pub unverifiable_confirmed_node_ids: Vec<String>,
3533}
3534
3535/// A node of the exit tree that is already on-chain.
3536#[derive(Debug, Clone, Serialize, Deserialize)]
3537#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3538pub struct ConfirmedExitNode {
3539    pub node_id: String,
3540    pub confirmed_by: ExitNodeConfirmation,
3541    /// The block it is in, where that is known. Unset for a node put in a block
3542    /// by a descendant's confirmation rather than read directly.
3543    pub block_height: Option<u32>,
3544}
3545
3546/// Which of a node's two pre-signed spends took it on-chain.
3547#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
3548#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
3549pub enum ExitNodeConfirmation {
3550    /// The CPFP transaction, whose fee a child paid.
3551    Cpfp,
3552    /// The direct transaction, which pays its own fee. A leaf that went out this
3553    /// way is refunded by its direct refund transaction.
3554    Direct,
3555}
3556
3557/// A leaf's refund as the chain shows it.
3558#[derive(Debug, Clone, Serialize, Deserialize)]
3559#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3560pub struct ExitRefund {
3561    pub leaf_id: String,
3562    pub state: ExitRefundState,
3563}
3564
3565#[derive(Debug, Clone, Serialize, Deserialize)]
3566#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
3567pub enum ExitRefundState {
3568    /// On-chain with its output still there, which is what the sweep pulls from.
3569    /// A sweep sitting unconfirmed in the mempool leaves the refund here, so
3570    /// that sweep is rebuilt rather than dropped.
3571    OnChain {
3572        tx_hex: String,
3573        vout: u32,
3574        value_sats: u64,
3575        block_height: Option<u32>,
3576    },
3577    /// Spent by a confirmed transaction: the sweep landed.
3578    Swept,
3579}
3580
3581/// Result of `export_unilateral_exit_state`: a self-contained copy of the
3582/// wallet's exit state, ready to be stored outside the wallet.
3583#[derive(Debug, Clone, Serialize, Deserialize)]
3584#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3585pub struct ExportUnilateralExitStateResponse {
3586    /// The serialized exit state, to be handed back to
3587    /// `import_unilateral_exit_state` unmodified.
3588    pub exit_state: String,
3589}
3590
3591/// Request for `import_unilateral_exit_state`.
3592#[derive(Debug, Clone, Serialize, Deserialize)]
3593#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3594pub struct ImportUnilateralExitStateRequest {
3595    /// An exit state as returned by `export_unilateral_exit_state`.
3596    pub exit_state: String,
3597}
3598
3599/// Result of `import_unilateral_exit_state`.
3600#[derive(Debug, Clone, Serialize, Deserialize)]
3601#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3602pub struct ImportUnilateralExitStateResponse {
3603    /// Leaves merged into the wallet's exit state, whether or not their exit
3604    /// data was taken with them.
3605    pub imported_leaves: u32,
3606    /// Leaves left out because the exit state does not record this wallet as
3607    /// their owner.
3608    pub skipped_foreign_leaves: u32,
3609    /// Leaves left out because their exit data disagrees with what the wallet
3610    /// already holds, so none of it could be trusted. The wallet is left without
3611    /// these leaves.
3612    pub skipped_conflicting_leaves: u32,
3613    /// Leaves the wallet holds whose imported exit data was left out: it is
3614    /// incomplete, the wallet's own copy can already back an exit, or the leaf
3615    /// was named more than once. The leaf itself is in the wallet either way.
3616    pub skipped_chains: u32,
3617}
3618
3619// ===========================================================================
3620// Recover funds
3621// ===========================================================================
3622
3623/// Request for `prepare_recover_funds`, which returns the recovery quote.
3624#[derive(Debug, Clone)]
3625#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3626pub struct PrepareRecoverFundsRequest {
3627    /// Target fee rate in sat/vByte, applied to every transaction the recovery
3628    /// builds. Below the network's relay minimum, zero included, nodes do not
3629    /// relay the transactions: they have to reach a miner another way.
3630    pub fee_rate_sat_per_vbyte: u64,
3631    /// The kind of UTXO that will fund a unilateral exit. Needed when a
3632    /// transaction of a unilateral exit in the quote has a CPFP child.
3633    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
3634    pub funding_kind: Option<CpfpFundingKind>,
3635    /// How the funding UTXOs pay a unilateral exit's CPFP children. Unset funds
3636    /// one UTXO per branch.
3637    #[cfg_attr(feature = "uniffi", uniffi(default = None))]
3638    pub funding_strategy: Option<CpfpFundingStrategy>,
3639    /// The Bitcoin address the recovered funds are sent to.
3640    pub destination: String,
3641    pub selection: ExitLeafSelection,
3642}
3643
3644/// Response from `prepare_recover_funds`: which leaves would be recovered and
3645/// how, the exact fee at the requested rate, and how much to fund.
3646#[derive(Debug, Clone, Serialize, Deserialize)]
3647#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3648pub struct PrepareRecoverFundsResponse {
3649    pub leaves: Vec<RecoverFundsLeaf>,
3650    /// The leaves the selection covers that are not in `leaves`, each with the
3651    /// reason. A leaf whose recovery already finished is not listed.
3652    pub skipped: Vec<SkippedLeaf>,
3653    /// Total value of the selected leaves, in satoshis.
3654    pub recoverable_value_sats: u64,
3655    /// `cooperative_fee_sats + cpfp_fee_sats + fanout_fee_sats +
3656    /// refund_fee_sats + sweep_fee_sats`.
3657    pub total_fee_sats: u64,
3658    /// Paid from the cooperative leaves' own value. Each leaf has two fees: the
3659    /// one of the transaction the operators broadcast to move it on-chain, which
3660    /// is already paid, and the one of its recovery transaction.
3661    pub cooperative_fee_sats: u64,
3662    /// Paid by the CPFP children, from your funding UTXOs.
3663    pub cpfp_fee_sats: u64,
3664    /// Paid by the fan-out, from your funding UTXO. Funding one UTXO per
3665    /// `funding.per_branch` entry avoids it.
3666    pub fanout_fee_sats: u64,
3667    /// Paid from the unilateral leaves' own value before the sweep: each leaf's
3668    /// value less what its refund passes to the sweep. The output of a direct
3669    /// transaction, the Spark operators' version of a leaf's transaction, is its
3670    /// input less its fee, and a refund received with a watchtower fee ladder
3671    /// pays its own fee. For a refund that is not on-chain yet, the SDK counts
3672    /// the refund it expects: the operators can broadcast one with another fee.
3673    pub refund_fee_sats: u64,
3674    /// Paid by the final sweep, off the value it moves.
3675    pub sweep_fee_sats: u64,
3676    /// What to fund the unilateral exit with. Unset when nothing needs funding:
3677    /// no transaction of the recovery has a CPFP child.
3678    pub funding: Option<RecoveryFunding>,
3679    pub fee_rate_sat_per_vbyte: u64,
3680    pub destination: String,
3681    /// What the chain has already done to the unilateral leaves, read while
3682    /// preparing.
3683    pub exit_chain_state: ExitChainState,
3684}
3685
3686/// How much to fund a unilateral exit with.
3687#[derive(Debug, Clone, Serialize, Deserialize)]
3688#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3689pub struct RecoveryFunding {
3690    /// Fund a single UTXO of at least this many satoshis, which a fan-out
3691    /// splits over the `per_branch` entries.
3692    pub single_utxo_sats: u64,
3693    /// To skip the fan-out, fund one UTXO per entry of at least its amount, in
3694    /// any order. An entry is a branch, or under `CpfpFundingStrategy::PerNode`
3695    /// one transaction of a branch, so a leaf can have several.
3696    pub per_branch: Vec<PerBranchFunding>,
3697    /// The strategy the amounts were computed for, which `recover_funds` builds
3698    /// with.
3699    #[serde(default)]
3700    pub strategy: CpfpFundingStrategy,
3701}
3702
3703/// A leaf the selection covers that `prepare_recover_funds` leaves out of the
3704/// quote.
3705#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3706#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3707pub struct SkippedLeaf {
3708    pub leaf_id: String,
3709    pub value_sats: u64,
3710    pub reason: SkippedLeafReason,
3711}
3712
3713/// Why `prepare_recover_funds` leaves a leaf out of the quote.
3714#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3715#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
3716pub enum SkippedLeafReason {
3717    /// At the quote's fee rate, recovering the leaf costs at least what is left
3718    /// of its funds: `value_sats`, less the fee of each direct transaction in its
3719    /// exit and the fee the operators paid out of the leaf when they broadcast
3720    /// its refund. A low enough fee rate brings the leaf back into the quote.
3721    FeeExceedsValue,
3722    /// The SDK read the chain and found no output holding the leaf's funds.
3723    FundsNotFound,
3724    /// The SDK could not look up where the leaf's funds are. It looks again the
3725    /// next time you prepare.
3726    Unverified,
3727    /// The SDK needs data from the Spark operators to find the leaf's funds,
3728    /// and could not reach them or they were too busy. It tries again the next
3729    /// time you prepare.
3730    OperatorsUnavailable,
3731    /// No recovery can be built for the leaf as it stands. `message` says why.
3732    NotRecoverable { message: String },
3733}
3734
3735/// A leaf selected for recovery.
3736#[derive(Debug, Clone, Serialize, Deserialize)]
3737#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3738pub struct RecoverFundsLeaf {
3739    pub leaf_id: String,
3740    pub value_sats: u64,
3741    pub method: RecoveryMethod,
3742}
3743
3744/// How a leaf's funds are recovered.
3745#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3746#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
3747pub enum RecoveryMethod {
3748    /// One transaction the operators co-sign, paid from the leaf's value. No
3749    /// funding, no timelock.
3750    Cooperative,
3751    /// The leaf's own pre-signed transactions, fee-bumped from funding you
3752    /// supply, with timelock waits.
3753    Unilateral,
3754}
3755
3756/// Request for `recover_funds`: a `prepare_recover_funds` quote plus the
3757/// funding UTXOs that pay its fees.
3758#[derive(Debug, Clone)]
3759#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3760pub struct RecoverFundsRequest {
3761    pub prepared: PrepareRecoverFundsResponse,
3762    /// UTXOs that meet the quote's `funding`: one of at least `single_utxo_sats`,
3763    /// or one per `per_branch` entry. Not needed when the quote has no `funding`.
3764    #[cfg_attr(feature = "uniffi", uniffi(default = []))]
3765    pub funding_inputs: Vec<CpfpInput>,
3766}
3767
3768/// Result of `recover_funds`: every signed transaction of the recovery, and its
3769/// totals. The totals mean what they do on the quote, for the leaves in
3770/// `leaves`: the ones in `failed` add nothing to them.
3771#[derive(Debug, Clone, Serialize, Deserialize)]
3772#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3773pub struct RecoverFundsResponse {
3774    pub recoverable_value_sats: u64,
3775    pub total_fee_sats: u64,
3776    pub cooperative_fee_sats: u64,
3777    pub cpfp_fee_sats: u64,
3778    pub fanout_fee_sats: u64,
3779    pub refund_fee_sats: u64,
3780    pub sweep_fee_sats: u64,
3781    pub leaves: Vec<RecoverFundsLeaf>,
3782    /// Cooperative leaves whose recovery could not be produced, and why. No
3783    /// transaction depends on them.
3784    pub failed: Vec<CooperativeRecoveryFailure>,
3785    /// Every transaction, cooperative ones included, in broadcast order.
3786    pub transactions: Vec<RecoveryTransaction>,
3787    /// The funding UTXOs this recovery was built from, as you supplied them.
3788    /// Hand them back when you build it again and they are followed to whatever
3789    /// they have since become.
3790    pub funding_inputs: Vec<CpfpInput>,
3791    pub fee_rate_sat_per_vbyte: u64,
3792    pub destination: String,
3793}
3794
3795/// A cooperative recovery that could not be produced.
3796#[derive(Debug, Clone, Serialize, Deserialize)]
3797#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3798pub struct CooperativeRecoveryFailure {
3799    pub leaf_id: String,
3800    /// The output the recovery would have spent: the on-chain output where the
3801    /// SDK found the leaf's funds, or the one it assumed to hold them.
3802    pub output_txid: String,
3803    pub output_vout: u32,
3804    pub error: CooperativeRecoveryError,
3805}
3806
3807/// One transaction of a recovery, with everything needed to order and
3808/// broadcast it.
3809#[derive(Debug, Clone, Serialize, Deserialize)]
3810#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3811pub struct RecoveryTransaction {
3812    pub kind: RecoveryTxKind,
3813    /// The tree node it belongs to (the leaf, for `Cooperative`). Unset for
3814    /// `FanOut` and `Sweep`.
3815    pub node_id: Option<String>,
3816    pub txid: String,
3817    pub tx_hex: String,
3818    /// The signed CPFP child to broadcast alongside `tx_hex` as a package. Unset
3819    /// when there is no anchor to bump, and for a `Confirmed` step.
3820    pub cpfp_tx_hex: Option<String>,
3821    /// Relative CSV timelock, in blocks, that must mature on the spent input
3822    /// before this transaction can confirm. Unset when there is no timelock.
3823    pub csv_timelock_blocks: Option<u32>,
3824    /// Txids of other entries in this list that must be confirmed before this
3825    /// one can be broadcast.
3826    pub depends_on: Vec<String>,
3827    pub status: ExitTransactionStatus,
3828}
3829
3830/// The role of a transaction in a recovery.
3831#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3832#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
3833pub enum RecoveryTxKind {
3834    /// Spends the on-chain output holding a leaf's funds, co-signed by the
3835    /// operators. Pays its fee from the leaf's value: no CPFP child, no
3836    /// dependencies, no timelock.
3837    Cooperative,
3838    /// Splits the caller's funding into one output per `funding.per_branch`
3839    /// entry: per branch, or under `CpfpFundingStrategy::PerNode` per
3840    /// transaction. Present only when the funding couldn't be matched
3841    /// one-to-one to the entries.
3842    FanOut,
3843    /// A tree node transaction (root, intermediate, or leaf node).
3844    Node,
3845    /// A leaf's refund transaction.
3846    Refund,
3847    /// The final transaction sweeping all refund outputs to the destination.
3848    Sweep,
3849}
3850
3851/// Request for `check_recover_funds`: the recovery you kept from a previous
3852/// `recover_funds`, as you last stored it.
3853#[derive(Debug, Clone)]
3854#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3855pub struct CheckRecoverFundsRequest {
3856    pub recovery: RecoverFundsResponse,
3857}
3858
3859/// Result of `check_recover_funds`: the same recovery, read back against the
3860/// chain.
3861#[derive(Debug, Clone)]
3862#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
3863pub struct CheckRecoverFundsResponse {
3864    /// Store it in place of the one you passed in.
3865    pub recovery: RecoverFundsResponse,
3866    pub verdict: RecoveryVerdict,
3867}
3868
3869/// What to do with a recovery that has been read back against the chain.
3870#[derive(Debug, Clone, Serialize, Deserialize)]
3871#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
3872pub enum RecoveryVerdict {
3873    /// The recovery can still be finished as it stands. Broadcast the `Ready`
3874    /// transactions.
3875    Valid,
3876    /// Every transaction is confirmed. The leaves in `failed` are not part of
3877    /// the recovery.
3878    Done,
3879    /// The recovery cannot be finished as it stands. Prepare and recover again.
3880    Redo { reason: RecoveryRedoReason },
3881}
3882
3883/// Why a recovery has to be built again.
3884#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
3885#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
3886pub enum RecoveryRedoReason {
3887    /// The chain no longer matches the recovery: something that is not one of
3888    /// its own transactions took an outpoint it still needs. A different
3889    /// refund, a fee bump from elsewhere, or funding spent on something else
3890    /// all land here.
3891    OnChainStateDiverged,
3892}
3893
3894#[cfg(test)]
3895mod tests {
3896    use super::*;
3897    use crate::{ConversionStatus, PaymentMethod};
3898    use macros::test_all;
3899
3900    #[cfg(feature = "browser-tests")]
3901    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
3902
3903    #[test_all]
3904    fn seed_debug_redacts_secrets() {
3905        let mnemonic = Seed::Mnemonic {
3906            mnemonic: "abandon ability able".to_string(),
3907            passphrase: Some("hunter2".to_string()),
3908        };
3909        let rendered = format!("{mnemonic:?}");
3910        assert!(!rendered.contains("abandon"), "{rendered}");
3911        assert!(!rendered.contains("hunter2"), "{rendered}");
3912
3913        let entropy = format!("{:?}", Seed::Entropy(vec![0xab; 16]));
3914        assert!(!entropy.contains("171"), "{entropy}");
3915    }
3916
3917    #[test_all]
3918    fn debug_fmt_redacts_the_preimage_of_a_bolt11_receive() {
3919        let preimage = "ab".repeat(32);
3920        let method = ReceivePaymentMethod::Bolt11Invoice {
3921            description: "test".to_string(),
3922            amount_sats: Some(1_000),
3923            expiry_secs: None,
3924            payment_hash: None,
3925            receiver_identity_public_key: None,
3926            preimage: Some(preimage.clone()),
3927        };
3928
3929        let written = format!("{method:?}");
3930
3931        assert!(
3932            written.contains(r#"preimage: Some("<redacted>")"#),
3933            "{written}"
3934        );
3935        assert!(!written.contains(&preimage), "{written}");
3936    }
3937
3938    #[test_all]
3939    fn payment_link_types_map_to_bridge_from_cash_app() {
3940        let route = crate::CrossChainRoutePair {
3941            provider: crate::CrossChainProvider::Orchestra,
3942            chain: "base".to_string(),
3943            chain_id: None,
3944            asset: "USDC".to_string(),
3945            contract_address: None,
3946            decimals: 6,
3947            exact_out_eligible: false,
3948            accepted_assets: Vec::new(),
3949            delivery_methods: Vec::new(),
3950        };
3951        let request: BridgeFromCashAppRequest = PreparePaymentLinkRequest {
3952            address: "0xabc".to_string(),
3953            route: route.clone(),
3954            amount: 1_000_000,
3955            fee_policy: Some(FeePolicy::FeesIncluded),
3956            max_slippage_bps: Some(50),
3957        }
3958        .into();
3959        assert_eq!(request.address, "0xabc");
3960        assert_eq!(request.route, route);
3961        assert_eq!(request.amount, 1_000_000);
3962        assert_eq!(request.fee_policy, Some(FeePolicy::FeesIncluded));
3963        assert_eq!(request.max_slippage_bps, Some(50));
3964
3965        let response: PreparePaymentLinkResponse = BridgeFromCashAppResponse {
3966            url: "https://cash.app/launch/lightning/lnbc1".to_string(),
3967            amount_sats: 1_500,
3968            estimated_out: 990_000,
3969            asset: "USDC".to_string(),
3970            service_fee_amount: 10_000,
3971            service_fee_asset: Some("USDC".to_string()),
3972            service_fee_asset_decimals: Some(6),
3973            expires_at: 1_791_374_400,
3974        }
3975        .into();
3976        assert_eq!(response.url, "https://cash.app/launch/lightning/lnbc1");
3977        assert_eq!(response.amount_sats, 1_500);
3978        assert_eq!(response.estimated_out, 990_000);
3979        assert_eq!(response.asset, "USDC");
3980        assert_eq!(response.service_fee_amount, 10_000);
3981        assert_eq!(response.service_fee_asset.as_deref(), Some("USDC"));
3982        assert_eq!(response.service_fee_asset_decimals, Some(6));
3983        assert_eq!(response.expires_at, "2026-10-07T12:00:00Z");
3984    }
3985
3986    fn spark_payment(conversion_info: Option<ConversionInfo>) -> Payment {
3987        Payment {
3988            id: "pmt".to_string(),
3989            payment_type: PaymentType::Receive,
3990            status: PaymentStatus::Completed,
3991            amount: 1_000,
3992            fees: 0,
3993            timestamp: 0,
3994            method: PaymentMethod::Spark,
3995            details: Some(PaymentDetails::Spark {
3996                invoice_details: None,
3997                htlc_details: None,
3998                conversion_info,
3999            }),
4000            conversion_details: None,
4001        }
4002    }
4003
4004    fn amm_info() -> ConversionInfo {
4005        ConversionInfo::Amm {
4006            pool_id: "pool".to_string(),
4007            conversion_id: "conv".to_string(),
4008            status: ConversionStatus::Completed,
4009            fee: None,
4010            purpose: None,
4011            amount_adjustment: None,
4012            degradation: None,
4013        }
4014    }
4015
4016    fn orchestra_info() -> ConversionInfo {
4017        ConversionInfo::Orchestra {
4018            order_id: "ord".to_string(),
4019            quote_id: "q".to_string(),
4020            read_token: None,
4021            chain: "base".to_string(),
4022            chain_id: Some("8453".to_string()),
4023            asset: "USDC".to_string(),
4024            recipient_address: "sp1rcv".to_string(),
4025            asset_amount_in: Some(1_000_000),
4026            estimated_out: 990_000,
4027            delivered_amount: Some(990_000),
4028            external_tx_hash: None,
4029            status: ConversionStatus::Completed,
4030            fee_amount: Some(10_000),
4031            service_fee_amount: None,
4032            service_fee_asset: None,
4033            service_fee_asset_decimals: None,
4034            asset_decimals: 6,
4035            asset_contract: None,
4036        }
4037    }
4038
4039    #[test_all]
4040    fn only_amm_legs_are_conversion_children() {
4041        // The AMM settles a conversion as its own payments, and those are the
4042        // ones the event middleware keeps to itself.
4043        assert!(spark_payment(Some(amm_info())).is_conversion_child());
4044
4045        // A cross-chain conversion annotates the payment the user made or
4046        // received. Treating it as a child would swallow its events, which on
4047        // receive is the only signal the funds arrived.
4048        assert!(!spark_payment(Some(orchestra_info())).is_conversion_child());
4049
4050        assert!(!spark_payment(None).is_conversion_child());
4051    }
4052
4053    #[test_all]
4054    fn claim_error_stored_with_a_fee_and_rate_still_loads() {
4055        let stored = r#"{"MaxDepositClaimFeeExceeded":{"tx":"tx","vout":1,
4056            "max_fee":{"Rate":{"sat_per_vbyte":2}},"required_fee_sats":500,
4057            "required_fee_rate_sat_per_vbyte":6}}"#;
4058        let error: crate::DepositClaimError = serde_json::from_str(stored).unwrap();
4059        assert_eq!(
4060            error,
4061            crate::DepositClaimError::MaxDepositClaimFeeExceeded {
4062                tx: "tx".to_string(),
4063                vout: 1,
4064                max_fee_sats: None,
4065                required_fee_sats: 500,
4066            }
4067        );
4068    }
4069
4070    #[test_all]
4071    fn max_fee_stored_without_a_proportional_part_still_loads() {
4072        for (stored, onchain) in [
4073            (
4074                r#"{"Fixed":{"amount":500}}"#,
4075                MaxOnchainFee::Fixed { amount: 500 },
4076            ),
4077            (
4078                r#"{"Rate":{"sat_per_vbyte":12}}"#,
4079                MaxOnchainFee::Rate { sat_per_vbyte: 12 },
4080            ),
4081            (
4082                r#"{"NetworkRecommended":{"leeway_sat_per_vbyte":3}}"#,
4083                MaxOnchainFee::NetworkRecommended {
4084                    leeway_sat_per_vbyte: 3,
4085                },
4086            ),
4087        ] {
4088            let max_fee: MaxFee = serde_json::from_str(stored).unwrap();
4089            assert_eq!(
4090                max_fee,
4091                MaxFee {
4092                    onchain,
4093                    proportional_ppm: None,
4094                }
4095            );
4096        }
4097    }
4098
4099    #[test_all]
4100    fn max_fee_is_stored_in_its_flat_shape() {
4101        let max_fee = MaxFee {
4102            onchain: MaxOnchainFee::Fixed { amount: 500 },
4103            proportional_ppm: Some(1_000),
4104        };
4105        let stored = serde_json::to_string(&max_fee).unwrap();
4106        assert_eq!(
4107            stored,
4108            r#"{"Fixed":{"amount":500,"proportional_ppm":1000}}"#
4109        );
4110        assert_eq!(serde_json::from_str::<MaxFee>(&stored).unwrap(), max_fee);
4111        let without_ppm = MaxFee {
4112            onchain: MaxOnchainFee::Rate { sat_per_vbyte: 2 },
4113            proportional_ppm: None,
4114        };
4115        assert_eq!(
4116            serde_json::to_string(&without_ppm).unwrap(),
4117            r#"{"Rate":{"sat_per_vbyte":2}}"#
4118        );
4119    }
4120
4121    #[test_all]
4122    fn recovery_funding_without_a_strategy_reads_as_per_branch() {
4123        // A quote is held while its funding is gathered, so one stored before the
4124        // field existed still has to read back, as the funding it always was.
4125        let stored =
4126            r#"{"single_utxo_sats":5000,"per_branch":[{"leaf_id":"a","funding_sats":5000}]}"#;
4127        let funding: RecoveryFunding = serde_json::from_str(stored).unwrap();
4128        assert_eq!(funding.strategy, CpfpFundingStrategy::PerBranch);
4129
4130        let per_node = RecoveryFunding {
4131            strategy: CpfpFundingStrategy::PerNode,
4132            ..funding
4133        };
4134        let round_trip: RecoveryFunding =
4135            serde_json::from_str(&serde_json::to_string(&per_node).unwrap()).unwrap();
4136        assert_eq!(round_trip.strategy, CpfpFundingStrategy::PerNode);
4137    }
4138}