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