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