breez_sdk_liquid/
model.rs

1use anyhow::{anyhow, bail, Result};
2use bitcoin::{bip32, ScriptBuf};
3use boltz_client::{
4    boltz::{ChainPair, BOLTZ_MAINNET_URL_V2, BOLTZ_REGTEST, BOLTZ_TESTNET_URL_V2},
5    network::{BitcoinChain, Chain, LiquidChain},
6    swaps::boltz::{
7        CreateChainResponse, CreateReverseResponse, CreateSubmarineResponse, Leaf, Side, SwapTree,
8    },
9    BtcSwapScript, Keypair, LBtcSwapScript,
10};
11use derivative::Derivative;
12use elements::AssetId;
13use lwk_wollet::ElementsNetwork;
14use maybe_sync::{MaybeSend, MaybeSync};
15use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef};
16use rusqlite::ToSql;
17use sdk_common::prelude::*;
18use sdk_common::utils::Arc;
19use sdk_common::{bitcoin::hashes::hex::ToHex, lightning_with_bolt12::offers::offer::Offer};
20use serde::{Deserialize, Serialize};
21use std::cmp::PartialEq;
22use std::path::PathBuf;
23use std::str::FromStr;
24use strum_macros::{Display, EnumString};
25
26use crate::receive_swap::DEFAULT_ZERO_CONF_MAX_SAT;
27use crate::utils;
28use crate::{
29    bitcoin,
30    chain::{bitcoin::BitcoinChainService, liquid::LiquidChainService},
31    elements,
32    error::{PaymentError, SdkError, SdkResult},
33};
34use crate::{
35    chain::bitcoin::esplora::EsploraBitcoinChainService,
36    chain::liquid::esplora::EsploraLiquidChainService, prelude::DEFAULT_EXTERNAL_INPUT_PARSERS,
37};
38
39// Uses f64 for the maximum precision when converting between units
40pub const LIQUID_FEE_RATE_SAT_PER_VBYTE: f64 = 0.1;
41pub const LIQUID_FEE_RATE_MSAT_PER_VBYTE: f32 = (LIQUID_FEE_RATE_SAT_PER_VBYTE * 1000.0) as f32;
42pub const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology";
43pub const BREEZ_LIQUID_ESPLORA_URL: &str = "https://lq1.breez.technology/liquid/api";
44
45const SIDESWAP_API_KEY: &str = "97fb6a1dfa37ee6656af92ef79675cc03b8ac4c52e04655f41edbd5af888dcc2";
46
47#[derive(Clone, Debug, Serialize)]
48pub enum BlockchainExplorer {
49    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
50    Electrum { url: String },
51    Esplora {
52        url: String,
53        /// Whether or not to use the "waterfalls" extension
54        use_waterfalls: bool,
55    },
56}
57
58/// Configuration for the Liquid SDK
59#[derive(Clone, Debug, Serialize)]
60pub struct Config {
61    pub liquid_explorer: BlockchainExplorer,
62    pub bitcoin_explorer: BlockchainExplorer,
63    /// Directory in which the DB and log files are stored.
64    ///
65    /// Prefix can be a relative or absolute path to this directory.
66    pub working_dir: String,
67    pub network: LiquidNetwork,
68    /// Send payment timeout. See [LiquidSdk::send_payment](crate::sdk::LiquidSdk::send_payment)
69    pub payment_timeout_sec: u64,
70    /// The url of the real-time sync service. Defaults to [BREEZ_SYNC_SERVICE_URL]
71    /// Setting this field to `None` will disable the service
72    pub sync_service_url: Option<String>,
73    /// Maximum amount in satoshi to accept zero-conf payments with
74    /// Defaults to [DEFAULT_ZERO_CONF_MAX_SAT]
75    pub zero_conf_max_amount_sat: Option<u64>,
76    /// The Breez API key used for making requests to the sync service
77    pub breez_api_key: Option<String>,
78    /// A set of external input parsers that are used by [LiquidSdk::parse](crate::sdk::LiquidSdk::parse) when the input
79    /// is not recognized. See [ExternalInputParser] for more details on how to configure
80    /// external parsing.
81    pub external_input_parsers: Option<Vec<ExternalInputParser>>,
82    /// The SDK includes some default external input parsers
83    /// ([DEFAULT_EXTERNAL_INPUT_PARSERS](crate::sdk::DEFAULT_EXTERNAL_INPUT_PARSERS)).
84    /// Set this to false in order to prevent their use.
85    pub use_default_external_input_parsers: bool,
86    /// For payments where the onchain fees can only be estimated on creation, this can be used
87    /// in order to automatically allow slightly more expensive fees. If the actual fee rate ends up
88    /// being above the sum of the initial estimate and this leeway, the payment will require
89    /// user fee acceptance. See [WaitingFeeAcceptance](PaymentState::WaitingFeeAcceptance).
90    ///
91    /// Defaults to zero.
92    pub onchain_fee_rate_leeway_sat_per_vbyte: Option<u32>,
93    /// A set of asset metadata used by [LiquidSdk::parse](crate::sdk::LiquidSdk::parse) when the input is a
94    /// [LiquidAddressData] and the [asset_id](LiquidAddressData::asset_id) differs from the Liquid Bitcoin asset.
95    /// See [AssetMetadata] for more details on how define asset metadata.
96    /// By default the asset metadata for Liquid Bitcoin and Tether USD are included.
97    pub asset_metadata: Option<Vec<AssetMetadata>>,
98    /// The SideSwap API key used for making requests to the SideSwap payjoin service
99    pub sideswap_api_key: Option<String>,
100}
101
102impl Config {
103    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
104    pub fn mainnet(breez_api_key: Option<String>) -> Self {
105        Config {
106            liquid_explorer: BlockchainExplorer::Electrum {
107                url: "elements-mainnet.breez.technology:50002".to_string(),
108            },
109            bitcoin_explorer: BlockchainExplorer::Electrum {
110                url: "bitcoin-mainnet.blockstream.info:50002".to_string(),
111            },
112            working_dir: ".".to_string(),
113            network: LiquidNetwork::Mainnet,
114            payment_timeout_sec: 15,
115            sync_service_url: Some(BREEZ_SYNC_SERVICE_URL.to_string()),
116            zero_conf_max_amount_sat: None,
117            breez_api_key,
118            external_input_parsers: None,
119            use_default_external_input_parsers: true,
120            onchain_fee_rate_leeway_sat_per_vbyte: None,
121            asset_metadata: None,
122            sideswap_api_key: Some(SIDESWAP_API_KEY.to_string()),
123        }
124    }
125
126    pub fn mainnet_esplora(breez_api_key: Option<String>) -> Self {
127        Config {
128            liquid_explorer: BlockchainExplorer::Esplora {
129                url: BREEZ_LIQUID_ESPLORA_URL.to_string(),
130                use_waterfalls: true,
131            },
132            bitcoin_explorer: BlockchainExplorer::Esplora {
133                url: "https://blockstream.info/api/".to_string(),
134                use_waterfalls: false,
135            },
136            working_dir: ".".to_string(),
137            network: LiquidNetwork::Mainnet,
138            payment_timeout_sec: 15,
139            sync_service_url: Some(BREEZ_SYNC_SERVICE_URL.to_string()),
140            zero_conf_max_amount_sat: None,
141            breez_api_key,
142            external_input_parsers: None,
143            use_default_external_input_parsers: true,
144            onchain_fee_rate_leeway_sat_per_vbyte: None,
145            asset_metadata: None,
146            sideswap_api_key: Some(SIDESWAP_API_KEY.to_string()),
147        }
148    }
149
150    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
151    pub fn testnet(breez_api_key: Option<String>) -> Self {
152        Config {
153            liquid_explorer: BlockchainExplorer::Electrum {
154                url: "elements-testnet.blockstream.info:50002".to_string(),
155            },
156            bitcoin_explorer: BlockchainExplorer::Electrum {
157                url: "bitcoin-testnet.blockstream.info:50002".to_string(),
158            },
159            working_dir: ".".to_string(),
160            network: LiquidNetwork::Testnet,
161            payment_timeout_sec: 15,
162            sync_service_url: Some(BREEZ_SYNC_SERVICE_URL.to_string()),
163            zero_conf_max_amount_sat: None,
164            breez_api_key,
165            external_input_parsers: None,
166            use_default_external_input_parsers: true,
167            onchain_fee_rate_leeway_sat_per_vbyte: None,
168            asset_metadata: None,
169            sideswap_api_key: Some(SIDESWAP_API_KEY.to_string()),
170        }
171    }
172
173    pub fn testnet_esplora(breez_api_key: Option<String>) -> Self {
174        Config {
175            liquid_explorer: BlockchainExplorer::Esplora {
176                url: "https://blockstream.info/liquidtestnet/api".to_string(),
177                use_waterfalls: false,
178            },
179            bitcoin_explorer: BlockchainExplorer::Esplora {
180                url: "https://blockstream.info/testnet/api/".to_string(),
181                use_waterfalls: false,
182            },
183            working_dir: ".".to_string(),
184            network: LiquidNetwork::Testnet,
185            payment_timeout_sec: 15,
186            sync_service_url: Some(BREEZ_SYNC_SERVICE_URL.to_string()),
187            zero_conf_max_amount_sat: None,
188            breez_api_key,
189            external_input_parsers: None,
190            use_default_external_input_parsers: true,
191            onchain_fee_rate_leeway_sat_per_vbyte: None,
192            asset_metadata: None,
193            sideswap_api_key: Some(SIDESWAP_API_KEY.to_string()),
194        }
195    }
196
197    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
198    pub fn regtest() -> Self {
199        Config {
200            liquid_explorer: BlockchainExplorer::Electrum {
201                url: "localhost:19002".to_string(),
202            },
203            bitcoin_explorer: BlockchainExplorer::Electrum {
204                url: "localhost:19001".to_string(),
205            },
206            working_dir: ".".to_string(),
207            network: LiquidNetwork::Regtest,
208            payment_timeout_sec: 15,
209            sync_service_url: Some("http://localhost:8088".to_string()),
210            zero_conf_max_amount_sat: None,
211            breez_api_key: None,
212            external_input_parsers: None,
213            use_default_external_input_parsers: true,
214            onchain_fee_rate_leeway_sat_per_vbyte: None,
215            asset_metadata: None,
216            sideswap_api_key: None,
217        }
218    }
219
220    pub fn regtest_esplora() -> Self {
221        Config {
222            liquid_explorer: BlockchainExplorer::Esplora {
223                url: "http://localhost:3120/api".to_string(),
224                use_waterfalls: true,
225            },
226            bitcoin_explorer: BlockchainExplorer::Esplora {
227                url: "http://localhost:4002/api".to_string(),
228                use_waterfalls: false,
229            },
230            working_dir: ".".to_string(),
231            network: LiquidNetwork::Regtest,
232            payment_timeout_sec: 15,
233            sync_service_url: Some("http://localhost:8089".to_string()),
234            zero_conf_max_amount_sat: None,
235            breez_api_key: None,
236            external_input_parsers: None,
237            use_default_external_input_parsers: true,
238            onchain_fee_rate_leeway_sat_per_vbyte: None,
239            asset_metadata: None,
240            sideswap_api_key: None,
241        }
242    }
243
244    pub fn get_wallet_dir(&self, base_dir: &str, fingerprint_hex: &str) -> anyhow::Result<String> {
245        Ok(PathBuf::from(base_dir)
246            .join(match self.network {
247                LiquidNetwork::Mainnet => "mainnet",
248                LiquidNetwork::Testnet => "testnet",
249                LiquidNetwork::Regtest => "regtest",
250            })
251            .join(fingerprint_hex)
252            .to_str()
253            .ok_or(anyhow::anyhow!(
254                "Could not get retrieve current wallet directory"
255            ))?
256            .to_string())
257    }
258
259    pub fn zero_conf_max_amount_sat(&self) -> u64 {
260        self.zero_conf_max_amount_sat
261            .unwrap_or(DEFAULT_ZERO_CONF_MAX_SAT)
262    }
263
264    pub(crate) fn lbtc_asset_id(&self) -> String {
265        utils::lbtc_asset_id(self.network).to_string()
266    }
267
268    pub(crate) fn get_all_external_input_parsers(&self) -> Vec<ExternalInputParser> {
269        let mut external_input_parsers = Vec::new();
270        if self.use_default_external_input_parsers {
271            let default_parsers = DEFAULT_EXTERNAL_INPUT_PARSERS
272                .iter()
273                .map(|(id, regex, url)| ExternalInputParser {
274                    provider_id: id.to_string(),
275                    input_regex: regex.to_string(),
276                    parser_url: url.to_string(),
277                })
278                .collect::<Vec<_>>();
279            external_input_parsers.extend(default_parsers);
280        }
281        external_input_parsers.extend(self.external_input_parsers.clone().unwrap_or_default());
282
283        external_input_parsers
284    }
285
286    pub(crate) fn default_boltz_url(&self) -> &str {
287        match self.network {
288            LiquidNetwork::Mainnet => BOLTZ_MAINNET_URL_V2,
289            LiquidNetwork::Testnet => BOLTZ_TESTNET_URL_V2,
290            LiquidNetwork::Regtest => BOLTZ_REGTEST,
291        }
292    }
293
294    pub fn sync_enabled(&self) -> bool {
295        self.sync_service_url.is_some()
296    }
297
298    pub(crate) fn bitcoin_chain_service(&self) -> Arc<dyn BitcoinChainService> {
299        match self.bitcoin_explorer {
300            BlockchainExplorer::Esplora { .. } => {
301                Arc::new(EsploraBitcoinChainService::new(self.clone()))
302            }
303            #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
304            BlockchainExplorer::Electrum { .. } => Arc::new(
305                crate::chain::bitcoin::electrum::ElectrumBitcoinChainService::new(self.clone()),
306            ),
307        }
308    }
309
310    pub(crate) fn liquid_chain_service(&self) -> Result<Arc<dyn LiquidChainService>> {
311        match &self.liquid_explorer {
312            BlockchainExplorer::Esplora { url, .. } => {
313                if url == BREEZ_LIQUID_ESPLORA_URL && self.breez_api_key.is_none() {
314                    bail!("Cannot start the Breez Esplora chain service without providing a valid API key. See https://sdk-doc-liquid.breez.technology/guide/getting_started.html#api-key")
315                }
316                Ok(Arc::new(EsploraLiquidChainService::new(self.clone())))
317            }
318            #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
319            BlockchainExplorer::Electrum { .. } => Ok(Arc::new(
320                crate::chain::liquid::electrum::ElectrumLiquidChainService::new(self.clone()),
321            )),
322        }
323    }
324
325    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
326    pub(crate) fn electrum_tls_options(&self) -> (/*tls*/ bool, /*validate_domain*/ bool) {
327        match self.network {
328            LiquidNetwork::Mainnet | LiquidNetwork::Testnet => (true, true),
329            LiquidNetwork::Regtest => (false, false),
330        }
331    }
332
333    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
334    pub(crate) fn electrum_client(
335        &self,
336        url: &str,
337    ) -> Result<lwk_wollet::ElectrumClient, lwk_wollet::Error> {
338        let (tls, validate_domain) = self.electrum_tls_options();
339        let electrum_url = lwk_wollet::ElectrumUrl::new(url, tls, validate_domain)?;
340        lwk_wollet::ElectrumClient::with_options(
341            &electrum_url,
342            lwk_wollet::ElectrumOptions { timeout: Some(3) },
343        )
344    }
345}
346
347/// Network chosen for this Liquid SDK instance. Note that it represents both the Liquid and the
348/// Bitcoin network used.
349#[derive(Debug, Display, Copy, Clone, PartialEq, Serialize)]
350pub enum LiquidNetwork {
351    /// Mainnet Bitcoin and Liquid chains
352    Mainnet,
353    /// Testnet Bitcoin and Liquid chains
354    Testnet,
355    /// Regtest Bitcoin and Liquid chains
356    Regtest,
357}
358impl LiquidNetwork {
359    pub fn as_bitcoin_chain(&self) -> BitcoinChain {
360        match self {
361            LiquidNetwork::Mainnet => BitcoinChain::Bitcoin,
362            LiquidNetwork::Testnet => BitcoinChain::BitcoinTestnet,
363            LiquidNetwork::Regtest => BitcoinChain::BitcoinRegtest,
364        }
365    }
366}
367
368impl From<LiquidNetwork> for ElementsNetwork {
369    fn from(value: LiquidNetwork) -> Self {
370        match value {
371            LiquidNetwork::Mainnet => ElementsNetwork::Liquid,
372            LiquidNetwork::Testnet => ElementsNetwork::LiquidTestnet,
373            LiquidNetwork::Regtest => ElementsNetwork::ElementsRegtest {
374                policy_asset: AssetId::from_str(
375                    "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225",
376                )
377                .unwrap(),
378            },
379        }
380    }
381}
382
383impl From<LiquidNetwork> for Chain {
384    fn from(value: LiquidNetwork) -> Self {
385        Chain::Liquid(value.into())
386    }
387}
388
389impl From<LiquidNetwork> for LiquidChain {
390    fn from(value: LiquidNetwork) -> Self {
391        match value {
392            LiquidNetwork::Mainnet => LiquidChain::Liquid,
393            LiquidNetwork::Testnet => LiquidChain::LiquidTestnet,
394            LiquidNetwork::Regtest => LiquidChain::LiquidRegtest,
395        }
396    }
397}
398
399impl TryFrom<&str> for LiquidNetwork {
400    type Error = anyhow::Error;
401
402    fn try_from(value: &str) -> Result<LiquidNetwork, anyhow::Error> {
403        match value.to_lowercase().as_str() {
404            "mainnet" => Ok(LiquidNetwork::Mainnet),
405            "testnet" => Ok(LiquidNetwork::Testnet),
406            "regtest" => Ok(LiquidNetwork::Regtest),
407            _ => Err(anyhow!("Invalid network")),
408        }
409    }
410}
411
412impl From<LiquidNetwork> for Network {
413    fn from(value: LiquidNetwork) -> Self {
414        match value {
415            LiquidNetwork::Mainnet => Self::Bitcoin,
416            LiquidNetwork::Testnet => Self::Testnet,
417            LiquidNetwork::Regtest => Self::Regtest,
418        }
419    }
420}
421
422impl From<LiquidNetwork> for sdk_common::bitcoin::Network {
423    fn from(value: LiquidNetwork) -> Self {
424        match value {
425            LiquidNetwork::Mainnet => Self::Bitcoin,
426            LiquidNetwork::Testnet => Self::Testnet,
427            LiquidNetwork::Regtest => Self::Regtest,
428        }
429    }
430}
431
432impl From<LiquidNetwork> for boltz_client::bitcoin::Network {
433    fn from(value: LiquidNetwork) -> Self {
434        match value {
435            LiquidNetwork::Mainnet => Self::Bitcoin,
436            LiquidNetwork::Testnet => Self::Testnet,
437            LiquidNetwork::Regtest => Self::Regtest,
438        }
439    }
440}
441
442/// Trait that can be used to react to various [SdkEvent]s emitted by the SDK.
443pub trait EventListener: MaybeSend + MaybeSync {
444    fn on_event(&self, e: SdkEvent);
445}
446
447/// Event emitted by the SDK. Add an [EventListener] by calling [crate::sdk::LiquidSdk::add_event_listener]
448/// to listen for emitted events.
449#[derive(Clone, Debug, PartialEq)]
450pub enum SdkEvent {
451    PaymentFailed {
452        details: Payment,
453    },
454    PaymentPending {
455        details: Payment,
456    },
457    PaymentRefundable {
458        details: Payment,
459    },
460    PaymentRefunded {
461        details: Payment,
462    },
463    PaymentRefundPending {
464        details: Payment,
465    },
466    PaymentSucceeded {
467        details: Payment,
468    },
469    PaymentWaitingConfirmation {
470        details: Payment,
471    },
472    PaymentWaitingFeeAcceptance {
473        details: Payment,
474    },
475    /// Synced with mempool and onchain data
476    Synced,
477    /// Synced with real-time data sync
478    DataSynced {
479        /// Indicates new data was pulled from other instances.
480        did_pull_new_records: bool,
481    },
482}
483
484#[derive(thiserror::Error, Debug)]
485pub enum SignerError {
486    #[error("Signer error: {err}")]
487    Generic { err: String },
488}
489
490impl From<anyhow::Error> for SignerError {
491    fn from(err: anyhow::Error) -> Self {
492        SignerError::Generic {
493            err: err.to_string(),
494        }
495    }
496}
497
498impl From<bip32::Error> for SignerError {
499    fn from(err: bip32::Error) -> Self {
500        SignerError::Generic {
501            err: err.to_string(),
502        }
503    }
504}
505
506/// A trait that can be used to sign messages and verify signatures.
507/// The sdk user can implement this trait to use their own signer.
508pub trait Signer: MaybeSend + MaybeSync {
509    /// The master xpub encoded as 78 bytes length as defined in bip32 specification.
510    /// For reference: <https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#user-content-Serialization_format>
511    fn xpub(&self) -> Result<Vec<u8>, SignerError>;
512
513    /// The derived xpub encoded as 78 bytes length as defined in bip32 specification.
514    /// The derivation path is a string represents the shorter notation of the key tree to derive. For example:
515    /// m/49'/1'/0'/0/0
516    /// m/48'/1'/0'/0/0
517    /// For reference: <https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#user-content-The_key_tree>
518    fn derive_xpub(&self, derivation_path: String) -> Result<Vec<u8>, SignerError>;
519
520    /// Sign an ECDSA message using the private key derived from the given derivation path
521    fn sign_ecdsa(&self, msg: Vec<u8>, derivation_path: String) -> Result<Vec<u8>, SignerError>;
522
523    /// Sign an ECDSA message using the private key derived from the master key
524    fn sign_ecdsa_recoverable(&self, msg: Vec<u8>) -> Result<Vec<u8>, SignerError>;
525
526    /// Return the master blinding key for SLIP77: <https://github.com/satoshilabs/slips/blob/master/slip-0077.md>
527    fn slip77_master_blinding_key(&self) -> Result<Vec<u8>, SignerError>;
528
529    /// HMAC-SHA256 using the private key derived from the given derivation path
530    /// This is used to calculate the linking key of lnurl-auth specification: <https://github.com/lnurl/luds/blob/luds/05.md>
531    fn hmac_sha256(&self, msg: Vec<u8>, derivation_path: String) -> Result<Vec<u8>, SignerError>;
532
533    /// Encrypts a message using (ECIES)[ecies::encrypt]
534    fn ecies_encrypt(&self, msg: Vec<u8>) -> Result<Vec<u8>, SignerError>;
535
536    /// Decrypts a message using (ECIES)[ecies::decrypt]
537    fn ecies_decrypt(&self, msg: Vec<u8>) -> Result<Vec<u8>, SignerError>;
538}
539
540/// An argument when calling [crate::sdk::LiquidSdk::connect].
541/// The resquest takes either a `mnemonic` and `passphrase`, or a `seed`.
542pub struct ConnectRequest {
543    /// The SDK [Config]
544    pub config: Config,
545    /// The optional Liquid wallet mnemonic
546    pub mnemonic: Option<String>,
547    /// The optional passphrase for the mnemonic
548    pub passphrase: Option<String>,
549    /// The optional Liquid wallet seed
550    pub seed: Option<Vec<u8>>,
551}
552
553pub struct ConnectWithSignerRequest {
554    pub config: Config,
555}
556
557/// A reserved address. Once an address is reserved, it can only be
558/// reallocated to another payment after the block height expiration.
559#[derive(Clone, Debug)]
560pub(crate) struct ReservedAddress {
561    /// The address that is reserved
562    pub(crate) address: String,
563    /// The block height that the address is reserved until
564    pub(crate) expiry_block_height: u32,
565}
566
567/// The send/receive methods supported by the SDK
568#[derive(Clone, Debug, Serialize)]
569pub enum PaymentMethod {
570    #[deprecated(since = "0.8.1", note = "Use `Bolt11Invoice` instead")]
571    Lightning,
572    Bolt11Invoice,
573    Bolt12Offer,
574    BitcoinAddress,
575    LiquidAddress,
576}
577
578#[derive(Debug, Serialize, Clone)]
579pub enum ReceiveAmount {
580    /// The amount in satoshi that should be paid
581    Bitcoin { payer_amount_sat: u64 },
582
583    /// The amount of an asset that should be paid
584    Asset {
585        asset_id: String,
586        payer_amount: Option<f64>,
587    },
588}
589
590/// An argument when calling [crate::sdk::LiquidSdk::prepare_receive_payment].
591#[derive(Debug, Serialize)]
592pub struct PrepareReceiveRequest {
593    pub payment_method: PaymentMethod,
594    /// The amount to be paid in either Bitcoin or another asset
595    pub amount: Option<ReceiveAmount>,
596}
597
598/// Returned when calling [crate::sdk::LiquidSdk::prepare_receive_payment].
599#[derive(Debug, Serialize, Clone)]
600pub struct PrepareReceiveResponse {
601    pub payment_method: PaymentMethod,
602    /// Generally represents the total fees that would be paid to send or receive this payment.
603    ///
604    /// In case of Zero-Amount Receive Chain swaps, the swapper service fee (`swapper_feerate` times
605    /// the amount) is paid in addition to `fees_sat`. The swapper service feerate is already known
606    /// in the beginning, but the exact swapper service fee will only be known when the
607    /// `payer_amount_sat` is known.
608    ///
609    /// In all other types of swaps, the swapper service fee is included in `fees_sat`.
610    pub fees_sat: u64,
611    /// The amount to be paid in either Bitcoin or another asset
612    pub amount: Option<ReceiveAmount>,
613    /// The minimum amount the payer can send for this swap to succeed.
614    ///
615    /// When the method is [PaymentMethod::LiquidAddress], this is empty.
616    pub min_payer_amount_sat: Option<u64>,
617    /// The maximum amount the payer can send for this swap to succeed.
618    ///
619    /// When the method is [PaymentMethod::LiquidAddress], this is empty.
620    pub max_payer_amount_sat: Option<u64>,
621    /// The percentage of the sent amount that will count towards the service fee.
622    ///
623    /// When the method is [PaymentMethod::LiquidAddress], this is empty.
624    pub swapper_feerate: Option<f64>,
625}
626
627/// An argument when calling [crate::sdk::LiquidSdk::receive_payment].
628#[derive(Debug, Serialize)]
629pub struct ReceivePaymentRequest {
630    pub prepare_response: PrepareReceiveResponse,
631    /// The description for this payment request.
632    pub description: Option<String>,
633    /// If set to true, then the hash of the description will be used.
634    pub use_description_hash: Option<bool>,
635}
636
637/// Returned when calling [crate::sdk::LiquidSdk::receive_payment].
638#[derive(Debug, Serialize)]
639pub struct ReceivePaymentResponse {
640    /// Either a BIP21 URI (Liquid or Bitcoin), a Liquid address
641    /// or an invoice, depending on the [PrepareReceiveResponse] parameters
642    pub destination: String,
643}
644
645/// An argument when calling [crate::sdk::LiquidSdk::create_bolt12_invoice].
646#[derive(Debug, Serialize)]
647pub struct CreateBolt12InvoiceRequest {
648    /// The BOLT12 offer
649    pub offer: String,
650    /// The invoice request created from the offer
651    pub invoice_request: String,
652}
653
654/// Returned when calling [crate::sdk::LiquidSdk::create_bolt12_invoice].
655#[derive(Debug, Serialize, Clone)]
656pub struct CreateBolt12InvoiceResponse {
657    /// The BOLT12 invoice
658    pub invoice: String,
659}
660
661/// The minimum and maximum in satoshis of a Lightning or onchain payment.
662#[derive(Debug, Serialize)]
663pub struct Limits {
664    pub min_sat: u64,
665    pub max_sat: u64,
666    pub max_zero_conf_sat: u64,
667}
668
669/// Returned when calling [crate::sdk::LiquidSdk::fetch_lightning_limits].
670#[derive(Debug, Serialize)]
671pub struct LightningPaymentLimitsResponse {
672    /// Amount limits for a Send Payment to be valid
673    pub send: Limits,
674    /// Amount limits for a Receive Payment to be valid
675    pub receive: Limits,
676}
677
678/// Returned when calling [crate::sdk::LiquidSdk::fetch_onchain_limits].
679#[derive(Debug, Serialize)]
680pub struct OnchainPaymentLimitsResponse {
681    /// Amount limits for a Send Onchain Payment to be valid
682    pub send: Limits,
683    /// Amount limits for a Receive Onchain Payment to be valid
684    pub receive: Limits,
685}
686
687/// An argument when calling [crate::sdk::LiquidSdk::prepare_send_payment].
688#[derive(Debug, Serialize, Clone)]
689pub struct PrepareSendRequest {
690    /// The destination we intend to pay to.
691    /// Supports BIP21 URIs, BOLT11 invoices, BOLT12 offers and Liquid addresses
692    pub destination: String,
693    /// Should only be set when paying directly onchain or to a BIP21 URI
694    /// where no amount is specified, or when the caller wishes to drain
695    pub amount: Option<PayAmount>,
696    /// An optional comment when paying a BOLT12 offer
697    pub comment: Option<String>,
698}
699
700/// Specifies the supported destinations which can be payed by the SDK
701#[derive(Clone, Debug, Serialize)]
702pub enum SendDestination {
703    LiquidAddress {
704        address_data: liquid::LiquidAddressData,
705        /// A BIP353 address, in case one was used to resolve this Liquid address
706        bip353_address: Option<String>,
707    },
708    Bolt11 {
709        invoice: LNInvoice,
710        /// A BIP353 address, in case one was used to resolve this BOLT11
711        bip353_address: Option<String>,
712    },
713    Bolt12 {
714        offer: LNOffer,
715        receiver_amount_sat: u64,
716        /// A BIP353 address, in case one was used to resolve this BOLT12
717        bip353_address: Option<String>,
718        /// An optional payer note
719        payer_note: Option<String>,
720    },
721}
722
723/// Returned when calling [crate::sdk::LiquidSdk::prepare_send_payment].
724#[derive(Debug, Serialize, Clone)]
725pub struct PrepareSendResponse {
726    pub destination: SendDestination,
727    /// The optional amount to be sent in either Bitcoin or another asset
728    pub amount: Option<PayAmount>,
729    /// The optional estimated fee in satoshi. Is set when there is Bitcoin available
730    /// to pay fees. When not set, there are asset fees available to pay fees.
731    pub fees_sat: Option<u64>,
732    /// The optional estimated fee in the asset. Is set when [PayAmount::Asset::estimate_asset_fees]
733    /// is set to `true`, the Payjoin service accepts this asset to pay fees and there
734    /// are funds available in this asset to pay fees.
735    pub estimated_asset_fees: Option<f64>,
736}
737
738/// An argument when calling [crate::sdk::LiquidSdk::send_payment].
739#[derive(Debug, Serialize)]
740pub struct SendPaymentRequest {
741    pub prepare_response: PrepareSendResponse,
742    pub use_asset_fees: Option<bool>,
743}
744
745/// Returned when calling [crate::sdk::LiquidSdk::send_payment].
746#[derive(Debug, Serialize)]
747pub struct SendPaymentResponse {
748    pub payment: Payment,
749}
750
751/// Used to specify the amount to sent or to send all funds.
752#[derive(Debug, Serialize, Clone)]
753pub enum PayAmount {
754    /// The amount in satoshi that will be received
755    Bitcoin { receiver_amount_sat: u64 },
756
757    /// The amount of an asset that will be received
758    Asset {
759        asset_id: String,
760        receiver_amount: f64,
761        estimate_asset_fees: Option<bool>,
762    },
763
764    /// Indicates that all available Bitcoin funds should be sent
765    Drain,
766}
767
768/// An argument when calling [crate::sdk::LiquidSdk::prepare_pay_onchain].
769#[derive(Debug, Serialize, Clone)]
770pub struct PreparePayOnchainRequest {
771    /// The amount to send
772    pub amount: PayAmount,
773    /// The optional fee rate of the Bitcoin claim transaction in sat/vB. Defaults to the swapper estimated claim fee.
774    pub fee_rate_sat_per_vbyte: Option<u32>,
775}
776
777/// Returned when calling [crate::sdk::LiquidSdk::prepare_pay_onchain].
778#[derive(Debug, Serialize, Clone)]
779pub struct PreparePayOnchainResponse {
780    pub receiver_amount_sat: u64,
781    pub claim_fees_sat: u64,
782    pub total_fees_sat: u64,
783}
784
785/// An argument when calling [crate::sdk::LiquidSdk::pay_onchain].
786#[derive(Debug, Serialize)]
787pub struct PayOnchainRequest {
788    pub address: String,
789    pub prepare_response: PreparePayOnchainResponse,
790}
791
792/// An argument when calling [crate::sdk::LiquidSdk::prepare_refund].
793#[derive(Debug, Serialize)]
794pub struct PrepareRefundRequest {
795    /// The address where the swap funds are locked up
796    pub swap_address: String,
797    /// The address to refund the swap funds to
798    pub refund_address: String,
799    /// The fee rate in sat/vB for the refund transaction
800    pub fee_rate_sat_per_vbyte: u32,
801}
802
803/// Returned when calling [crate::sdk::LiquidSdk::prepare_refund].
804#[derive(Debug, Serialize)]
805pub struct PrepareRefundResponse {
806    pub tx_vsize: u32,
807    pub tx_fee_sat: u64,
808    /// The txid of the last broadcasted refund tx, if any
809    pub last_refund_tx_id: Option<String>,
810}
811
812/// An argument when calling [crate::sdk::LiquidSdk::refund].
813#[derive(Debug, Serialize)]
814pub struct RefundRequest {
815    /// The address where the swap funds are locked up
816    pub swap_address: String,
817    /// The address to refund the swap funds to
818    pub refund_address: String,
819    /// The fee rate in sat/vB for the refund transaction
820    pub fee_rate_sat_per_vbyte: u32,
821}
822
823/// Returned when calling [crate::sdk::LiquidSdk::refund].
824#[derive(Debug, Serialize)]
825pub struct RefundResponse {
826    pub refund_tx_id: String,
827}
828
829/// An asset balance to denote the balance for each asset.
830#[derive(Clone, Debug, Default, Serialize, Deserialize)]
831pub struct AssetBalance {
832    pub asset_id: String,
833    pub balance_sat: u64,
834    pub name: Option<String>,
835    pub ticker: Option<String>,
836    pub balance: Option<f64>,
837}
838
839#[derive(Debug, Serialize, Deserialize, Default)]
840pub struct BlockchainInfo {
841    pub liquid_tip: u32,
842    pub bitcoin_tip: u32,
843}
844
845#[derive(Copy, Clone)]
846pub(crate) struct ChainTips {
847    pub liquid_tip: u32,
848    pub bitcoin_tip: u32,
849}
850
851#[derive(Debug, Serialize, Deserialize)]
852pub struct WalletInfo {
853    /// Usable balance. This is the confirmed onchain balance minus `pending_send_sat`.
854    pub balance_sat: u64,
855    /// Amount that is being used for ongoing Send swaps
856    pub pending_send_sat: u64,
857    /// Incoming amount that is pending from ongoing Receive swaps
858    pub pending_receive_sat: u64,
859    /// The wallet's fingerprint. It is used to build the working directory in [Config::get_wallet_dir].
860    pub fingerprint: String,
861    /// The wallet's pubkey. Used to verify signed messages.
862    pub pubkey: String,
863    /// Asset balances of non Liquid Bitcoin assets
864    #[serde(default)]
865    pub asset_balances: Vec<AssetBalance>,
866}
867
868impl WalletInfo {
869    pub(crate) fn validate_sufficient_funds(
870        &self,
871        network: LiquidNetwork,
872        amount_sat: u64,
873        fees_sat: Option<u64>,
874        asset_id: &str,
875    ) -> Result<(), PaymentError> {
876        let fees_sat = fees_sat.unwrap_or(0);
877        if asset_id.eq(&utils::lbtc_asset_id(network).to_string()) {
878            ensure_sdk!(
879                amount_sat + fees_sat <= self.balance_sat,
880                PaymentError::InsufficientFunds
881            );
882        } else {
883            match self
884                .asset_balances
885                .iter()
886                .find(|ab| ab.asset_id.eq(asset_id))
887            {
888                Some(asset_balance) => ensure_sdk!(
889                    amount_sat <= asset_balance.balance_sat && fees_sat <= self.balance_sat,
890                    PaymentError::InsufficientFunds
891                ),
892                None => return Err(PaymentError::InsufficientFunds),
893            }
894        }
895        Ok(())
896    }
897}
898
899/// Returned when calling [crate::sdk::LiquidSdk::get_info].
900#[derive(Debug, Serialize, Deserialize)]
901pub struct GetInfoResponse {
902    /// The wallet information, such as the balance, fingerprint and public key
903    pub wallet_info: WalletInfo,
904    /// The latest synced blockchain information, such as the Liquid/Bitcoin tips
905    #[serde(default)]
906    pub blockchain_info: BlockchainInfo,
907}
908
909/// An argument when calling [crate::sdk::LiquidSdk::sign_message].
910#[derive(Clone, Debug, PartialEq)]
911pub struct SignMessageRequest {
912    pub message: String,
913}
914
915/// Returned when calling [crate::sdk::LiquidSdk::sign_message].
916#[derive(Clone, Debug, PartialEq)]
917pub struct SignMessageResponse {
918    pub signature: String,
919}
920
921/// An argument when calling [crate::sdk::LiquidSdk::check_message].
922#[derive(Clone, Debug, PartialEq)]
923pub struct CheckMessageRequest {
924    /// The message that was signed.
925    pub message: String,
926    /// The public key of the node that signed the message.
927    pub pubkey: String,
928    /// The zbase encoded signature to verify.
929    pub signature: String,
930}
931
932/// Returned when calling [crate::sdk::LiquidSdk::check_message].
933#[derive(Clone, Debug, PartialEq)]
934pub struct CheckMessageResponse {
935    /// Boolean value indicating whether the signature covers the message and
936    /// was signed by the given pubkey.
937    pub is_valid: bool,
938}
939
940/// An argument when calling [crate::sdk::LiquidSdk::backup].
941#[derive(Debug, Serialize)]
942pub struct BackupRequest {
943    /// Path to the backup.
944    ///
945    /// If not set, it defaults to `backup.sql` for mainnet, `backup-testnet.sql` for testnet,
946    /// and `backup-regtest.sql` for regtest.
947    ///
948    /// The file will be saved in [ConnectRequest]'s `data_dir`.
949    pub backup_path: Option<String>,
950}
951
952/// An argument when calling [crate::sdk::LiquidSdk::restore].
953#[derive(Debug, Serialize)]
954pub struct RestoreRequest {
955    pub backup_path: Option<String>,
956}
957
958/// An argument when calling [crate::sdk::LiquidSdk::list_payments].
959#[derive(Default)]
960pub struct ListPaymentsRequest {
961    pub filters: Option<Vec<PaymentType>>,
962    pub states: Option<Vec<PaymentState>>,
963    /// Epoch time, in seconds
964    pub from_timestamp: Option<i64>,
965    /// Epoch time, in seconds
966    pub to_timestamp: Option<i64>,
967    pub offset: Option<u32>,
968    pub limit: Option<u32>,
969    pub details: Option<ListPaymentDetails>,
970    pub sort_ascending: Option<bool>,
971}
972
973/// An argument of [ListPaymentsRequest] when calling [crate::sdk::LiquidSdk::list_payments].
974#[derive(Debug, Serialize)]
975pub enum ListPaymentDetails {
976    /// A Liquid payment
977    Liquid {
978        /// Optional asset id
979        asset_id: Option<String>,
980        /// Optional BIP21 URI or address
981        destination: Option<String>,
982    },
983
984    /// A Bitcoin payment
985    Bitcoin {
986        /// Optional address
987        address: Option<String>,
988    },
989}
990
991/// An argument when calling [crate::sdk::LiquidSdk::get_payment].
992#[derive(Debug, Serialize)]
993pub enum GetPaymentRequest {
994    /// The payment hash of a Lightning payment
995    PaymentHash { payment_hash: String },
996    /// A swap id or its SHA256 hash
997    SwapId { swap_id: String },
998}
999
1000/// Trait that can be used to react to new blocks from Bitcoin and Liquid chains
1001#[sdk_macros::async_trait]
1002pub(crate) trait BlockListener: MaybeSend + MaybeSync {
1003    async fn on_bitcoin_block(&self, height: u32);
1004    async fn on_liquid_block(&self, height: u32);
1005}
1006
1007// A swap enum variant
1008#[derive(Clone, Debug)]
1009pub enum Swap {
1010    Chain(ChainSwap),
1011    Send(SendSwap),
1012    Receive(ReceiveSwap),
1013}
1014impl Swap {
1015    pub(crate) fn id(&self) -> String {
1016        match &self {
1017            Swap::Chain(ChainSwap { id, .. })
1018            | Swap::Send(SendSwap { id, .. })
1019            | Swap::Receive(ReceiveSwap { id, .. }) => id.clone(),
1020        }
1021    }
1022
1023    pub(crate) fn version(&self) -> u64 {
1024        match self {
1025            Swap::Chain(ChainSwap { metadata, .. })
1026            | Swap::Send(SendSwap { metadata, .. })
1027            | Swap::Receive(ReceiveSwap { metadata, .. }) => metadata.version,
1028        }
1029    }
1030
1031    pub(crate) fn set_version(&mut self, version: u64) {
1032        match self {
1033            Swap::Chain(chain_swap) => {
1034                chain_swap.metadata.version = version;
1035            }
1036            Swap::Send(send_swap) => {
1037                send_swap.metadata.version = version;
1038            }
1039            Swap::Receive(receive_swap) => {
1040                receive_swap.metadata.version = version;
1041            }
1042        }
1043    }
1044
1045    pub(crate) fn last_updated_at(&self) -> u32 {
1046        match self {
1047            Swap::Chain(ChainSwap { metadata, .. })
1048            | Swap::Send(SendSwap { metadata, .. })
1049            | Swap::Receive(ReceiveSwap { metadata, .. }) => metadata.last_updated_at,
1050        }
1051    }
1052}
1053impl From<ChainSwap> for Swap {
1054    fn from(swap: ChainSwap) -> Self {
1055        Self::Chain(swap)
1056    }
1057}
1058impl From<SendSwap> for Swap {
1059    fn from(swap: SendSwap) -> Self {
1060        Self::Send(swap)
1061    }
1062}
1063impl From<ReceiveSwap> for Swap {
1064    fn from(swap: ReceiveSwap) -> Self {
1065        Self::Receive(swap)
1066    }
1067}
1068
1069#[derive(Clone, Debug)]
1070pub(crate) enum SwapScriptV2 {
1071    Bitcoin(BtcSwapScript),
1072    Liquid(LBtcSwapScript),
1073}
1074impl SwapScriptV2 {
1075    pub(crate) fn as_bitcoin_script(&self) -> Result<BtcSwapScript> {
1076        match self {
1077            SwapScriptV2::Bitcoin(script) => Ok(script.clone()),
1078            _ => Err(anyhow!("Invalid chain")),
1079        }
1080    }
1081
1082    pub(crate) fn as_liquid_script(&self) -> Result<LBtcSwapScript> {
1083        match self {
1084            SwapScriptV2::Liquid(script) => Ok(script.clone()),
1085            _ => Err(anyhow!("Invalid chain")),
1086        }
1087    }
1088}
1089
1090#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
1091pub enum Direction {
1092    Incoming = 0,
1093    Outgoing = 1,
1094}
1095impl ToSql for Direction {
1096    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
1097        Ok(rusqlite::types::ToSqlOutput::from(*self as i8))
1098    }
1099}
1100impl FromSql for Direction {
1101    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
1102        match value {
1103            ValueRef::Integer(i) => match i as u8 {
1104                0 => Ok(Direction::Incoming),
1105                1 => Ok(Direction::Outgoing),
1106                _ => Err(FromSqlError::OutOfRange(i)),
1107            },
1108            _ => Err(FromSqlError::InvalidType),
1109        }
1110    }
1111}
1112
1113#[derive(Clone, Debug, Default)]
1114pub(crate) struct SwapMetadata {
1115    /// Version used for optimistic concurrency control within local db
1116    pub(crate) version: u64,
1117    pub(crate) last_updated_at: u32,
1118    pub(crate) is_local: bool,
1119}
1120
1121/// A chain swap
1122///
1123/// See <https://docs.boltz.exchange/v/api/lifecycle#chain-swaps>
1124#[derive(Clone, Debug, Derivative)]
1125#[derivative(PartialEq)]
1126pub struct ChainSwap {
1127    pub(crate) id: String,
1128    pub(crate) direction: Direction,
1129    /// Always set for Outgoing Chain Swaps. It's the destination Bitcoin address
1130    /// Only set for Incoming Chain Swaps when a claim is broadcasted. It's a local Liquid wallet address
1131    pub(crate) claim_address: Option<String>,
1132    pub(crate) lockup_address: String,
1133    /// The Liquid refund address is only set for Outgoing Chain Swaps
1134    pub(crate) refund_address: Option<String>,
1135    pub(crate) timeout_block_height: u32,
1136    pub(crate) preimage: String,
1137    pub(crate) description: Option<String>,
1138    /// Payer amount defined at swap creation
1139    pub(crate) payer_amount_sat: u64,
1140    /// The actual payer amount as seen on the user lockup tx. Might differ from `payer_amount_sat`
1141    /// in the case of an over/underpayment
1142    pub(crate) actual_payer_amount_sat: Option<u64>,
1143    /// Receiver amount defined at swap creation
1144    pub(crate) receiver_amount_sat: u64,
1145    /// The final receiver amount, in case of an amountless swap for which fees have been accepted
1146    pub(crate) accepted_receiver_amount_sat: Option<u64>,
1147    pub(crate) claim_fees_sat: u64,
1148    /// The [ChainPair] chosen on swap creation
1149    pub(crate) pair_fees_json: String,
1150    pub(crate) accept_zero_conf: bool,
1151    /// JSON representation of [crate::persist::chain::InternalCreateChainResponse]
1152    pub(crate) create_response_json: String,
1153    /// Persisted only when the server lockup tx is successfully broadcast
1154    pub(crate) server_lockup_tx_id: Option<String>,
1155    /// Persisted only when the user lockup tx is successfully broadcast
1156    pub(crate) user_lockup_tx_id: Option<String>,
1157    /// Persisted as soon as a claim tx is broadcast
1158    pub(crate) claim_tx_id: Option<String>,
1159    /// Persisted as soon as a refund tx is broadcast
1160    pub(crate) refund_tx_id: Option<String>,
1161    pub(crate) created_at: u32,
1162    pub(crate) state: PaymentState,
1163    pub(crate) claim_private_key: String,
1164    pub(crate) refund_private_key: String,
1165    pub(crate) auto_accepted_fees: bool,
1166    /// Swap metadata that is only valid when reading one from the local database
1167    #[derivative(PartialEq = "ignore")]
1168    pub(crate) metadata: SwapMetadata,
1169}
1170impl ChainSwap {
1171    pub(crate) fn get_claim_keypair(&self) -> SdkResult<Keypair> {
1172        utils::decode_keypair(&self.claim_private_key)
1173    }
1174
1175    pub(crate) fn get_refund_keypair(&self) -> SdkResult<Keypair> {
1176        utils::decode_keypair(&self.refund_private_key)
1177    }
1178
1179    pub(crate) fn get_boltz_create_response(&self) -> Result<CreateChainResponse> {
1180        let internal_create_response: crate::persist::chain::InternalCreateChainResponse =
1181            serde_json::from_str(&self.create_response_json).map_err(|e| {
1182                anyhow!("Failed to deserialize InternalCreateSubmarineResponse: {e:?}")
1183            })?;
1184
1185        Ok(CreateChainResponse {
1186            id: self.id.clone(),
1187            claim_details: internal_create_response.claim_details,
1188            lockup_details: internal_create_response.lockup_details,
1189        })
1190    }
1191
1192    pub(crate) fn get_boltz_pair(&self) -> Result<ChainPair> {
1193        let pair: ChainPair = serde_json::from_str(&self.pair_fees_json)
1194            .map_err(|e| anyhow!("Failed to deserialize ChainPair: {e:?}"))?;
1195
1196        Ok(pair)
1197    }
1198
1199    pub(crate) fn get_claim_swap_script(&self) -> SdkResult<SwapScriptV2> {
1200        let chain_swap_details = self.get_boltz_create_response()?.claim_details;
1201        let our_pubkey = self.get_claim_keypair()?.public_key();
1202        let swap_script = match self.direction {
1203            Direction::Incoming => SwapScriptV2::Liquid(LBtcSwapScript::chain_from_swap_resp(
1204                Side::Claim,
1205                chain_swap_details,
1206                our_pubkey.into(),
1207            )?),
1208            Direction::Outgoing => SwapScriptV2::Bitcoin(BtcSwapScript::chain_from_swap_resp(
1209                Side::Claim,
1210                chain_swap_details,
1211                our_pubkey.into(),
1212            )?),
1213        };
1214        Ok(swap_script)
1215    }
1216
1217    pub(crate) fn get_lockup_swap_script(&self) -> SdkResult<SwapScriptV2> {
1218        let chain_swap_details = self.get_boltz_create_response()?.lockup_details;
1219        let our_pubkey = self.get_refund_keypair()?.public_key();
1220        let swap_script = match self.direction {
1221            Direction::Incoming => SwapScriptV2::Bitcoin(BtcSwapScript::chain_from_swap_resp(
1222                Side::Lockup,
1223                chain_swap_details,
1224                our_pubkey.into(),
1225            )?),
1226            Direction::Outgoing => SwapScriptV2::Liquid(LBtcSwapScript::chain_from_swap_resp(
1227                Side::Lockup,
1228                chain_swap_details,
1229                our_pubkey.into(),
1230            )?),
1231        };
1232        Ok(swap_script)
1233    }
1234
1235    /// Returns the lockup script pubkey for Receive Chain Swaps
1236    pub(crate) fn get_receive_lockup_swap_script_pubkey(
1237        &self,
1238        network: LiquidNetwork,
1239    ) -> SdkResult<ScriptBuf> {
1240        let swap_script = self.get_lockup_swap_script()?.as_bitcoin_script()?;
1241        let script_pubkey = swap_script
1242            .to_address(network.as_bitcoin_chain())
1243            .map_err(|e| SdkError::generic(format!("Error getting script address: {e:?}")))?
1244            .script_pubkey();
1245        Ok(script_pubkey)
1246    }
1247
1248    pub(crate) fn to_refundable(&self, amount_sat: u64) -> RefundableSwap {
1249        RefundableSwap {
1250            swap_address: self.lockup_address.clone(),
1251            timestamp: self.created_at,
1252            amount_sat,
1253            last_refund_tx_id: self.refund_tx_id.clone(),
1254        }
1255    }
1256
1257    pub(crate) fn from_boltz_struct_to_json(
1258        create_response: &CreateChainResponse,
1259        expected_swap_id: &str,
1260    ) -> Result<String, PaymentError> {
1261        let internal_create_response =
1262            crate::persist::chain::InternalCreateChainResponse::try_convert_from_boltz(
1263                create_response,
1264                expected_swap_id,
1265            )?;
1266
1267        let create_response_json =
1268            serde_json::to_string(&internal_create_response).map_err(|e| {
1269                PaymentError::Generic {
1270                    err: format!("Failed to serialize InternalCreateChainResponse: {e:?}"),
1271                }
1272            })?;
1273
1274        Ok(create_response_json)
1275    }
1276
1277    pub(crate) fn is_waiting_fee_acceptance(&self) -> bool {
1278        self.payer_amount_sat == 0 && self.accepted_receiver_amount_sat.is_none()
1279    }
1280}
1281
1282#[derive(Clone, Debug, Default)]
1283pub(crate) struct ChainSwapUpdate {
1284    pub(crate) swap_id: String,
1285    pub(crate) to_state: PaymentState,
1286    pub(crate) server_lockup_tx_id: Option<String>,
1287    pub(crate) user_lockup_tx_id: Option<String>,
1288    pub(crate) claim_address: Option<String>,
1289    pub(crate) claim_tx_id: Option<String>,
1290    pub(crate) refund_tx_id: Option<String>,
1291}
1292
1293/// A submarine swap, used for Send
1294#[derive(Clone, Debug, Derivative)]
1295#[derivative(PartialEq)]
1296pub struct SendSwap {
1297    pub(crate) id: String,
1298    /// Bolt11 or Bolt12 invoice. This is determined by whether `bolt12_offer` is set or not.
1299    pub(crate) invoice: String,
1300    /// The bolt12 offer, if this swap sends to a Bolt12 offer
1301    pub(crate) bolt12_offer: Option<String>,
1302    pub(crate) payment_hash: Option<String>,
1303    pub(crate) destination_pubkey: Option<String>,
1304    pub(crate) description: Option<String>,
1305    pub(crate) preimage: Option<String>,
1306    pub(crate) payer_amount_sat: u64,
1307    pub(crate) receiver_amount_sat: u64,
1308    /// The [SubmarinePair] chosen on swap creation
1309    pub(crate) pair_fees_json: String,
1310    /// JSON representation of [crate::persist::send::InternalCreateSubmarineResponse]
1311    pub(crate) create_response_json: String,
1312    /// Persisted only when the lockup tx is successfully broadcast
1313    pub(crate) lockup_tx_id: Option<String>,
1314    /// Persisted only when a refund tx is needed
1315    pub(crate) refund_address: Option<String>,
1316    /// Persisted after the refund tx is broadcast
1317    pub(crate) refund_tx_id: Option<String>,
1318    pub(crate) created_at: u32,
1319    pub(crate) timeout_block_height: u64,
1320    pub(crate) state: PaymentState,
1321    pub(crate) refund_private_key: String,
1322    /// Swap metadata that is only valid when reading one from the local database
1323    #[derivative(PartialEq = "ignore")]
1324    pub(crate) metadata: SwapMetadata,
1325}
1326impl SendSwap {
1327    pub(crate) fn get_refund_keypair(&self) -> Result<Keypair, SdkError> {
1328        utils::decode_keypair(&self.refund_private_key)
1329    }
1330
1331    pub(crate) fn get_boltz_create_response(&self) -> Result<CreateSubmarineResponse> {
1332        let internal_create_response: crate::persist::send::InternalCreateSubmarineResponse =
1333            serde_json::from_str(&self.create_response_json).map_err(|e| {
1334                anyhow!("Failed to deserialize InternalCreateSubmarineResponse: {e:?}")
1335            })?;
1336
1337        let res = CreateSubmarineResponse {
1338            id: self.id.clone(),
1339            accept_zero_conf: internal_create_response.accept_zero_conf,
1340            address: internal_create_response.address.clone(),
1341            bip21: internal_create_response.bip21.clone(),
1342            claim_public_key: crate::utils::json_to_pubkey(
1343                &internal_create_response.claim_public_key,
1344            )?,
1345            expected_amount: internal_create_response.expected_amount,
1346            referral_id: internal_create_response.referral_id,
1347            swap_tree: internal_create_response.swap_tree.clone().into(),
1348            timeout_block_height: internal_create_response.timeout_block_height,
1349            blinding_key: internal_create_response.blinding_key.clone(),
1350        };
1351        Ok(res)
1352    }
1353
1354    pub(crate) fn get_swap_script(&self) -> Result<LBtcSwapScript, SdkError> {
1355        LBtcSwapScript::submarine_from_swap_resp(
1356            &self.get_boltz_create_response()?,
1357            self.get_refund_keypair()?.public_key().into(),
1358        )
1359        .map_err(|e| {
1360            SdkError::generic(format!(
1361                "Failed to create swap script for Send Swap {}: {e:?}",
1362                self.id
1363            ))
1364        })
1365    }
1366
1367    pub(crate) fn from_boltz_struct_to_json(
1368        create_response: &CreateSubmarineResponse,
1369        expected_swap_id: &str,
1370    ) -> Result<String, PaymentError> {
1371        let internal_create_response =
1372            crate::persist::send::InternalCreateSubmarineResponse::try_convert_from_boltz(
1373                create_response,
1374                expected_swap_id,
1375            )?;
1376
1377        let create_response_json =
1378            serde_json::to_string(&internal_create_response).map_err(|e| {
1379                PaymentError::Generic {
1380                    err: format!("Failed to serialize InternalCreateSubmarineResponse: {e:?}"),
1381                }
1382            })?;
1383
1384        Ok(create_response_json)
1385    }
1386}
1387
1388/// A reverse swap, used for Receive
1389#[derive(Clone, Debug, Derivative)]
1390#[derivative(PartialEq)]
1391pub struct ReceiveSwap {
1392    pub(crate) id: String,
1393    pub(crate) preimage: String,
1394    /// JSON representation of [crate::persist::receive::InternalCreateReverseResponse]
1395    pub(crate) create_response_json: String,
1396    pub(crate) claim_private_key: String,
1397    pub(crate) invoice: String,
1398    /// The bolt12 offer, if used to create the swap
1399    pub(crate) bolt12_offer: Option<String>,
1400    pub(crate) payment_hash: Option<String>,
1401    pub(crate) destination_pubkey: Option<String>,
1402    pub(crate) description: Option<String>,
1403    /// The amount of the invoice
1404    pub(crate) payer_amount_sat: u64,
1405    pub(crate) receiver_amount_sat: u64,
1406    /// The [ReversePair] chosen on swap creation
1407    pub(crate) pair_fees_json: String,
1408    pub(crate) claim_fees_sat: u64,
1409    /// Persisted only when a claim tx is needed
1410    pub(crate) claim_address: Option<String>,
1411    /// Persisted after the claim tx is broadcast
1412    pub(crate) claim_tx_id: Option<String>,
1413    /// The transaction id of the swapper's tx broadcast
1414    pub(crate) lockup_tx_id: Option<String>,
1415    /// The address reserved for a magic routing hint payment
1416    pub(crate) mrh_address: String,
1417    /// Persisted only if a transaction is sent to the `mrh_address`
1418    pub(crate) mrh_tx_id: Option<String>,
1419    /// Until the lockup tx is seen in the mempool, it contains the swap creation time.
1420    /// Afterwards, it shows the lockup tx creation time.
1421    pub(crate) created_at: u32,
1422    pub(crate) timeout_block_height: u32,
1423    pub(crate) state: PaymentState,
1424    /// Swap metadata that is only valid when reading one from the local database
1425    #[derivative(PartialEq = "ignore")]
1426    pub(crate) metadata: SwapMetadata,
1427}
1428impl ReceiveSwap {
1429    pub(crate) fn get_claim_keypair(&self) -> Result<Keypair, PaymentError> {
1430        utils::decode_keypair(&self.claim_private_key).map_err(Into::into)
1431    }
1432
1433    pub(crate) fn claim_script(&self) -> Result<elements::Script> {
1434        Ok(self
1435            .get_swap_script()?
1436            .funding_addrs
1437            .ok_or(anyhow!("No funding address found"))?
1438            .script_pubkey())
1439    }
1440
1441    pub(crate) fn get_boltz_create_response(&self) -> Result<CreateReverseResponse, PaymentError> {
1442        let internal_create_response: crate::persist::receive::InternalCreateReverseResponse =
1443            serde_json::from_str(&self.create_response_json).map_err(|e| {
1444                PaymentError::Generic {
1445                    err: format!("Failed to deserialize InternalCreateReverseResponse: {e:?}"),
1446                }
1447            })?;
1448
1449        let res = CreateReverseResponse {
1450            id: self.id.clone(),
1451            invoice: Some(self.invoice.clone()),
1452            swap_tree: internal_create_response.swap_tree.clone().into(),
1453            lockup_address: internal_create_response.lockup_address.clone(),
1454            refund_public_key: crate::utils::json_to_pubkey(
1455                &internal_create_response.refund_public_key,
1456            )?,
1457            timeout_block_height: internal_create_response.timeout_block_height,
1458            onchain_amount: internal_create_response.onchain_amount,
1459            blinding_key: internal_create_response.blinding_key.clone(),
1460        };
1461        Ok(res)
1462    }
1463
1464    pub(crate) fn get_swap_script(&self) -> Result<LBtcSwapScript, PaymentError> {
1465        let keypair = self.get_claim_keypair()?;
1466        let create_response =
1467            self.get_boltz_create_response()
1468                .map_err(|e| PaymentError::Generic {
1469                    err: format!(
1470                        "Failed to create swap script for Receive Swap {}: {e:?}",
1471                        self.id
1472                    ),
1473                })?;
1474        LBtcSwapScript::reverse_from_swap_resp(&create_response, keypair.public_key().into())
1475            .map_err(|e| PaymentError::Generic {
1476                err: format!(
1477                    "Failed to create swap script for Receive Swap {}: {e:?}",
1478                    self.id
1479                ),
1480            })
1481    }
1482
1483    pub(crate) fn from_boltz_struct_to_json(
1484        create_response: &CreateReverseResponse,
1485        expected_swap_id: &str,
1486        expected_invoice: Option<&str>,
1487    ) -> Result<String, PaymentError> {
1488        let internal_create_response =
1489            crate::persist::receive::InternalCreateReverseResponse::try_convert_from_boltz(
1490                create_response,
1491                expected_swap_id,
1492                expected_invoice,
1493            )?;
1494
1495        let create_response_json =
1496            serde_json::to_string(&internal_create_response).map_err(|e| {
1497                PaymentError::Generic {
1498                    err: format!("Failed to serialize InternalCreateReverseResponse: {e:?}"),
1499                }
1500            })?;
1501
1502        Ok(create_response_json)
1503    }
1504}
1505
1506/// Returned when calling [crate::sdk::LiquidSdk::list_refundables].
1507#[derive(Clone, Debug, PartialEq, Serialize)]
1508pub struct RefundableSwap {
1509    pub swap_address: String,
1510    pub timestamp: u32,
1511    /// Amount that is refundable, from all UTXOs
1512    pub amount_sat: u64,
1513    /// The txid of the last broadcasted refund tx, if any
1514    pub last_refund_tx_id: Option<String>,
1515}
1516
1517/// A BOLT12 offer
1518#[derive(Clone, Debug, Derivative)]
1519#[derivative(PartialEq)]
1520pub(crate) struct Bolt12Offer {
1521    /// The BOLT12 offer string
1522    pub(crate) id: String,
1523    /// The description of the offer
1524    pub(crate) description: String,
1525    /// The private key used to create the offer
1526    pub(crate) private_key: String,
1527    /// The optional webhook URL where the offer invoice request will be sent
1528    pub(crate) webhook_url: Option<String>,
1529    /// The creation time of the offer
1530    pub(crate) created_at: u32,
1531}
1532impl Bolt12Offer {
1533    pub(crate) fn get_keypair(&self) -> Result<Keypair, SdkError> {
1534        utils::decode_keypair(&self.private_key)
1535    }
1536}
1537impl TryFrom<Bolt12Offer> for Offer {
1538    type Error = SdkError;
1539
1540    fn try_from(val: Bolt12Offer) -> Result<Self, Self::Error> {
1541        Offer::from_str(&val.id)
1542            .map_err(|e| SdkError::generic(format!("Failed to parse BOLT12 offer: {e:?}")))
1543    }
1544}
1545
1546/// The payment state of an individual payment.
1547#[derive(Clone, Copy, Debug, Default, EnumString, Eq, PartialEq, Serialize, Hash)]
1548#[strum(serialize_all = "lowercase")]
1549pub enum PaymentState {
1550    #[default]
1551    Created = 0,
1552
1553    /// ## Receive Swaps
1554    ///
1555    /// Covers the cases when
1556    /// - the lockup tx is seen in the mempool or
1557    /// - our claim tx is broadcast
1558    ///
1559    /// When the claim tx is broadcast, `claim_tx_id` is set in the swap.
1560    ///
1561    /// ## Send Swaps
1562    ///
1563    /// This is the status when our lockup tx was broadcast
1564    ///
1565    /// ## Chain Swaps
1566    ///
1567    /// This is the status when the user lockup tx was broadcast
1568    ///
1569    /// ## No swap data available
1570    ///
1571    /// If no associated swap is found, this indicates the underlying tx is not confirmed yet.
1572    Pending = 1,
1573
1574    /// ## Receive Swaps
1575    ///
1576    /// Covers the case when the claim tx is confirmed.
1577    ///
1578    /// ## Send and Chain Swaps
1579    ///
1580    /// This is the status when the claim tx is broadcast and we see it in the mempool.
1581    ///
1582    /// ## No swap data available
1583    ///
1584    /// If no associated swap is found, this indicates the underlying tx is confirmed.
1585    Complete = 2,
1586
1587    /// ## Receive Swaps
1588    ///
1589    /// This is the status when the swap failed for any reason and the Receive could not complete.
1590    ///
1591    /// ## Send and Chain Swaps
1592    ///
1593    /// This is the status when a swap refund was initiated and the refund tx is confirmed.
1594    Failed = 3,
1595
1596    /// ## Send and Outgoing Chain Swaps
1597    ///
1598    /// This covers the case when the swap state is still Created and the swap fails to reach the
1599    /// Pending state in time. The TimedOut state indicates the lockup tx should never be broadcast.
1600    TimedOut = 4,
1601
1602    /// ## Incoming Chain Swaps
1603    ///
1604    /// This covers the case when the swap failed for any reason and there is a user lockup tx.
1605    /// The swap in this case has to be manually refunded with a provided Bitcoin address
1606    Refundable = 5,
1607
1608    /// ## Send and Chain Swaps
1609    ///
1610    /// This is the status when a refund was initiated and/or our refund tx was broadcast
1611    ///
1612    /// When the refund tx is broadcast, `refund_tx_id` is set in the swap.
1613    RefundPending = 6,
1614
1615    /// ## Chain Swaps
1616    ///
1617    /// This is the state when the user needs to accept new fees before the payment can proceed.
1618    ///
1619    /// Use [LiquidSdk::fetch_payment_proposed_fees](crate::sdk::LiquidSdk::fetch_payment_proposed_fees)
1620    /// to find out the current fees and
1621    /// [LiquidSdk::accept_payment_proposed_fees](crate::sdk::LiquidSdk::accept_payment_proposed_fees)
1622    /// to accept them, allowing the payment to proceed.
1623    ///
1624    /// Otherwise, this payment can be immediately refunded using
1625    /// [prepare_refund](crate::sdk::LiquidSdk::prepare_refund)/[refund](crate::sdk::LiquidSdk::refund).
1626    WaitingFeeAcceptance = 7,
1627}
1628
1629impl ToSql for PaymentState {
1630    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
1631        Ok(rusqlite::types::ToSqlOutput::from(*self as i8))
1632    }
1633}
1634impl FromSql for PaymentState {
1635    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
1636        match value {
1637            ValueRef::Integer(i) => match i as u8 {
1638                0 => Ok(PaymentState::Created),
1639                1 => Ok(PaymentState::Pending),
1640                2 => Ok(PaymentState::Complete),
1641                3 => Ok(PaymentState::Failed),
1642                4 => Ok(PaymentState::TimedOut),
1643                5 => Ok(PaymentState::Refundable),
1644                6 => Ok(PaymentState::RefundPending),
1645                7 => Ok(PaymentState::WaitingFeeAcceptance),
1646                _ => Err(FromSqlError::OutOfRange(i)),
1647            },
1648            _ => Err(FromSqlError::InvalidType),
1649        }
1650    }
1651}
1652
1653impl PaymentState {
1654    pub(crate) fn is_refundable(&self) -> bool {
1655        matches!(
1656            self,
1657            PaymentState::Refundable
1658                | PaymentState::RefundPending
1659                | PaymentState::WaitingFeeAcceptance
1660        )
1661    }
1662}
1663
1664#[derive(Debug, Copy, Clone, Eq, EnumString, Display, Hash, PartialEq, Serialize)]
1665#[strum(serialize_all = "lowercase")]
1666pub enum PaymentType {
1667    Receive = 0,
1668    Send = 1,
1669}
1670impl From<Direction> for PaymentType {
1671    fn from(value: Direction) -> Self {
1672        match value {
1673            Direction::Incoming => Self::Receive,
1674            Direction::Outgoing => Self::Send,
1675        }
1676    }
1677}
1678impl ToSql for PaymentType {
1679    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
1680        Ok(rusqlite::types::ToSqlOutput::from(*self as i8))
1681    }
1682}
1683impl FromSql for PaymentType {
1684    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
1685        match value {
1686            ValueRef::Integer(i) => match i as u8 {
1687                0 => Ok(PaymentType::Receive),
1688                1 => Ok(PaymentType::Send),
1689                _ => Err(FromSqlError::OutOfRange(i)),
1690            },
1691            _ => Err(FromSqlError::InvalidType),
1692        }
1693    }
1694}
1695
1696#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
1697pub enum PaymentStatus {
1698    Pending = 0,
1699    Complete = 1,
1700}
1701impl ToSql for PaymentStatus {
1702    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
1703        Ok(rusqlite::types::ToSqlOutput::from(*self as i8))
1704    }
1705}
1706impl FromSql for PaymentStatus {
1707    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
1708        match value {
1709            ValueRef::Integer(i) => match i as u8 {
1710                0 => Ok(PaymentStatus::Pending),
1711                1 => Ok(PaymentStatus::Complete),
1712                _ => Err(FromSqlError::OutOfRange(i)),
1713            },
1714            _ => Err(FromSqlError::InvalidType),
1715        }
1716    }
1717}
1718
1719#[derive(Debug, Clone, Serialize)]
1720pub struct PaymentTxData {
1721    /// The tx ID of the transaction
1722    pub tx_id: String,
1723
1724    /// The point in time when the underlying tx was included in a block.
1725    pub timestamp: Option<u32>,
1726
1727    /// The asset id
1728    pub asset_id: String,
1729
1730    /// The onchain tx amount.
1731    ///
1732    /// In case of an outbound payment (Send), this is the payer amount. Otherwise it's the receiver amount.
1733    pub amount: u64,
1734
1735    /// The onchain fees of this tx
1736    pub fees_sat: u64,
1737
1738    pub payment_type: PaymentType,
1739
1740    /// Onchain tx status
1741    pub is_confirmed: bool,
1742
1743    /// Data to use in the `blinded` param when unblinding the transaction in an explorer.
1744    /// See: <https://docs.liquid.net/docs/unblinding-transactions>
1745    pub unblinding_data: Option<String>,
1746}
1747
1748#[derive(Debug, Clone, Serialize)]
1749pub enum PaymentSwapType {
1750    Receive,
1751    Send,
1752    Chain,
1753}
1754
1755#[derive(Debug, Clone, Serialize)]
1756pub struct PaymentSwapData {
1757    pub swap_id: String,
1758
1759    pub swap_type: PaymentSwapType,
1760
1761    /// Swap creation timestamp
1762    pub created_at: u32,
1763
1764    /// The height of the block at which the swap will no longer be valid
1765    pub expiration_blockheight: u32,
1766
1767    pub preimage: Option<String>,
1768    pub invoice: Option<String>,
1769    pub bolt12_offer: Option<String>,
1770    pub payment_hash: Option<String>,
1771    pub destination_pubkey: Option<String>,
1772    pub description: String,
1773
1774    /// Amount sent by the swap payer
1775    pub payer_amount_sat: u64,
1776
1777    /// Amount received by the swap receiver
1778    pub receiver_amount_sat: u64,
1779
1780    /// The swapper service fee
1781    pub swapper_fees_sat: u64,
1782
1783    pub refund_tx_id: Option<String>,
1784    pub refund_tx_amount_sat: Option<u64>,
1785
1786    /// Present only for chain swaps.
1787    /// It's the Bitcoin address that receives funds.
1788    pub bitcoin_address: Option<String>,
1789
1790    /// Payment status derived from the swap status
1791    pub status: PaymentState,
1792}
1793
1794/// Represents the payment LNURL info
1795#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
1796pub struct LnUrlInfo {
1797    pub ln_address: Option<String>,
1798    pub lnurl_pay_comment: Option<String>,
1799    pub lnurl_pay_domain: Option<String>,
1800    pub lnurl_pay_metadata: Option<String>,
1801    pub lnurl_pay_success_action: Option<SuccessActionProcessed>,
1802    pub lnurl_pay_unprocessed_success_action: Option<SuccessAction>,
1803    pub lnurl_withdraw_endpoint: Option<String>,
1804}
1805
1806/// Configuration for asset metadata. Each asset metadata item represents an entry in the
1807/// [Liquid Asset Registry](https://docs.liquid.net/docs/blockstream-liquid-asset-registry).
1808/// An example Liquid Asset in the registry would be [Tether USD](https://assets.blockstream.info/ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2.json>).
1809#[derive(Debug, Clone, Serialize)]
1810pub struct AssetMetadata {
1811    /// The asset id of the registered asset
1812    pub asset_id: String,
1813    /// The name of the asset
1814    pub name: String,
1815    /// The ticker of the asset
1816    pub ticker: String,
1817    /// The precision used to display the asset amount.
1818    /// For example, precision of 2 shifts the decimal 2 places left from the satoshi amount.
1819    pub precision: u8,
1820    /// The optional ID of the fiat currency used to represent the asset
1821    pub fiat_id: Option<String>,
1822}
1823
1824impl AssetMetadata {
1825    pub fn amount_to_sat(&self, amount: f64) -> u64 {
1826        (amount * (10_u64.pow(self.precision.into()) as f64)) as u64
1827    }
1828
1829    pub fn amount_from_sat(&self, amount_sat: u64) -> f64 {
1830        amount_sat as f64 / (10_u64.pow(self.precision.into()) as f64)
1831    }
1832}
1833
1834/// Represents the Liquid payment asset info. The asset info is derived from
1835/// the available [AssetMetadata] that is set in the [Config].
1836#[derive(Clone, Debug, PartialEq, Serialize)]
1837pub struct AssetInfo {
1838    /// The name of the asset
1839    pub name: String,
1840    /// The ticker of the asset
1841    pub ticker: String,
1842    /// The amount calculated from the satoshi amount of the transaction, having its
1843    /// decimal shifted to the left by the [precision](AssetMetadata::precision)
1844    pub amount: f64,
1845    /// The optional fees when paid using the asset, having its
1846    /// decimal shifted to the left by the [precision](AssetMetadata::precision)
1847    pub fees: Option<f64>,
1848}
1849
1850/// The specific details of a payment, depending on its type
1851#[derive(Debug, Clone, PartialEq, Serialize)]
1852#[allow(clippy::large_enum_variant)]
1853pub enum PaymentDetails {
1854    /// Swapping to or from Lightning
1855    Lightning {
1856        swap_id: String,
1857
1858        /// Represents the invoice description
1859        description: String,
1860
1861        /// The height of the block at which the swap will no longer be valid
1862        liquid_expiration_blockheight: u32,
1863
1864        /// The preimage of the paid invoice (proof of payment).
1865        preimage: Option<String>,
1866
1867        /// Represents the Bolt11/Bolt12 invoice associated with a payment
1868        /// In the case of a Send payment, this is the invoice paid by the swapper
1869        /// In the case of a Receive payment, this is the invoice paid by the user
1870        invoice: Option<String>,
1871
1872        bolt12_offer: Option<String>,
1873
1874        /// The payment hash of the invoice
1875        payment_hash: Option<String>,
1876
1877        /// The invoice destination/payee pubkey
1878        destination_pubkey: Option<String>,
1879
1880        /// The payment LNURL info
1881        lnurl_info: Option<LnUrlInfo>,
1882
1883        /// The BIP353 address used to resolve this payment
1884        bip353_address: Option<String>,
1885
1886        /// For a Receive payment, this is the claim tx id in case it has already been broadcast
1887        claim_tx_id: Option<String>,
1888
1889        /// For a Send swap which was refunded, this is the refund tx id
1890        refund_tx_id: Option<String>,
1891
1892        /// For a Send swap which was refunded, this is the refund amount
1893        refund_tx_amount_sat: Option<u64>,
1894    },
1895    /// Direct onchain payment to a Liquid address
1896    Liquid {
1897        /// Represents either a Liquid BIP21 URI or pure address
1898        destination: String,
1899
1900        /// Represents the BIP21 `message` field
1901        description: String,
1902
1903        /// The asset id
1904        asset_id: String,
1905
1906        /// The asset info derived from the [AssetMetadata]
1907        asset_info: Option<AssetInfo>,
1908
1909        /// The payment LNURL info
1910        lnurl_info: Option<LnUrlInfo>,
1911
1912        /// The BIP353 address used to resolve this payment
1913        bip353_address: Option<String>,
1914    },
1915    /// Swapping to or from the Bitcoin chain
1916    Bitcoin {
1917        swap_id: String,
1918
1919        /// The Bitcoin address that receives funds.
1920        bitcoin_address: String,
1921
1922        /// Represents the invoice description
1923        description: String,
1924
1925        /// For an amountless receive swap, this indicates if fees were automatically accepted.
1926        /// Fees are auto accepted when the swapper proposes fees that are within the initial
1927        /// estimate, plus the `onchain_fee_rate_leeway_sat_per_vbyte` set in the [Config], if any.
1928        auto_accepted_fees: bool,
1929
1930        /// The height of the Liquid block at which the swap will no longer be valid
1931        /// It should always be populated in case of an outgoing chain swap
1932        liquid_expiration_blockheight: Option<u32>,
1933
1934        /// The height of the Bitcoin block at which the swap will no longer be valid
1935        /// It should always be populated in case of an incoming chain swap
1936        bitcoin_expiration_blockheight: Option<u32>,
1937
1938        /// The lockup tx id that initiates the swap
1939        lockup_tx_id: Option<String>,
1940
1941        /// The claim tx id that claims the server lockup tx
1942        claim_tx_id: Option<String>,
1943
1944        /// For a Send swap which was refunded, this is the refund tx id
1945        refund_tx_id: Option<String>,
1946
1947        /// For a Send swap which was refunded, this is the refund amount
1948        refund_tx_amount_sat: Option<u64>,
1949    },
1950}
1951
1952impl PaymentDetails {
1953    pub(crate) fn get_swap_id(&self) -> Option<String> {
1954        match self {
1955            Self::Lightning { swap_id, .. } | Self::Bitcoin { swap_id, .. } => {
1956                Some(swap_id.clone())
1957            }
1958            Self::Liquid { .. } => None,
1959        }
1960    }
1961
1962    pub(crate) fn get_refund_tx_amount_sat(&self) -> Option<u64> {
1963        match self {
1964            Self::Lightning {
1965                refund_tx_amount_sat,
1966                ..
1967            }
1968            | Self::Bitcoin {
1969                refund_tx_amount_sat,
1970                ..
1971            } => *refund_tx_amount_sat,
1972            Self::Liquid { .. } => None,
1973        }
1974    }
1975
1976    pub(crate) fn get_description(&self) -> Option<String> {
1977        match self {
1978            Self::Lightning { description, .. }
1979            | Self::Bitcoin { description, .. }
1980            | Self::Liquid { description, .. } => Some(description.clone()),
1981        }
1982    }
1983
1984    pub(crate) fn is_lbtc_asset_id(&self, network: LiquidNetwork) -> bool {
1985        match self {
1986            Self::Liquid { asset_id, .. } => {
1987                asset_id.eq(&utils::lbtc_asset_id(network).to_string())
1988            }
1989            _ => true,
1990        }
1991    }
1992}
1993
1994/// Represents an SDK payment.
1995///
1996/// By default, this is an onchain tx. It may represent a swap, if swap metadata is available.
1997#[derive(Debug, Clone, PartialEq, Serialize)]
1998pub struct Payment {
1999    /// The destination associated with the payment, if it was created via our SDK.
2000    /// Can be either a Liquid/Bitcoin address, a Liquid BIP21 URI or an invoice
2001    pub destination: Option<String>,
2002
2003    pub tx_id: Option<String>,
2004
2005    /// Data to use in the `blinded` param when unblinding the transaction in an explorer.
2006    /// See: <https://docs.liquid.net/docs/unblinding-transactions>
2007    pub unblinding_data: Option<String>,
2008
2009    /// Composite timestamp that can be used for sorting or displaying the payment.
2010    ///
2011    /// If this payment has an associated swap, it is the swap creation time. Otherwise, the point
2012    /// in time when the underlying tx was included in a block. If there is no associated swap
2013    /// available and the underlying tx is not yet confirmed, the value is `now()`.
2014    pub timestamp: u32,
2015
2016    /// The payment amount, which corresponds to the onchain tx amount.
2017    ///
2018    /// In case of an outbound payment (Send), this is the payer amount. Otherwise it's the receiver amount.
2019    pub amount_sat: u64,
2020
2021    /// Represents the fees paid by this wallet for this payment.
2022    ///
2023    /// ### Swaps
2024    /// If there is an associated Send Swap, these fees represent the total fees paid by this wallet
2025    /// (the sender). It is the difference between the amount that was sent and the amount received.
2026    ///
2027    /// If there is an associated Receive Swap, these fees represent the total fees paid by this wallet
2028    /// (the receiver). It is also the difference between the amount that was sent and the amount received.
2029    ///
2030    /// ### Pure onchain txs
2031    /// If no swap is associated with this payment:
2032    /// - for Send payments, this is the onchain tx fee
2033    /// - for Receive payments, this is zero
2034    pub fees_sat: u64,
2035
2036    /// Service fees paid to the swapper service. This is only set for swaps (i.e. doesn't apply to
2037    /// direct Liquid payments).
2038    pub swapper_fees_sat: Option<u64>,
2039
2040    /// If it is a `Send` or `Receive` payment
2041    pub payment_type: PaymentType,
2042
2043    /// Composite status representing the overall status of the payment.
2044    ///
2045    /// If the tx has no associated swap, this reflects the onchain tx status (confirmed or not).
2046    ///
2047    /// If the tx has an associated swap, this is determined by the swap status (pending or complete).
2048    pub status: PaymentState,
2049
2050    /// The details of a payment, depending on its [destination](Payment::destination) and
2051    /// [type](Payment::payment_type)
2052    pub details: PaymentDetails,
2053}
2054impl Payment {
2055    pub(crate) fn from_pending_swap(
2056        swap: PaymentSwapData,
2057        payment_type: PaymentType,
2058        payment_details: PaymentDetails,
2059    ) -> Payment {
2060        let amount_sat = match payment_type {
2061            PaymentType::Receive => swap.receiver_amount_sat,
2062            PaymentType::Send => swap.payer_amount_sat,
2063        };
2064
2065        Payment {
2066            destination: swap.invoice.clone(),
2067            tx_id: None,
2068            unblinding_data: None,
2069            timestamp: swap.created_at,
2070            amount_sat,
2071            fees_sat: swap
2072                .payer_amount_sat
2073                .saturating_sub(swap.receiver_amount_sat),
2074            swapper_fees_sat: Some(swap.swapper_fees_sat),
2075            payment_type,
2076            status: swap.status,
2077            details: payment_details,
2078        }
2079    }
2080
2081    pub(crate) fn from_tx_data(
2082        tx: PaymentTxData,
2083        swap: Option<PaymentSwapData>,
2084        details: PaymentDetails,
2085    ) -> Payment {
2086        let (amount_sat, fees_sat) = match swap.as_ref() {
2087            Some(s) => match tx.payment_type {
2088                // For receive swaps, to avoid some edge case issues related to potential past
2089                // overpayments, we use the actual claim value as the final received amount
2090                // for fee calculation.
2091                PaymentType::Receive => (tx.amount, s.payer_amount_sat.saturating_sub(tx.amount)),
2092                PaymentType::Send => (
2093                    s.receiver_amount_sat,
2094                    s.payer_amount_sat.saturating_sub(s.receiver_amount_sat),
2095                ),
2096            },
2097            None => {
2098                let (amount_sat, fees_sat) = match tx.payment_type {
2099                    PaymentType::Receive => (tx.amount, 0),
2100                    PaymentType::Send => (tx.amount, tx.fees_sat),
2101                };
2102                // If the payment is a Liquid payment, we only show the amount if the asset
2103                // is LBTC and only show the fees if the asset info has no set fees
2104                match details {
2105                    PaymentDetails::Liquid {
2106                        asset_info: Some(ref asset_info),
2107                        ..
2108                    } if asset_info.ticker != "BTC" => (0, asset_info.fees.map_or(fees_sat, |_| 0)),
2109                    _ => (amount_sat, fees_sat),
2110                }
2111            }
2112        };
2113        Payment {
2114            tx_id: Some(tx.tx_id),
2115            unblinding_data: tx.unblinding_data,
2116            // When the swap is present and of type send and receive, we retrieve the destination from the invoice.
2117            // If it's a chain swap instead, we use the `bitcoin_address` field from the swap data.
2118            // Otherwise, we specify the Liquid address (BIP21 or pure), set in `payment_details.address`.
2119            destination: match &swap {
2120                Some(PaymentSwapData {
2121                    swap_type: PaymentSwapType::Receive,
2122                    invoice,
2123                    ..
2124                }) => invoice.clone(),
2125                Some(PaymentSwapData {
2126                    swap_type: PaymentSwapType::Send,
2127                    invoice,
2128                    bolt12_offer,
2129                    ..
2130                }) => bolt12_offer.clone().or(invoice.clone()),
2131                Some(PaymentSwapData {
2132                    swap_type: PaymentSwapType::Chain,
2133                    bitcoin_address,
2134                    ..
2135                }) => bitcoin_address.clone(),
2136                _ => match &details {
2137                    PaymentDetails::Liquid { destination, .. } => Some(destination.clone()),
2138                    _ => None,
2139                },
2140            },
2141            timestamp: tx
2142                .timestamp
2143                .or(swap.as_ref().map(|s| s.created_at))
2144                .unwrap_or(utils::now()),
2145            amount_sat,
2146            fees_sat,
2147            swapper_fees_sat: swap.as_ref().map(|s| s.swapper_fees_sat),
2148            payment_type: tx.payment_type,
2149            status: match &swap {
2150                Some(swap) => swap.status,
2151                None => match tx.is_confirmed {
2152                    true => PaymentState::Complete,
2153                    false => PaymentState::Pending,
2154                },
2155            },
2156            details,
2157        }
2158    }
2159
2160    pub(crate) fn get_refund_tx_id(&self) -> Option<String> {
2161        match self.details.clone() {
2162            PaymentDetails::Lightning { refund_tx_id, .. } => Some(refund_tx_id),
2163            PaymentDetails::Bitcoin { refund_tx_id, .. } => Some(refund_tx_id),
2164            PaymentDetails::Liquid { .. } => None,
2165        }
2166        .flatten()
2167    }
2168}
2169
2170/// Returned when calling [crate::sdk::LiquidSdk::recommended_fees].
2171#[derive(Deserialize, Serialize, Clone, Debug)]
2172#[serde(rename_all = "camelCase")]
2173pub struct RecommendedFees {
2174    pub fastest_fee: u64,
2175    pub half_hour_fee: u64,
2176    pub hour_fee: u64,
2177    pub economy_fee: u64,
2178    pub minimum_fee: u64,
2179}
2180
2181/// An argument of [PrepareBuyBitcoinRequest] when calling [crate::sdk::LiquidSdk::prepare_buy_bitcoin].
2182#[derive(Debug, Clone, Copy, EnumString, PartialEq, Serialize)]
2183pub enum BuyBitcoinProvider {
2184    #[strum(serialize = "moonpay")]
2185    Moonpay,
2186}
2187
2188/// An argument when calling [crate::sdk::LiquidSdk::prepare_buy_bitcoin].
2189#[derive(Debug, Serialize)]
2190pub struct PrepareBuyBitcoinRequest {
2191    pub provider: BuyBitcoinProvider,
2192    pub amount_sat: u64,
2193}
2194
2195/// Returned when calling [crate::sdk::LiquidSdk::prepare_buy_bitcoin].
2196#[derive(Clone, Debug, Serialize)]
2197pub struct PrepareBuyBitcoinResponse {
2198    pub provider: BuyBitcoinProvider,
2199    pub amount_sat: u64,
2200    pub fees_sat: u64,
2201}
2202
2203/// An argument when calling [crate::sdk::LiquidSdk::buy_bitcoin].
2204#[derive(Clone, Debug, Serialize)]
2205pub struct BuyBitcoinRequest {
2206    pub prepare_response: PrepareBuyBitcoinResponse,
2207
2208    /// The optional URL to redirect to after completing the buy.
2209    ///
2210    /// For Moonpay, see <https://dev.moonpay.com/docs/on-ramp-configure-user-journey-params>
2211    pub redirect_url: Option<String>,
2212}
2213
2214/// Internal SDK log entry used in the Uniffi and Dart bindings
2215#[derive(Clone, Debug)]
2216pub struct LogEntry {
2217    pub line: String,
2218    pub level: String,
2219}
2220
2221#[derive(Clone, Debug, Serialize, Deserialize)]
2222struct InternalLeaf {
2223    pub output: String,
2224    pub version: u8,
2225}
2226impl From<InternalLeaf> for Leaf {
2227    fn from(value: InternalLeaf) -> Self {
2228        Leaf {
2229            output: value.output,
2230            version: value.version,
2231        }
2232    }
2233}
2234impl From<Leaf> for InternalLeaf {
2235    fn from(value: Leaf) -> Self {
2236        InternalLeaf {
2237            output: value.output,
2238            version: value.version,
2239        }
2240    }
2241}
2242
2243#[derive(Clone, Debug, Serialize, Deserialize)]
2244pub(super) struct InternalSwapTree {
2245    claim_leaf: InternalLeaf,
2246    refund_leaf: InternalLeaf,
2247}
2248impl From<InternalSwapTree> for SwapTree {
2249    fn from(value: InternalSwapTree) -> Self {
2250        SwapTree {
2251            claim_leaf: value.claim_leaf.into(),
2252            refund_leaf: value.refund_leaf.into(),
2253        }
2254    }
2255}
2256impl From<SwapTree> for InternalSwapTree {
2257    fn from(value: SwapTree) -> Self {
2258        InternalSwapTree {
2259            claim_leaf: value.claim_leaf.into(),
2260            refund_leaf: value.refund_leaf.into(),
2261        }
2262    }
2263}
2264
2265/// An argument when calling [crate::sdk::LiquidSdk::prepare_lnurl_pay].
2266#[derive(Debug, Serialize)]
2267pub struct PrepareLnUrlPayRequest {
2268    /// The [LnUrlPayRequestData] returned by [parse]
2269    pub data: LnUrlPayRequestData,
2270    /// The amount to send
2271    pub amount: PayAmount,
2272    /// A BIP353 address, in case one was used in order to fetch the LNURL Pay request data.
2273    /// Returned by [parse].
2274    pub bip353_address: Option<String>,
2275    /// An optional comment for this payment
2276    pub comment: Option<String>,
2277    /// Validates that, if there is a URL success action, the URL domain matches
2278    /// the LNURL callback domain. Defaults to `true`
2279    pub validate_success_action_url: Option<bool>,
2280}
2281
2282/// Returned when calling [crate::sdk::LiquidSdk::prepare_lnurl_pay].
2283#[derive(Debug, Serialize)]
2284pub struct PrepareLnUrlPayResponse {
2285    /// The destination of the payment
2286    pub destination: SendDestination,
2287    /// The fees in satoshis to send the payment
2288    pub fees_sat: u64,
2289    /// The [LnUrlPayRequestData] returned by [parse]
2290    pub data: LnUrlPayRequestData,
2291    /// The amount to send
2292    pub amount: PayAmount,
2293    /// An optional comment for this payment
2294    pub comment: Option<String>,
2295    /// The unprocessed LUD-09 success action. This will be processed and decrypted if
2296    /// needed after calling [crate::sdk::LiquidSdk::lnurl_pay]
2297    pub success_action: Option<SuccessAction>,
2298}
2299
2300/// An argument when calling [crate::sdk::LiquidSdk::lnurl_pay].
2301#[derive(Debug, Serialize)]
2302pub struct LnUrlPayRequest {
2303    /// The response from calling [crate::sdk::LiquidSdk::prepare_lnurl_pay]
2304    pub prepare_response: PrepareLnUrlPayResponse,
2305}
2306
2307/// Contains the result of the entire LNURL-pay interaction, as reported by the LNURL endpoint.
2308///
2309/// * `EndpointSuccess` indicates the payment is complete. The endpoint may return a `SuccessActionProcessed`,
2310///   in which case, the wallet has to present it to the user as described in
2311///   <https://github.com/lnurl/luds/blob/luds/09.md>
2312///
2313/// * `EndpointError` indicates a generic issue the LNURL endpoint encountered, including a freetext
2314///   field with the reason.
2315///
2316/// * `PayError` indicates that an error occurred while trying to pay the invoice from the LNURL endpoint.
2317///   This includes the payment hash of the failed invoice and the failure reason.
2318#[derive(Serialize)]
2319#[allow(clippy::large_enum_variant)]
2320pub enum LnUrlPayResult {
2321    EndpointSuccess { data: LnUrlPaySuccessData },
2322    EndpointError { data: LnUrlErrorData },
2323    PayError { data: LnUrlPayErrorData },
2324}
2325
2326#[derive(Serialize)]
2327pub struct LnUrlPaySuccessData {
2328    pub payment: Payment,
2329    pub success_action: Option<SuccessActionProcessed>,
2330}
2331
2332#[derive(Debug, Clone)]
2333pub enum Transaction {
2334    Liquid(boltz_client::elements::Transaction),
2335    Bitcoin(boltz_client::bitcoin::Transaction),
2336}
2337
2338impl Transaction {
2339    pub(crate) fn txid(&self) -> String {
2340        match self {
2341            Transaction::Liquid(tx) => tx.txid().to_hex(),
2342            Transaction::Bitcoin(tx) => tx.compute_txid().to_hex(),
2343        }
2344    }
2345}
2346
2347#[derive(Debug, Clone)]
2348pub enum Utxo {
2349    Liquid(
2350        Box<(
2351            boltz_client::elements::OutPoint,
2352            boltz_client::elements::TxOut,
2353        )>,
2354    ),
2355    Bitcoin(
2356        (
2357            boltz_client::bitcoin::OutPoint,
2358            boltz_client::bitcoin::TxOut,
2359        ),
2360    ),
2361}
2362
2363impl Utxo {
2364    pub(crate) fn as_bitcoin(
2365        &self,
2366    ) -> Option<&(
2367        boltz_client::bitcoin::OutPoint,
2368        boltz_client::bitcoin::TxOut,
2369    )> {
2370        match self {
2371            Utxo::Liquid(_) => None,
2372            Utxo::Bitcoin(utxo) => Some(utxo),
2373        }
2374    }
2375
2376    pub(crate) fn as_liquid(
2377        &self,
2378    ) -> Option<
2379        Box<(
2380            boltz_client::elements::OutPoint,
2381            boltz_client::elements::TxOut,
2382        )>,
2383    > {
2384        match self {
2385            Utxo::Bitcoin(_) => None,
2386            Utxo::Liquid(utxo) => Some(utxo.clone()),
2387        }
2388    }
2389}
2390
2391/// An argument when calling [crate::sdk::LiquidSdk::fetch_payment_proposed_fees].
2392#[derive(Debug, Clone)]
2393pub struct FetchPaymentProposedFeesRequest {
2394    pub swap_id: String,
2395}
2396
2397/// Returned when calling [crate::sdk::LiquidSdk::fetch_payment_proposed_fees].
2398#[derive(Debug, Clone, Serialize)]
2399pub struct FetchPaymentProposedFeesResponse {
2400    pub swap_id: String,
2401    pub fees_sat: u64,
2402    /// Amount sent by the swap payer
2403    pub payer_amount_sat: u64,
2404    /// Amount that will be received if these fees are accepted
2405    pub receiver_amount_sat: u64,
2406}
2407
2408/// An argument when calling [crate::sdk::LiquidSdk::accept_payment_proposed_fees].
2409#[derive(Debug, Clone)]
2410pub struct AcceptPaymentProposedFeesRequest {
2411    pub response: FetchPaymentProposedFeesResponse,
2412}
2413
2414#[derive(Clone, Debug)]
2415pub struct History<T> {
2416    pub txid: T,
2417    /// Confirmation height of txid
2418    ///
2419    /// -1 means unconfirmed with unconfirmed parents
2420    ///  0 means unconfirmed with confirmed parents
2421    pub height: i32,
2422}
2423pub(crate) type LBtcHistory = History<elements::Txid>;
2424pub(crate) type BtcHistory = History<bitcoin::Txid>;
2425
2426impl<T> History<T> {
2427    pub(crate) fn confirmed(&self) -> bool {
2428        self.height > 0
2429    }
2430}
2431#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
2432impl From<electrum_client::GetHistoryRes> for BtcHistory {
2433    fn from(value: electrum_client::GetHistoryRes) -> Self {
2434        Self {
2435            txid: value.tx_hash,
2436            height: value.height,
2437        }
2438    }
2439}
2440impl From<lwk_wollet::History> for LBtcHistory {
2441    fn from(value: lwk_wollet::History) -> Self {
2442        Self::from(&value)
2443    }
2444}
2445impl From<&lwk_wollet::History> for LBtcHistory {
2446    fn from(value: &lwk_wollet::History) -> Self {
2447        Self {
2448            txid: value.txid,
2449            height: value.height,
2450        }
2451    }
2452}
2453pub(crate) type BtcScript = bitcoin::ScriptBuf;
2454pub(crate) type LBtcScript = elements::Script;
2455
2456#[derive(Clone, Debug)]
2457pub struct BtcScriptBalance {
2458    /// Confirmed balance in Satoshis for the address.
2459    pub confirmed: u64,
2460    /// Unconfirmed balance in Satoshis for the address.
2461    ///
2462    /// Some servers (e.g. `electrs`) return this as a negative value.
2463    pub unconfirmed: i64,
2464}
2465#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
2466impl From<electrum_client::GetBalanceRes> for BtcScriptBalance {
2467    fn from(val: electrum_client::GetBalanceRes) -> Self {
2468        Self {
2469            confirmed: val.confirmed,
2470            unconfirmed: val.unconfirmed,
2471        }
2472    }
2473}
2474
2475#[macro_export]
2476macro_rules! get_updated_fields {
2477    ($($var:ident),* $(,)?) => {{
2478        let mut options = Vec::new();
2479        $(
2480            if $var.is_some() {
2481                options.push(stringify!($var).to_string());
2482            }
2483        )*
2484        match options.len() > 0 {
2485            true => Some(options),
2486            false => None,
2487        }
2488    }};
2489}