Skip to main content

breez_sdk_spark/models/
mod.rs

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