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