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}