1use anyhow::{anyhow, bail, Result};
2use bitcoin::{bip32, ScriptBuf};
3use boltz_client::{
4 boltz::{ChainPair, BOLTZ_MAINNET_URL_V2, BOLTZ_TESTNET_URL_V2},
5 network::{BitcoinChain, Chain, LiquidChain},
6 swaps::boltz::{
7 CreateChainResponse, CreateReverseResponse, CreateSubmarineResponse, Leaf, Side, SwapTree,
8 },
9 BtcSwapScript, Keypair, LBtcSwapScript,
10};
11use derivative::Derivative;
12use elements::AssetId;
13use lwk_wollet::ElementsNetwork;
14use maybe_sync::{MaybeSend, MaybeSync};
15use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef};
16use rusqlite::ToSql;
17use sdk_common::prelude::*;
18use sdk_common::utils::Arc;
19use sdk_common::{bitcoin::hashes::hex::ToHex, lightning_with_bolt12::offers::offer::Offer};
20use serde::{Deserialize, Serialize};
21use std::cmp::PartialEq;
22use std::path::PathBuf;
23use std::str::FromStr;
24use strum_macros::{Display, EnumString};
25
26use crate::{
27 bitcoin,
28 chain::bitcoin::esplora::EsploraBitcoinChainService,
29 chain::liquid::esplora::EsploraLiquidChainService,
30 chain::{bitcoin::BitcoinChainService, liquid::LiquidChainService},
31 elements,
32 error::{PaymentError, SdkError, SdkResult},
33 persist::model::PaymentTxBalance,
34 prelude::DEFAULT_EXTERNAL_INPUT_PARSERS,
35 receive_swap::DEFAULT_ZERO_CONF_MAX_SAT,
36 side_swap::api::{SIDESWAP_MAINNET_URL, SIDESWAP_TESTNET_URL},
37 utils,
38};
39
40pub const LIQUID_FEE_RATE_SAT_PER_VBYTE: f64 = 0.1;
42pub const LIQUID_FEE_RATE_MSAT_PER_VBYTE: f32 = (LIQUID_FEE_RATE_SAT_PER_VBYTE * 1000.0) as f32;
43pub const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology";
44pub const BREEZ_LIQUID_ESPLORA_URL: &str = "https://lq1.breez.technology/liquid/api";
45pub const BREEZ_SWAP_PROXY_URL: &str = "https://swap.breez.technology/v2";
46pub const DEFAULT_ONCHAIN_FEE_RATE_LEEWAY_SAT: u64 = 500;
47
48const SIDESWAP_API_KEY: &str = "97fb6a1dfa37ee6656af92ef79675cc03b8ac4c52e04655f41edbd5af888dcc2";
49
50#[derive(Clone, Debug, Serialize)]
51pub enum BlockchainExplorer {
52 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
53 Electrum { url: String },
54 Esplora {
55 url: String,
56 use_waterfalls: bool,
58 },
59}
60
61#[derive(Clone, Debug, Serialize)]
63pub struct Config {
64 pub liquid_explorer: BlockchainExplorer,
65 pub bitcoin_explorer: BlockchainExplorer,
66 pub working_dir: String,
70 pub network: LiquidNetwork,
71 pub payment_timeout_sec: u64,
73 pub sync_service_url: Option<String>,
76 pub zero_conf_max_amount_sat: Option<u64>,
79 pub breez_api_key: Option<String>,
81 pub external_input_parsers: Option<Vec<ExternalInputParser>>,
85 pub use_default_external_input_parsers: bool,
89 pub onchain_fee_rate_leeway_sat: Option<u64>,
96 pub asset_metadata: Option<Vec<AssetMetadata>>,
101 pub sideswap_api_key: Option<String>,
103 pub use_magic_routing_hints: bool,
105}
106
107impl Config {
108 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
109 pub fn mainnet(breez_api_key: Option<String>) -> Self {
110 Config {
111 liquid_explorer: BlockchainExplorer::Electrum {
112 url: "elements-mainnet.breez.technology:50002".to_string(),
113 },
114 bitcoin_explorer: BlockchainExplorer::Electrum {
115 url: "bitcoin-mainnet.blockstream.info:50002".to_string(),
116 },
117 working_dir: ".".to_string(),
118 network: LiquidNetwork::Mainnet,
119 payment_timeout_sec: 15,
120 sync_service_url: Some(BREEZ_SYNC_SERVICE_URL.to_string()),
121 zero_conf_max_amount_sat: None,
122 breez_api_key,
123 external_input_parsers: None,
124 use_default_external_input_parsers: true,
125 onchain_fee_rate_leeway_sat: None,
126 asset_metadata: None,
127 sideswap_api_key: Some(SIDESWAP_API_KEY.to_string()),
128 use_magic_routing_hints: true,
129 }
130 }
131
132 pub fn mainnet_esplora(breez_api_key: Option<String>) -> Self {
133 Config {
134 liquid_explorer: BlockchainExplorer::Esplora {
135 url: BREEZ_LIQUID_ESPLORA_URL.to_string(),
136 use_waterfalls: true,
137 },
138 bitcoin_explorer: BlockchainExplorer::Esplora {
139 url: "https://blockstream.info/api/".to_string(),
140 use_waterfalls: false,
141 },
142 working_dir: ".".to_string(),
143 network: LiquidNetwork::Mainnet,
144 payment_timeout_sec: 15,
145 sync_service_url: Some(BREEZ_SYNC_SERVICE_URL.to_string()),
146 zero_conf_max_amount_sat: None,
147 breez_api_key,
148 external_input_parsers: None,
149 use_default_external_input_parsers: true,
150 onchain_fee_rate_leeway_sat: None,
151 asset_metadata: None,
152 sideswap_api_key: Some(SIDESWAP_API_KEY.to_string()),
153 use_magic_routing_hints: true,
154 }
155 }
156
157 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
158 pub fn testnet(breez_api_key: Option<String>) -> Self {
159 Config {
160 liquid_explorer: BlockchainExplorer::Electrum {
161 url: "elements-testnet.blockstream.info:50002".to_string(),
162 },
163 bitcoin_explorer: BlockchainExplorer::Electrum {
164 url: "bitcoin-testnet.blockstream.info:50002".to_string(),
165 },
166 working_dir: ".".to_string(),
167 network: LiquidNetwork::Testnet,
168 payment_timeout_sec: 15,
169 sync_service_url: Some(BREEZ_SYNC_SERVICE_URL.to_string()),
170 zero_conf_max_amount_sat: None,
171 breez_api_key,
172 external_input_parsers: None,
173 use_default_external_input_parsers: true,
174 onchain_fee_rate_leeway_sat: None,
175 asset_metadata: None,
176 sideswap_api_key: Some(SIDESWAP_API_KEY.to_string()),
177 use_magic_routing_hints: true,
178 }
179 }
180
181 pub fn testnet_esplora(breez_api_key: Option<String>) -> Self {
182 Config {
183 liquid_explorer: BlockchainExplorer::Esplora {
184 url: "https://blockstream.info/liquidtestnet/api".to_string(),
185 use_waterfalls: false,
186 },
187 bitcoin_explorer: BlockchainExplorer::Esplora {
188 url: "https://blockstream.info/testnet/api/".to_string(),
189 use_waterfalls: false,
190 },
191 working_dir: ".".to_string(),
192 network: LiquidNetwork::Testnet,
193 payment_timeout_sec: 15,
194 sync_service_url: Some(BREEZ_SYNC_SERVICE_URL.to_string()),
195 zero_conf_max_amount_sat: None,
196 breez_api_key,
197 external_input_parsers: None,
198 use_default_external_input_parsers: true,
199 onchain_fee_rate_leeway_sat: None,
200 asset_metadata: None,
201 sideswap_api_key: Some(SIDESWAP_API_KEY.to_string()),
202 use_magic_routing_hints: true,
203 }
204 }
205
206 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
207 pub fn regtest() -> Self {
208 Config {
209 liquid_explorer: BlockchainExplorer::Electrum {
210 url: "localhost:19002".to_string(),
211 },
212 bitcoin_explorer: BlockchainExplorer::Electrum {
213 url: "localhost:19001".to_string(),
214 },
215 working_dir: ".".to_string(),
216 network: LiquidNetwork::Regtest,
217 payment_timeout_sec: 15,
218 sync_service_url: Some("http://localhost:8088".to_string()),
219 zero_conf_max_amount_sat: None,
220 breez_api_key: None,
221 external_input_parsers: None,
222 use_default_external_input_parsers: true,
223 onchain_fee_rate_leeway_sat: None,
224 asset_metadata: None,
225 sideswap_api_key: None,
226 use_magic_routing_hints: true,
227 }
228 }
229
230 pub fn regtest_esplora() -> Self {
231 Config {
232 liquid_explorer: BlockchainExplorer::Esplora {
233 url: "http://localhost:3120/api".to_string(),
234 use_waterfalls: true,
235 },
236 bitcoin_explorer: BlockchainExplorer::Esplora {
237 url: "http://localhost:4002/api".to_string(),
238 use_waterfalls: false,
239 },
240 working_dir: ".".to_string(),
241 network: LiquidNetwork::Regtest,
242 payment_timeout_sec: 15,
243 sync_service_url: Some("http://localhost:8089".to_string()),
244 zero_conf_max_amount_sat: None,
245 breez_api_key: None,
246 external_input_parsers: None,
247 use_default_external_input_parsers: true,
248 onchain_fee_rate_leeway_sat: None,
249 asset_metadata: None,
250 sideswap_api_key: None,
251 use_magic_routing_hints: true,
252 }
253 }
254
255 pub fn get_wallet_dir(&self, base_dir: &str, fingerprint_hex: &str) -> anyhow::Result<String> {
256 Ok(PathBuf::from(base_dir)
257 .join(match self.network {
258 LiquidNetwork::Mainnet => "mainnet",
259 LiquidNetwork::Testnet => "testnet",
260 LiquidNetwork::Regtest => "regtest",
261 })
262 .join(fingerprint_hex)
263 .to_str()
264 .ok_or(anyhow::anyhow!(
265 "Could not get retrieve current wallet directory"
266 ))?
267 .to_string())
268 }
269
270 pub fn zero_conf_max_amount_sat(&self) -> u64 {
271 self.zero_conf_max_amount_sat
272 .unwrap_or(DEFAULT_ZERO_CONF_MAX_SAT)
273 }
274
275 pub(crate) fn lbtc_asset_id(&self) -> String {
276 utils::lbtc_asset_id(self.network).to_string()
277 }
278
279 pub(crate) fn get_all_external_input_parsers(&self) -> Vec<ExternalInputParser> {
280 let mut external_input_parsers = Vec::new();
281 if self.use_default_external_input_parsers {
282 let default_parsers = DEFAULT_EXTERNAL_INPUT_PARSERS
283 .iter()
284 .map(|(id, regex, url)| ExternalInputParser {
285 provider_id: id.to_string(),
286 input_regex: regex.to_string(),
287 parser_url: url.to_string(),
288 })
289 .collect::<Vec<_>>();
290 external_input_parsers.extend(default_parsers);
291 }
292 external_input_parsers.extend(self.external_input_parsers.clone().unwrap_or_default());
293
294 external_input_parsers
295 }
296
297 pub(crate) fn default_boltz_url(&self) -> &str {
298 match self.network {
299 LiquidNetwork::Mainnet => {
300 if self.breez_api_key.is_some() {
301 BREEZ_SWAP_PROXY_URL
302 } else {
303 BOLTZ_MAINNET_URL_V2
304 }
305 }
306 LiquidNetwork::Testnet => BOLTZ_TESTNET_URL_V2,
307 LiquidNetwork::Regtest => "http://localhost:8387/v2",
309 }
310 }
311
312 pub fn sync_enabled(&self) -> bool {
313 self.sync_service_url.is_some()
314 }
315
316 pub(crate) fn bitcoin_chain_service(&self) -> Arc<dyn BitcoinChainService> {
317 match self.bitcoin_explorer {
318 BlockchainExplorer::Esplora { .. } => {
319 Arc::new(EsploraBitcoinChainService::new(self.clone()))
320 }
321 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
322 BlockchainExplorer::Electrum { .. } => Arc::new(
323 crate::chain::bitcoin::electrum::ElectrumBitcoinChainService::new(self.clone()),
324 ),
325 }
326 }
327
328 pub(crate) fn liquid_chain_service(&self) -> Result<Arc<dyn LiquidChainService>> {
329 match &self.liquid_explorer {
330 BlockchainExplorer::Esplora { url, .. } => {
331 if url == BREEZ_LIQUID_ESPLORA_URL && self.breez_api_key.is_none() {
332 bail!("Cannot start the Breez Esplora chain service without providing an API key. See https://sdk-doc-liquid.breez.technology/guide/getting_started.html#api-key")
333 }
334 Ok(Arc::new(EsploraLiquidChainService::new(self.clone())))
335 }
336 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
337 BlockchainExplorer::Electrum { .. } => Ok(Arc::new(
338 crate::chain::liquid::electrum::ElectrumLiquidChainService::new(self.clone()),
339 )),
340 }
341 }
342
343 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
344 pub(crate) fn electrum_tls_options(&self) -> (bool, bool) {
345 match self.network {
346 LiquidNetwork::Mainnet | LiquidNetwork::Testnet => (true, true),
347 LiquidNetwork::Regtest => (false, false),
348 }
349 }
350
351 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
352 pub(crate) fn electrum_client(
353 &self,
354 url: &str,
355 ) -> Result<lwk_wollet::ElectrumClient, lwk_wollet::Error> {
356 let (tls, validate_domain) = self.electrum_tls_options();
357 let electrum_url = lwk_wollet::ElectrumUrl::new(url, tls, validate_domain)?;
358 lwk_wollet::ElectrumClient::with_options(
359 &electrum_url,
360 lwk_wollet::ElectrumOptions { timeout: Some(3) },
361 )
362 }
363
364 pub(crate) fn sideswap_url(&self) -> &'static str {
365 match self.network {
366 LiquidNetwork::Mainnet => SIDESWAP_MAINNET_URL,
367 LiquidNetwork::Testnet => SIDESWAP_TESTNET_URL,
368 LiquidNetwork::Regtest => unimplemented!(),
369 }
370 }
371}
372
373#[derive(Debug, Display, Copy, Clone, PartialEq, Serialize)]
376pub enum LiquidNetwork {
377 Mainnet,
379 Testnet,
381 Regtest,
383}
384impl LiquidNetwork {
385 pub fn as_bitcoin_chain(&self) -> BitcoinChain {
386 match self {
387 LiquidNetwork::Mainnet => BitcoinChain::Bitcoin,
388 LiquidNetwork::Testnet => BitcoinChain::BitcoinTestnet,
389 LiquidNetwork::Regtest => BitcoinChain::BitcoinRegtest,
390 }
391 }
392}
393
394impl From<LiquidNetwork> for ElementsNetwork {
395 fn from(value: LiquidNetwork) -> Self {
396 match value {
397 LiquidNetwork::Mainnet => ElementsNetwork::Liquid,
398 LiquidNetwork::Testnet => ElementsNetwork::LiquidTestnet,
399 LiquidNetwork::Regtest => ElementsNetwork::ElementsRegtest {
400 policy_asset: AssetId::from_str(
401 "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225",
402 )
403 .unwrap(),
404 },
405 }
406 }
407}
408
409impl From<LiquidNetwork> for Chain {
410 fn from(value: LiquidNetwork) -> Self {
411 Chain::Liquid(value.into())
412 }
413}
414
415impl From<LiquidNetwork> for LiquidChain {
416 fn from(value: LiquidNetwork) -> Self {
417 match value {
418 LiquidNetwork::Mainnet => LiquidChain::Liquid,
419 LiquidNetwork::Testnet => LiquidChain::LiquidTestnet,
420 LiquidNetwork::Regtest => LiquidChain::LiquidRegtest,
421 }
422 }
423}
424
425impl TryFrom<&str> for LiquidNetwork {
426 type Error = anyhow::Error;
427
428 fn try_from(value: &str) -> Result<LiquidNetwork, anyhow::Error> {
429 match value.to_lowercase().as_str() {
430 "mainnet" => Ok(LiquidNetwork::Mainnet),
431 "testnet" => Ok(LiquidNetwork::Testnet),
432 "regtest" => Ok(LiquidNetwork::Regtest),
433 _ => Err(anyhow!("Invalid network")),
434 }
435 }
436}
437
438impl From<LiquidNetwork> for Network {
439 fn from(value: LiquidNetwork) -> Self {
440 match value {
441 LiquidNetwork::Mainnet => Self::Bitcoin,
442 LiquidNetwork::Testnet => Self::Testnet,
443 LiquidNetwork::Regtest => Self::Regtest,
444 }
445 }
446}
447
448impl From<LiquidNetwork> for sdk_common::bitcoin::Network {
449 fn from(value: LiquidNetwork) -> Self {
450 match value {
451 LiquidNetwork::Mainnet => Self::Bitcoin,
452 LiquidNetwork::Testnet => Self::Testnet,
453 LiquidNetwork::Regtest => Self::Regtest,
454 }
455 }
456}
457
458impl From<LiquidNetwork> for boltz_client::bitcoin::Network {
459 fn from(value: LiquidNetwork) -> Self {
460 match value {
461 LiquidNetwork::Mainnet => Self::Bitcoin,
462 LiquidNetwork::Testnet => Self::Testnet,
463 LiquidNetwork::Regtest => Self::Regtest,
464 }
465 }
466}
467
468pub trait EventListener: MaybeSend + MaybeSync {
470 fn on_event(&self, e: SdkEvent);
471}
472
473#[derive(Clone, Debug, PartialEq)]
476pub enum SdkEvent {
477 PaymentFailed {
478 details: Payment,
479 },
480 PaymentPending {
481 details: Payment,
482 },
483 PaymentRefundable {
484 details: Payment,
485 },
486 PaymentRefunded {
487 details: Payment,
488 },
489 PaymentRefundPending {
490 details: Payment,
491 },
492 PaymentSucceeded {
493 details: Payment,
494 },
495 PaymentWaitingConfirmation {
496 details: Payment,
497 },
498 PaymentWaitingFeeAcceptance {
499 details: Payment,
500 },
501 Synced,
503 DataSynced {
505 did_pull_new_records: bool,
507 },
508}
509
510#[derive(thiserror::Error, Debug)]
511pub enum SignerError {
512 #[error("Signer error: {err}")]
513 Generic { err: String },
514}
515
516impl From<anyhow::Error> for SignerError {
517 fn from(err: anyhow::Error) -> Self {
518 SignerError::Generic {
519 err: err.to_string(),
520 }
521 }
522}
523
524impl From<bip32::Error> for SignerError {
525 fn from(err: bip32::Error) -> Self {
526 SignerError::Generic {
527 err: err.to_string(),
528 }
529 }
530}
531
532pub trait Signer: MaybeSend + MaybeSync {
535 fn xpub(&self) -> Result<Vec<u8>, SignerError>;
538
539 fn derive_xpub(&self, derivation_path: String) -> Result<Vec<u8>, SignerError>;
545
546 fn sign_ecdsa(&self, msg: Vec<u8>, derivation_path: String) -> Result<Vec<u8>, SignerError>;
548
549 fn sign_ecdsa_recoverable(&self, msg: Vec<u8>) -> Result<Vec<u8>, SignerError>;
551
552 fn slip77_master_blinding_key(&self) -> Result<Vec<u8>, SignerError>;
554
555 fn hmac_sha256(&self, msg: Vec<u8>, derivation_path: String) -> Result<Vec<u8>, SignerError>;
558
559 fn ecies_encrypt(&self, msg: Vec<u8>) -> Result<Vec<u8>, SignerError>;
561
562 fn ecies_decrypt(&self, msg: Vec<u8>) -> Result<Vec<u8>, SignerError>;
564}
565
566pub struct ConnectRequest {
569 pub config: Config,
571 pub mnemonic: Option<String>,
573 pub passphrase: Option<String>,
575 pub seed: Option<Vec<u8>>,
577}
578
579pub struct ConnectWithSignerRequest {
580 pub config: Config,
581}
582
583#[derive(Clone, Debug)]
586pub(crate) struct ReservedAddress {
587 pub(crate) address: String,
589 pub(crate) expiry_block_height: u32,
591}
592
593#[derive(Clone, Debug, Serialize)]
595pub enum PaymentMethod {
596 #[deprecated(since = "0.8.1", note = "Use `Bolt11Invoice` instead")]
597 Lightning,
598 Bolt11Invoice,
599 Bolt12Offer,
600 BitcoinAddress,
601 LiquidAddress,
602}
603
604#[derive(Debug, Serialize, Clone)]
605pub enum ReceiveAmount {
606 Bitcoin { payer_amount_sat: u64 },
608
609 Asset {
611 asset_id: String,
612 payer_amount: Option<f64>,
613 },
614}
615
616#[derive(Debug, Serialize)]
618pub struct PrepareReceiveRequest {
619 pub payment_method: PaymentMethod,
620 pub amount: Option<ReceiveAmount>,
622}
623
624#[derive(Debug, Serialize, Clone)]
626pub struct PrepareReceiveResponse {
627 pub payment_method: PaymentMethod,
628 pub fees_sat: u64,
637 pub amount: Option<ReceiveAmount>,
639 pub min_payer_amount_sat: Option<u64>,
643 pub max_payer_amount_sat: Option<u64>,
647 pub swapper_feerate: Option<f64>,
651}
652
653#[derive(Debug, Serialize)]
655pub struct ReceivePaymentRequest {
656 pub prepare_response: PrepareReceiveResponse,
657 pub description: Option<String>,
659 pub use_description_hash: Option<bool>,
661 pub payer_note: Option<String>,
663}
664
665#[derive(Debug, Serialize)]
667pub struct ReceivePaymentResponse {
668 pub destination: String,
671}
672
673#[derive(Debug, Serialize)]
675pub struct CreateBolt12InvoiceRequest {
676 pub offer: String,
678 pub invoice_request: String,
680}
681
682#[derive(Debug, Serialize, Clone)]
684pub struct CreateBolt12InvoiceResponse {
685 pub invoice: String,
687}
688
689#[derive(Debug, Serialize)]
691pub struct Limits {
692 pub min_sat: u64,
693 pub max_sat: u64,
694 pub max_zero_conf_sat: u64,
695}
696
697#[derive(Debug, Serialize)]
699pub struct LightningPaymentLimitsResponse {
700 pub send: Limits,
702 pub receive: Limits,
704}
705
706#[derive(Debug, Serialize)]
708pub struct OnchainPaymentLimitsResponse {
709 pub send: Limits,
711 pub receive: Limits,
713}
714
715#[derive(Debug, Serialize, Clone)]
717pub struct PrepareSendRequest {
718 pub destination: String,
721 pub amount: Option<PayAmount>,
724}
725
726#[derive(Clone, Debug, Serialize)]
728pub enum SendDestination {
729 LiquidAddress {
730 address_data: liquid::LiquidAddressData,
731 bip353_address: Option<String>,
733 },
734 Bolt11 {
735 invoice: LNInvoice,
736 bip353_address: Option<String>,
738 },
739 Bolt12 {
740 offer: LNOffer,
741 receiver_amount_sat: u64,
742 bip353_address: Option<String>,
744 },
745}
746
747#[derive(Debug, Serialize, Clone)]
749pub struct PrepareSendResponse {
750 pub destination: SendDestination,
751 pub amount: Option<PayAmount>,
753 pub fees_sat: Option<u64>,
756 pub estimated_asset_fees: Option<f64>,
760 pub exchange_amount_sat: Option<u64>,
763}
764
765#[derive(Debug, Serialize)]
767pub struct SendPaymentRequest {
768 pub prepare_response: PrepareSendResponse,
769 pub use_asset_fees: Option<bool>,
771 pub payer_note: Option<String>,
773}
774
775#[derive(Debug, Serialize)]
777pub struct SendPaymentResponse {
778 pub payment: Payment,
779}
780
781pub(crate) struct SendPaymentViaSwapRequest {
782 pub(crate) invoice: String,
783 pub(crate) bolt12_offer: Option<String>,
784 pub(crate) payment_hash: String,
785 pub(crate) description: Option<String>,
786 pub(crate) receiver_amount_sat: u64,
787 pub(crate) fees_sat: u64,
788}
789
790#[derive(Debug, Serialize, Clone)]
792pub enum PayAmount {
793 Bitcoin { receiver_amount_sat: u64 },
795
796 Asset {
798 asset_id: String,
799 receiver_amount: f64,
800 estimate_asset_fees: Option<bool>,
801 pay_with_bitcoin: Option<bool>,
804 },
805
806 Drain,
808}
809
810#[derive(Debug, Serialize, Clone)]
812pub struct PreparePayOnchainRequest {
813 pub amount: PayAmount,
815 pub fee_rate_sat_per_vbyte: Option<u32>,
817}
818
819#[derive(Debug, Serialize, Clone)]
821pub struct PreparePayOnchainResponse {
822 pub receiver_amount_sat: u64,
823 pub claim_fees_sat: u64,
824 pub total_fees_sat: u64,
825}
826
827#[derive(Debug, Serialize)]
829pub struct PayOnchainRequest {
830 pub address: String,
831 pub prepare_response: PreparePayOnchainResponse,
832}
833
834#[derive(Debug, Serialize)]
836pub struct PrepareRefundRequest {
837 pub swap_address: String,
839 pub refund_address: String,
841 pub fee_rate_sat_per_vbyte: u32,
843}
844
845#[derive(Debug, Serialize)]
847pub struct PrepareRefundResponse {
848 pub tx_vsize: u32,
849 pub tx_fee_sat: u64,
850 pub last_refund_tx_id: Option<String>,
852}
853
854#[derive(Debug, Serialize)]
856pub struct RefundRequest {
857 pub swap_address: String,
859 pub refund_address: String,
861 pub fee_rate_sat_per_vbyte: u32,
863}
864
865#[derive(Debug, Serialize)]
867pub struct RefundResponse {
868 pub refund_tx_id: String,
869}
870
871#[derive(Clone, Debug, Default, Serialize, Deserialize)]
873pub struct AssetBalance {
874 pub asset_id: String,
875 pub balance_sat: u64,
876 pub name: Option<String>,
877 pub ticker: Option<String>,
878 pub balance: Option<f64>,
879}
880
881#[derive(Debug, Serialize, Deserialize, Default)]
882pub struct BlockchainInfo {
883 pub liquid_tip: u32,
884 pub bitcoin_tip: u32,
885}
886
887#[derive(Copy, Clone)]
888pub(crate) struct ChainTips {
889 pub liquid_tip: u32,
890 pub bitcoin_tip: Option<u32>,
891}
892
893#[derive(Debug, Serialize, Deserialize)]
894pub struct WalletInfo {
895 pub balance_sat: u64,
897 pub pending_send_sat: u64,
899 pub pending_receive_sat: u64,
901 pub fingerprint: String,
903 pub pubkey: String,
905 #[serde(default)]
907 pub asset_balances: Vec<AssetBalance>,
908}
909
910impl WalletInfo {
911 pub(crate) fn validate_sufficient_funds(
912 &self,
913 network: LiquidNetwork,
914 amount_sat: u64,
915 fees_sat: Option<u64>,
916 asset_id: &str,
917 ) -> Result<(), PaymentError> {
918 let fees_sat = fees_sat.unwrap_or(0);
919 if asset_id.eq(&utils::lbtc_asset_id(network).to_string()) {
920 ensure_sdk!(
921 amount_sat + fees_sat <= self.balance_sat,
922 PaymentError::InsufficientFunds
923 );
924 } else {
925 match self
926 .asset_balances
927 .iter()
928 .find(|ab| ab.asset_id.eq(asset_id))
929 {
930 Some(asset_balance) => ensure_sdk!(
931 amount_sat <= asset_balance.balance_sat && fees_sat <= self.balance_sat,
932 PaymentError::InsufficientFunds
933 ),
934 None => return Err(PaymentError::InsufficientFunds),
935 }
936 }
937 Ok(())
938 }
939}
940
941#[derive(Debug, Serialize, Deserialize)]
943pub struct GetInfoResponse {
944 pub wallet_info: WalletInfo,
946 #[serde(default)]
948 pub blockchain_info: BlockchainInfo,
949}
950
951#[derive(Clone, Debug, PartialEq)]
953pub struct SignMessageRequest {
954 pub message: String,
955}
956
957#[derive(Clone, Debug, PartialEq)]
959pub struct SignMessageResponse {
960 pub signature: String,
961}
962
963#[derive(Clone, Debug, PartialEq)]
965pub struct CheckMessageRequest {
966 pub message: String,
968 pub pubkey: String,
970 pub signature: String,
972}
973
974#[derive(Clone, Debug, PartialEq)]
976pub struct CheckMessageResponse {
977 pub is_valid: bool,
980}
981
982#[derive(Debug, Serialize)]
984pub struct BackupRequest {
985 pub backup_path: Option<String>,
992}
993
994#[derive(Debug, Serialize)]
996pub struct RestoreRequest {
997 pub backup_path: Option<String>,
998}
999
1000#[derive(Default)]
1002pub struct ListPaymentsRequest {
1003 pub filters: Option<Vec<PaymentType>>,
1004 pub states: Option<Vec<PaymentState>>,
1005 pub from_timestamp: Option<i64>,
1007 pub to_timestamp: Option<i64>,
1009 pub offset: Option<u32>,
1010 pub limit: Option<u32>,
1011 pub details: Option<ListPaymentDetails>,
1012 pub sort_ascending: Option<bool>,
1013}
1014
1015#[derive(Debug, Serialize)]
1017pub enum ListPaymentDetails {
1018 Liquid {
1020 asset_id: Option<String>,
1022 destination: Option<String>,
1024 },
1025
1026 Bitcoin {
1028 address: Option<String>,
1030 },
1031}
1032
1033#[derive(Debug, Serialize)]
1035pub enum GetPaymentRequest {
1036 PaymentHash { payment_hash: String },
1038 SwapId { swap_id: String },
1040}
1041
1042#[sdk_macros::async_trait]
1044pub(crate) trait BlockListener: MaybeSend + MaybeSync {
1045 async fn on_bitcoin_block(&self, height: u32);
1046 async fn on_liquid_block(&self, height: u32);
1047}
1048
1049#[derive(Clone, Debug)]
1051pub enum Swap {
1052 Chain(ChainSwap),
1053 Send(SendSwap),
1054 Receive(ReceiveSwap),
1055}
1056impl Swap {
1057 pub(crate) fn id(&self) -> String {
1058 match &self {
1059 Swap::Chain(ChainSwap { id, .. })
1060 | Swap::Send(SendSwap { id, .. })
1061 | Swap::Receive(ReceiveSwap { id, .. }) => id.clone(),
1062 }
1063 }
1064
1065 pub(crate) fn version(&self) -> u64 {
1066 match self {
1067 Swap::Chain(ChainSwap { metadata, .. })
1068 | Swap::Send(SendSwap { metadata, .. })
1069 | Swap::Receive(ReceiveSwap { metadata, .. }) => metadata.version,
1070 }
1071 }
1072
1073 pub(crate) fn set_version(&mut self, version: u64) {
1074 match self {
1075 Swap::Chain(chain_swap) => {
1076 chain_swap.metadata.version = version;
1077 }
1078 Swap::Send(send_swap) => {
1079 send_swap.metadata.version = version;
1080 }
1081 Swap::Receive(receive_swap) => {
1082 receive_swap.metadata.version = version;
1083 }
1084 }
1085 }
1086
1087 pub(crate) fn last_updated_at(&self) -> u32 {
1088 match self {
1089 Swap::Chain(ChainSwap { metadata, .. })
1090 | Swap::Send(SendSwap { metadata, .. })
1091 | Swap::Receive(ReceiveSwap { metadata, .. }) => metadata.last_updated_at,
1092 }
1093 }
1094}
1095impl From<ChainSwap> for Swap {
1096 fn from(swap: ChainSwap) -> Self {
1097 Self::Chain(swap)
1098 }
1099}
1100impl From<SendSwap> for Swap {
1101 fn from(swap: SendSwap) -> Self {
1102 Self::Send(swap)
1103 }
1104}
1105impl From<ReceiveSwap> for Swap {
1106 fn from(swap: ReceiveSwap) -> Self {
1107 Self::Receive(swap)
1108 }
1109}
1110
1111#[derive(Clone, Debug)]
1112pub(crate) enum SwapScriptV2 {
1113 Bitcoin(BtcSwapScript),
1114 Liquid(LBtcSwapScript),
1115}
1116impl SwapScriptV2 {
1117 pub(crate) fn as_bitcoin_script(&self) -> Result<BtcSwapScript> {
1118 match self {
1119 SwapScriptV2::Bitcoin(script) => Ok(script.clone()),
1120 _ => Err(anyhow!("Invalid chain")),
1121 }
1122 }
1123
1124 pub(crate) fn as_liquid_script(&self) -> Result<LBtcSwapScript> {
1125 match self {
1126 SwapScriptV2::Liquid(script) => Ok(script.clone()),
1127 _ => Err(anyhow!("Invalid chain")),
1128 }
1129 }
1130}
1131
1132#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
1133pub enum Direction {
1134 Incoming = 0,
1135 Outgoing = 1,
1136}
1137impl ToSql for Direction {
1138 fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
1139 Ok(rusqlite::types::ToSqlOutput::from(*self as i8))
1140 }
1141}
1142impl FromSql for Direction {
1143 fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
1144 match value {
1145 ValueRef::Integer(i) => match i as u8 {
1146 0 => Ok(Direction::Incoming),
1147 1 => Ok(Direction::Outgoing),
1148 _ => Err(FromSqlError::OutOfRange(i)),
1149 },
1150 _ => Err(FromSqlError::InvalidType),
1151 }
1152 }
1153}
1154
1155#[derive(Clone, Debug, Default)]
1156pub(crate) struct SwapMetadata {
1157 pub(crate) version: u64,
1159 pub(crate) last_updated_at: u32,
1160 pub(crate) is_local: bool,
1161}
1162
1163#[derive(Clone, Debug, Derivative)]
1167#[derivative(PartialEq)]
1168pub struct ChainSwap {
1169 pub(crate) id: String,
1170 pub(crate) direction: Direction,
1171 pub(crate) claim_address: Option<String>,
1174 pub(crate) lockup_address: String,
1175 pub(crate) refund_address: Option<String>,
1177 pub(crate) timeout_block_height: u32,
1178 pub(crate) preimage: String,
1179 pub(crate) description: Option<String>,
1180 pub(crate) payer_amount_sat: u64,
1182 pub(crate) actual_payer_amount_sat: Option<u64>,
1185 pub(crate) receiver_amount_sat: u64,
1187 pub(crate) accepted_receiver_amount_sat: Option<u64>,
1189 pub(crate) claim_fees_sat: u64,
1190 pub(crate) pair_fees_json: String,
1192 pub(crate) accept_zero_conf: bool,
1193 pub(crate) create_response_json: String,
1195 pub(crate) server_lockup_tx_id: Option<String>,
1197 pub(crate) user_lockup_tx_id: Option<String>,
1199 pub(crate) claim_tx_id: Option<String>,
1201 pub(crate) refund_tx_id: Option<String>,
1203 pub(crate) created_at: u32,
1204 pub(crate) state: PaymentState,
1205 pub(crate) claim_private_key: String,
1206 pub(crate) refund_private_key: String,
1207 pub(crate) auto_accepted_fees: bool,
1208 #[derivative(PartialEq = "ignore")]
1210 pub(crate) metadata: SwapMetadata,
1211}
1212impl ChainSwap {
1213 pub(crate) fn get_claim_keypair(&self) -> SdkResult<Keypair> {
1214 utils::decode_keypair(&self.claim_private_key)
1215 }
1216
1217 pub(crate) fn get_refund_keypair(&self) -> SdkResult<Keypair> {
1218 utils::decode_keypair(&self.refund_private_key)
1219 }
1220
1221 pub(crate) fn get_boltz_create_response(&self) -> Result<CreateChainResponse> {
1222 let internal_create_response: crate::persist::chain::InternalCreateChainResponse =
1223 serde_json::from_str(&self.create_response_json).map_err(|e| {
1224 anyhow!("Failed to deserialize InternalCreateSubmarineResponse: {e:?}")
1225 })?;
1226
1227 Ok(CreateChainResponse {
1228 id: self.id.clone(),
1229 claim_details: internal_create_response.claim_details,
1230 lockup_details: internal_create_response.lockup_details,
1231 })
1232 }
1233
1234 pub(crate) fn get_boltz_pair(&self) -> Result<ChainPair> {
1235 let pair: ChainPair = serde_json::from_str(&self.pair_fees_json)
1236 .map_err(|e| anyhow!("Failed to deserialize ChainPair: {e:?}"))?;
1237
1238 Ok(pair)
1239 }
1240
1241 pub(crate) fn get_claim_swap_script(&self) -> SdkResult<SwapScriptV2> {
1242 let chain_swap_details = self.get_boltz_create_response()?.claim_details;
1243 let our_pubkey = self.get_claim_keypair()?.public_key();
1244 let swap_script = match self.direction {
1245 Direction::Incoming => SwapScriptV2::Liquid(LBtcSwapScript::chain_from_swap_resp(
1246 Side::Claim,
1247 chain_swap_details,
1248 our_pubkey.into(),
1249 )?),
1250 Direction::Outgoing => SwapScriptV2::Bitcoin(BtcSwapScript::chain_from_swap_resp(
1251 Side::Claim,
1252 chain_swap_details,
1253 our_pubkey.into(),
1254 )?),
1255 };
1256 Ok(swap_script)
1257 }
1258
1259 pub(crate) fn get_lockup_swap_script(&self) -> SdkResult<SwapScriptV2> {
1260 let chain_swap_details = self.get_boltz_create_response()?.lockup_details;
1261 let our_pubkey = self.get_refund_keypair()?.public_key();
1262 let swap_script = match self.direction {
1263 Direction::Incoming => SwapScriptV2::Bitcoin(BtcSwapScript::chain_from_swap_resp(
1264 Side::Lockup,
1265 chain_swap_details,
1266 our_pubkey.into(),
1267 )?),
1268 Direction::Outgoing => SwapScriptV2::Liquid(LBtcSwapScript::chain_from_swap_resp(
1269 Side::Lockup,
1270 chain_swap_details,
1271 our_pubkey.into(),
1272 )?),
1273 };
1274 Ok(swap_script)
1275 }
1276
1277 pub(crate) fn get_receive_lockup_swap_script_pubkey(
1279 &self,
1280 network: LiquidNetwork,
1281 ) -> SdkResult<ScriptBuf> {
1282 let swap_script = self.get_lockup_swap_script()?.as_bitcoin_script()?;
1283 let script_pubkey = swap_script
1284 .to_address(network.as_bitcoin_chain())
1285 .map_err(|e| SdkError::generic(format!("Error getting script address: {e:?}")))?
1286 .script_pubkey();
1287 Ok(script_pubkey)
1288 }
1289
1290 pub(crate) fn to_refundable(&self, amount_sat: u64) -> RefundableSwap {
1291 RefundableSwap {
1292 swap_address: self.lockup_address.clone(),
1293 timestamp: self.created_at,
1294 amount_sat,
1295 last_refund_tx_id: self.refund_tx_id.clone(),
1296 }
1297 }
1298
1299 pub(crate) fn from_boltz_struct_to_json(
1300 create_response: &CreateChainResponse,
1301 expected_swap_id: &str,
1302 ) -> Result<String, PaymentError> {
1303 let internal_create_response =
1304 crate::persist::chain::InternalCreateChainResponse::try_convert_from_boltz(
1305 create_response,
1306 expected_swap_id,
1307 )?;
1308
1309 let create_response_json =
1310 serde_json::to_string(&internal_create_response).map_err(|e| {
1311 PaymentError::Generic {
1312 err: format!("Failed to serialize InternalCreateChainResponse: {e:?}"),
1313 }
1314 })?;
1315
1316 Ok(create_response_json)
1317 }
1318
1319 pub(crate) fn is_waiting_fee_acceptance(&self) -> bool {
1320 self.payer_amount_sat == 0 && self.accepted_receiver_amount_sat.is_none()
1321 }
1322}
1323
1324#[derive(Clone, Debug, Default)]
1325pub(crate) struct ChainSwapUpdate {
1326 pub(crate) swap_id: String,
1327 pub(crate) to_state: PaymentState,
1328 pub(crate) server_lockup_tx_id: Option<String>,
1329 pub(crate) user_lockup_tx_id: Option<String>,
1330 pub(crate) claim_address: Option<String>,
1331 pub(crate) claim_tx_id: Option<String>,
1332 pub(crate) refund_tx_id: Option<String>,
1333}
1334
1335#[derive(Clone, Debug, Derivative)]
1337#[derivative(PartialEq)]
1338pub struct SendSwap {
1339 pub(crate) id: String,
1340 pub(crate) invoice: String,
1342 pub(crate) bolt12_offer: Option<String>,
1344 pub(crate) payment_hash: Option<String>,
1345 pub(crate) destination_pubkey: Option<String>,
1346 pub(crate) description: Option<String>,
1347 pub(crate) preimage: Option<String>,
1348 pub(crate) payer_amount_sat: u64,
1349 pub(crate) receiver_amount_sat: u64,
1350 pub(crate) pair_fees_json: String,
1352 pub(crate) create_response_json: String,
1354 pub(crate) lockup_tx_id: Option<String>,
1356 pub(crate) refund_address: Option<String>,
1358 pub(crate) refund_tx_id: Option<String>,
1360 pub(crate) created_at: u32,
1361 pub(crate) timeout_block_height: u64,
1362 pub(crate) state: PaymentState,
1363 pub(crate) refund_private_key: String,
1364 #[derivative(PartialEq = "ignore")]
1366 pub(crate) metadata: SwapMetadata,
1367}
1368impl SendSwap {
1369 pub(crate) fn get_refund_keypair(&self) -> Result<Keypair, SdkError> {
1370 utils::decode_keypair(&self.refund_private_key)
1371 }
1372
1373 pub(crate) fn get_boltz_create_response(&self) -> Result<CreateSubmarineResponse> {
1374 let internal_create_response: crate::persist::send::InternalCreateSubmarineResponse =
1375 serde_json::from_str(&self.create_response_json).map_err(|e| {
1376 anyhow!("Failed to deserialize InternalCreateSubmarineResponse: {e:?}")
1377 })?;
1378
1379 let res = CreateSubmarineResponse {
1380 id: self.id.clone(),
1381 accept_zero_conf: internal_create_response.accept_zero_conf,
1382 address: internal_create_response.address.clone(),
1383 bip21: internal_create_response.bip21.clone(),
1384 claim_public_key: crate::utils::json_to_pubkey(
1385 &internal_create_response.claim_public_key,
1386 )?,
1387 expected_amount: internal_create_response.expected_amount,
1388 referral_id: internal_create_response.referral_id,
1389 swap_tree: internal_create_response.swap_tree.clone().into(),
1390 timeout_block_height: internal_create_response.timeout_block_height,
1391 blinding_key: internal_create_response.blinding_key.clone(),
1392 };
1393 Ok(res)
1394 }
1395
1396 pub(crate) fn get_swap_script(&self) -> Result<LBtcSwapScript, SdkError> {
1397 LBtcSwapScript::submarine_from_swap_resp(
1398 &self.get_boltz_create_response()?,
1399 self.get_refund_keypair()?.public_key().into(),
1400 )
1401 .map_err(|e| {
1402 SdkError::generic(format!(
1403 "Failed to create swap script for Send Swap {}: {e:?}",
1404 self.id
1405 ))
1406 })
1407 }
1408
1409 pub(crate) fn from_boltz_struct_to_json(
1410 create_response: &CreateSubmarineResponse,
1411 expected_swap_id: &str,
1412 ) -> Result<String, PaymentError> {
1413 let internal_create_response =
1414 crate::persist::send::InternalCreateSubmarineResponse::try_convert_from_boltz(
1415 create_response,
1416 expected_swap_id,
1417 )?;
1418
1419 let create_response_json =
1420 serde_json::to_string(&internal_create_response).map_err(|e| {
1421 PaymentError::Generic {
1422 err: format!("Failed to serialize InternalCreateSubmarineResponse: {e:?}"),
1423 }
1424 })?;
1425
1426 Ok(create_response_json)
1427 }
1428}
1429
1430#[derive(Clone, Debug, Derivative)]
1432#[derivative(PartialEq)]
1433pub struct ReceiveSwap {
1434 pub(crate) id: String,
1435 pub(crate) preimage: String,
1436 pub(crate) create_response_json: String,
1438 pub(crate) claim_private_key: String,
1439 pub(crate) invoice: String,
1440 pub(crate) bolt12_offer: Option<String>,
1442 pub(crate) payment_hash: Option<String>,
1443 pub(crate) destination_pubkey: Option<String>,
1444 pub(crate) description: Option<String>,
1445 pub(crate) payer_note: Option<String>,
1446 pub(crate) payer_amount_sat: u64,
1448 pub(crate) receiver_amount_sat: u64,
1449 pub(crate) pair_fees_json: String,
1451 pub(crate) claim_fees_sat: u64,
1452 pub(crate) claim_address: Option<String>,
1454 pub(crate) claim_tx_id: Option<String>,
1456 pub(crate) lockup_tx_id: Option<String>,
1458 pub(crate) mrh_address: String,
1460 pub(crate) mrh_tx_id: Option<String>,
1462 pub(crate) created_at: u32,
1465 pub(crate) timeout_block_height: u32,
1466 pub(crate) state: PaymentState,
1467 #[derivative(PartialEq = "ignore")]
1469 pub(crate) metadata: SwapMetadata,
1470}
1471impl ReceiveSwap {
1472 pub(crate) fn get_claim_keypair(&self) -> Result<Keypair, PaymentError> {
1473 utils::decode_keypair(&self.claim_private_key).map_err(Into::into)
1474 }
1475
1476 pub(crate) fn claim_script(&self) -> Result<elements::Script> {
1477 Ok(self
1478 .get_swap_script()?
1479 .funding_addrs
1480 .ok_or(anyhow!("No funding address found"))?
1481 .script_pubkey())
1482 }
1483
1484 pub(crate) fn get_boltz_create_response(&self) -> Result<CreateReverseResponse, PaymentError> {
1485 let internal_create_response: crate::persist::receive::InternalCreateReverseResponse =
1486 serde_json::from_str(&self.create_response_json).map_err(|e| {
1487 PaymentError::Generic {
1488 err: format!("Failed to deserialize InternalCreateReverseResponse: {e:?}"),
1489 }
1490 })?;
1491
1492 let res = CreateReverseResponse {
1493 id: self.id.clone(),
1494 invoice: Some(self.invoice.clone()),
1495 swap_tree: internal_create_response.swap_tree.clone().into(),
1496 lockup_address: internal_create_response.lockup_address.clone(),
1497 refund_public_key: crate::utils::json_to_pubkey(
1498 &internal_create_response.refund_public_key,
1499 )?,
1500 timeout_block_height: internal_create_response.timeout_block_height,
1501 onchain_amount: internal_create_response.onchain_amount,
1502 blinding_key: internal_create_response.blinding_key.clone(),
1503 };
1504 Ok(res)
1505 }
1506
1507 pub(crate) fn get_swap_script(&self) -> Result<LBtcSwapScript, PaymentError> {
1508 let keypair = self.get_claim_keypair()?;
1509 let create_response =
1510 self.get_boltz_create_response()
1511 .map_err(|e| PaymentError::Generic {
1512 err: format!(
1513 "Failed to create swap script for Receive Swap {}: {e:?}",
1514 self.id
1515 ),
1516 })?;
1517 LBtcSwapScript::reverse_from_swap_resp(&create_response, keypair.public_key().into())
1518 .map_err(|e| PaymentError::Generic {
1519 err: format!(
1520 "Failed to create swap script for Receive Swap {}: {e:?}",
1521 self.id
1522 ),
1523 })
1524 }
1525
1526 pub(crate) fn from_boltz_struct_to_json(
1527 create_response: &CreateReverseResponse,
1528 expected_swap_id: &str,
1529 expected_invoice: Option<&str>,
1530 ) -> Result<String, PaymentError> {
1531 let internal_create_response =
1532 crate::persist::receive::InternalCreateReverseResponse::try_convert_from_boltz(
1533 create_response,
1534 expected_swap_id,
1535 expected_invoice,
1536 )?;
1537
1538 let create_response_json =
1539 serde_json::to_string(&internal_create_response).map_err(|e| {
1540 PaymentError::Generic {
1541 err: format!("Failed to serialize InternalCreateReverseResponse: {e:?}"),
1542 }
1543 })?;
1544
1545 Ok(create_response_json)
1546 }
1547}
1548
1549#[derive(Clone, Debug, PartialEq, Serialize)]
1551pub struct RefundableSwap {
1552 pub swap_address: String,
1553 pub timestamp: u32,
1554 pub amount_sat: u64,
1556 pub last_refund_tx_id: Option<String>,
1558}
1559
1560#[derive(Clone, Debug, Derivative)]
1562#[derivative(PartialEq)]
1563pub(crate) struct Bolt12Offer {
1564 pub(crate) id: String,
1566 pub(crate) description: String,
1568 pub(crate) private_key: String,
1570 pub(crate) webhook_url: Option<String>,
1572 pub(crate) created_at: u32,
1574}
1575impl Bolt12Offer {
1576 pub(crate) fn get_keypair(&self) -> Result<Keypair, SdkError> {
1577 utils::decode_keypair(&self.private_key)
1578 }
1579}
1580impl TryFrom<Bolt12Offer> for Offer {
1581 type Error = SdkError;
1582
1583 fn try_from(val: Bolt12Offer) -> Result<Self, Self::Error> {
1584 Offer::from_str(&val.id)
1585 .map_err(|e| SdkError::generic(format!("Failed to parse BOLT12 offer: {e:?}")))
1586 }
1587}
1588
1589#[derive(Clone, Copy, Debug, Default, EnumString, Eq, PartialEq, Serialize, Hash)]
1591#[strum(serialize_all = "lowercase")]
1592pub enum PaymentState {
1593 #[default]
1594 Created = 0,
1595
1596 Pending = 1,
1616
1617 Complete = 2,
1629
1630 Failed = 3,
1638
1639 TimedOut = 4,
1644
1645 Refundable = 5,
1650
1651 RefundPending = 6,
1657
1658 WaitingFeeAcceptance = 7,
1670}
1671
1672impl ToSql for PaymentState {
1673 fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
1674 Ok(rusqlite::types::ToSqlOutput::from(*self as i8))
1675 }
1676}
1677impl FromSql for PaymentState {
1678 fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
1679 match value {
1680 ValueRef::Integer(i) => match i as u8 {
1681 0 => Ok(PaymentState::Created),
1682 1 => Ok(PaymentState::Pending),
1683 2 => Ok(PaymentState::Complete),
1684 3 => Ok(PaymentState::Failed),
1685 4 => Ok(PaymentState::TimedOut),
1686 5 => Ok(PaymentState::Refundable),
1687 6 => Ok(PaymentState::RefundPending),
1688 7 => Ok(PaymentState::WaitingFeeAcceptance),
1689 _ => Err(FromSqlError::OutOfRange(i)),
1690 },
1691 _ => Err(FromSqlError::InvalidType),
1692 }
1693 }
1694}
1695
1696impl PaymentState {
1697 pub(crate) fn is_refundable(&self) -> bool {
1698 matches!(
1699 self,
1700 PaymentState::Refundable
1701 | PaymentState::RefundPending
1702 | PaymentState::WaitingFeeAcceptance
1703 )
1704 }
1705}
1706
1707#[derive(Debug, Copy, Clone, Eq, EnumString, Display, Hash, PartialEq, Serialize)]
1708#[strum(serialize_all = "lowercase")]
1709pub enum PaymentType {
1710 Receive = 0,
1711 Send = 1,
1712}
1713impl From<Direction> for PaymentType {
1714 fn from(value: Direction) -> Self {
1715 match value {
1716 Direction::Incoming => Self::Receive,
1717 Direction::Outgoing => Self::Send,
1718 }
1719 }
1720}
1721impl ToSql for PaymentType {
1722 fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
1723 Ok(rusqlite::types::ToSqlOutput::from(*self as i8))
1724 }
1725}
1726impl FromSql for PaymentType {
1727 fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
1728 match value {
1729 ValueRef::Integer(i) => match i as u8 {
1730 0 => Ok(PaymentType::Receive),
1731 1 => Ok(PaymentType::Send),
1732 _ => Err(FromSqlError::OutOfRange(i)),
1733 },
1734 _ => Err(FromSqlError::InvalidType),
1735 }
1736 }
1737}
1738
1739#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
1740pub enum PaymentStatus {
1741 Pending = 0,
1742 Complete = 1,
1743}
1744impl ToSql for PaymentStatus {
1745 fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
1746 Ok(rusqlite::types::ToSqlOutput::from(*self as i8))
1747 }
1748}
1749impl FromSql for PaymentStatus {
1750 fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
1751 match value {
1752 ValueRef::Integer(i) => match i as u8 {
1753 0 => Ok(PaymentStatus::Pending),
1754 1 => Ok(PaymentStatus::Complete),
1755 _ => Err(FromSqlError::OutOfRange(i)),
1756 },
1757 _ => Err(FromSqlError::InvalidType),
1758 }
1759 }
1760}
1761
1762#[derive(Debug, Clone, Serialize)]
1763pub struct PaymentTxData {
1764 pub tx_id: String,
1766
1767 pub timestamp: Option<u32>,
1769
1770 pub fees_sat: u64,
1772
1773 pub is_confirmed: bool,
1775
1776 pub unblinding_data: Option<String>,
1779}
1780
1781#[derive(Debug, Clone, Serialize)]
1782pub enum PaymentSwapType {
1783 Receive,
1784 Send,
1785 Chain,
1786}
1787
1788#[derive(Debug, Clone, Serialize)]
1789pub struct PaymentSwapData {
1790 pub swap_id: String,
1791
1792 pub swap_type: PaymentSwapType,
1793
1794 pub created_at: u32,
1796
1797 pub expiration_blockheight: u32,
1799
1800 pub preimage: Option<String>,
1801 pub invoice: Option<String>,
1802 pub bolt12_offer: Option<String>,
1803 pub payment_hash: Option<String>,
1804 pub destination_pubkey: Option<String>,
1805 pub description: String,
1806 pub payer_note: Option<String>,
1807
1808 pub payer_amount_sat: u64,
1810
1811 pub receiver_amount_sat: u64,
1813
1814 pub swapper_fees_sat: u64,
1816
1817 pub refund_tx_id: Option<String>,
1818 pub refund_tx_amount_sat: Option<u64>,
1819
1820 pub bitcoin_address: Option<String>,
1823
1824 pub status: PaymentState,
1826}
1827
1828#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
1830pub struct LnUrlInfo {
1831 pub ln_address: Option<String>,
1832 pub lnurl_pay_comment: Option<String>,
1833 pub lnurl_pay_domain: Option<String>,
1834 pub lnurl_pay_metadata: Option<String>,
1835 pub lnurl_pay_success_action: Option<SuccessActionProcessed>,
1836 pub lnurl_pay_unprocessed_success_action: Option<SuccessAction>,
1837 pub lnurl_withdraw_endpoint: Option<String>,
1838}
1839
1840#[derive(Debug, Clone, Serialize)]
1844pub struct AssetMetadata {
1845 pub asset_id: String,
1847 pub name: String,
1849 pub ticker: String,
1851 pub precision: u8,
1854 pub fiat_id: Option<String>,
1856}
1857
1858impl AssetMetadata {
1859 pub fn amount_to_sat(&self, amount: f64) -> u64 {
1860 (amount * (10_u64.pow(self.precision.into()) as f64)) as u64
1861 }
1862
1863 pub fn amount_from_sat(&self, amount_sat: u64) -> f64 {
1864 amount_sat as f64 / (10_u64.pow(self.precision.into()) as f64)
1865 }
1866}
1867
1868#[derive(Clone, Debug, PartialEq, Serialize)]
1871pub struct AssetInfo {
1872 pub name: String,
1874 pub ticker: String,
1876 pub amount: f64,
1879 pub fees: Option<f64>,
1882}
1883
1884#[derive(Debug, Clone, PartialEq, Serialize)]
1886#[allow(clippy::large_enum_variant)]
1887pub enum PaymentDetails {
1888 Lightning {
1890 swap_id: String,
1891
1892 description: String,
1894
1895 liquid_expiration_blockheight: u32,
1897
1898 preimage: Option<String>,
1900
1901 invoice: Option<String>,
1905
1906 bolt12_offer: Option<String>,
1907
1908 payment_hash: Option<String>,
1910
1911 destination_pubkey: Option<String>,
1913
1914 lnurl_info: Option<LnUrlInfo>,
1916
1917 bip353_address: Option<String>,
1919
1920 payer_note: Option<String>,
1922
1923 claim_tx_id: Option<String>,
1925
1926 refund_tx_id: Option<String>,
1928
1929 refund_tx_amount_sat: Option<u64>,
1931 },
1932 Liquid {
1934 destination: String,
1936
1937 description: String,
1939
1940 asset_id: String,
1942
1943 asset_info: Option<AssetInfo>,
1945
1946 lnurl_info: Option<LnUrlInfo>,
1948
1949 bip353_address: Option<String>,
1951
1952 payer_note: Option<String>,
1954 },
1955 Bitcoin {
1957 swap_id: String,
1958
1959 bitcoin_address: String,
1961
1962 description: String,
1964
1965 auto_accepted_fees: bool,
1969
1970 liquid_expiration_blockheight: Option<u32>,
1973
1974 bitcoin_expiration_blockheight: Option<u32>,
1977
1978 lockup_tx_id: Option<String>,
1980
1981 claim_tx_id: Option<String>,
1983
1984 refund_tx_id: Option<String>,
1986
1987 refund_tx_amount_sat: Option<u64>,
1989 },
1990}
1991
1992impl PaymentDetails {
1993 pub(crate) fn get_swap_id(&self) -> Option<String> {
1994 match self {
1995 Self::Lightning { swap_id, .. } | Self::Bitcoin { swap_id, .. } => {
1996 Some(swap_id.clone())
1997 }
1998 Self::Liquid { .. } => None,
1999 }
2000 }
2001
2002 pub(crate) fn get_refund_tx_amount_sat(&self) -> Option<u64> {
2003 match self {
2004 Self::Lightning {
2005 refund_tx_amount_sat,
2006 ..
2007 }
2008 | Self::Bitcoin {
2009 refund_tx_amount_sat,
2010 ..
2011 } => *refund_tx_amount_sat,
2012 Self::Liquid { .. } => None,
2013 }
2014 }
2015
2016 pub(crate) fn get_description(&self) -> Option<String> {
2017 match self {
2018 Self::Lightning { description, .. }
2019 | Self::Bitcoin { description, .. }
2020 | Self::Liquid { description, .. } => Some(description.clone()),
2021 }
2022 }
2023
2024 pub(crate) fn is_lbtc_asset_id(&self, network: LiquidNetwork) -> bool {
2025 match self {
2026 Self::Liquid { asset_id, .. } => {
2027 asset_id.eq(&utils::lbtc_asset_id(network).to_string())
2028 }
2029 _ => true,
2030 }
2031 }
2032}
2033
2034#[derive(Debug, Clone, PartialEq, Serialize)]
2038pub struct Payment {
2039 pub destination: Option<String>,
2042
2043 pub tx_id: Option<String>,
2044
2045 pub unblinding_data: Option<String>,
2048
2049 pub timestamp: u32,
2055
2056 pub amount_sat: u64,
2060
2061 pub fees_sat: u64,
2075
2076 pub swapper_fees_sat: Option<u64>,
2079
2080 pub payment_type: PaymentType,
2082
2083 pub status: PaymentState,
2089
2090 pub details: PaymentDetails,
2093}
2094impl Payment {
2095 pub(crate) fn from_pending_swap(
2096 swap: PaymentSwapData,
2097 payment_type: PaymentType,
2098 payment_details: PaymentDetails,
2099 ) -> Payment {
2100 let amount_sat = match payment_type {
2101 PaymentType::Receive => swap.receiver_amount_sat,
2102 PaymentType::Send => swap.payer_amount_sat,
2103 };
2104
2105 Payment {
2106 destination: swap.invoice.clone(),
2107 tx_id: None,
2108 unblinding_data: None,
2109 timestamp: swap.created_at,
2110 amount_sat,
2111 fees_sat: swap
2112 .payer_amount_sat
2113 .saturating_sub(swap.receiver_amount_sat),
2114 swapper_fees_sat: Some(swap.swapper_fees_sat),
2115 payment_type,
2116 status: swap.status,
2117 details: payment_details,
2118 }
2119 }
2120
2121 pub(crate) fn from_tx_data(
2122 tx: PaymentTxData,
2123 balance: PaymentTxBalance,
2124 swap: Option<PaymentSwapData>,
2125 details: PaymentDetails,
2126 ) -> Payment {
2127 let (amount_sat, fees_sat) = match swap.as_ref() {
2128 Some(s) => match balance.payment_type {
2129 PaymentType::Receive => (
2133 balance.amount,
2134 s.payer_amount_sat.saturating_sub(balance.amount),
2135 ),
2136 PaymentType::Send => (
2137 s.receiver_amount_sat,
2138 s.payer_amount_sat.saturating_sub(s.receiver_amount_sat),
2139 ),
2140 },
2141 None => {
2142 let (amount_sat, fees_sat) = match balance.payment_type {
2143 PaymentType::Receive => (balance.amount, 0),
2144 PaymentType::Send => (balance.amount, tx.fees_sat),
2145 };
2146 match details {
2149 PaymentDetails::Liquid {
2150 asset_info: Some(ref asset_info),
2151 ..
2152 } if asset_info.ticker != "BTC" => (0, asset_info.fees.map_or(fees_sat, |_| 0)),
2153 _ => (amount_sat, fees_sat),
2154 }
2155 }
2156 };
2157 Payment {
2158 tx_id: Some(tx.tx_id),
2159 unblinding_data: tx.unblinding_data,
2160 destination: match &swap {
2164 Some(PaymentSwapData {
2165 swap_type: PaymentSwapType::Receive,
2166 invoice,
2167 ..
2168 }) => invoice.clone(),
2169 Some(PaymentSwapData {
2170 swap_type: PaymentSwapType::Send,
2171 invoice,
2172 bolt12_offer,
2173 ..
2174 }) => bolt12_offer.clone().or(invoice.clone()),
2175 Some(PaymentSwapData {
2176 swap_type: PaymentSwapType::Chain,
2177 bitcoin_address,
2178 ..
2179 }) => bitcoin_address.clone(),
2180 _ => match &details {
2181 PaymentDetails::Liquid { destination, .. } => Some(destination.clone()),
2182 _ => None,
2183 },
2184 },
2185 timestamp: tx
2186 .timestamp
2187 .or(swap.as_ref().map(|s| s.created_at))
2188 .unwrap_or(utils::now()),
2189 amount_sat,
2190 fees_sat,
2191 swapper_fees_sat: swap.as_ref().map(|s| s.swapper_fees_sat),
2192 payment_type: balance.payment_type,
2193 status: match &swap {
2194 Some(swap) => swap.status,
2195 None => match tx.is_confirmed {
2196 true => PaymentState::Complete,
2197 false => PaymentState::Pending,
2198 },
2199 },
2200 details,
2201 }
2202 }
2203
2204 pub(crate) fn get_refund_tx_id(&self) -> Option<String> {
2205 match self.details.clone() {
2206 PaymentDetails::Lightning { refund_tx_id, .. } => Some(refund_tx_id),
2207 PaymentDetails::Bitcoin { refund_tx_id, .. } => Some(refund_tx_id),
2208 PaymentDetails::Liquid { .. } => None,
2209 }
2210 .flatten()
2211 }
2212}
2213
2214#[derive(Deserialize, Serialize, Clone, Debug)]
2216#[serde(rename_all = "camelCase")]
2217pub struct RecommendedFees {
2218 pub fastest_fee: u64,
2219 pub half_hour_fee: u64,
2220 pub hour_fee: u64,
2221 pub economy_fee: u64,
2222 pub minimum_fee: u64,
2223}
2224
2225#[derive(Debug, Clone, Copy, EnumString, PartialEq, Serialize)]
2227pub enum BuyBitcoinProvider {
2228 #[strum(serialize = "moonpay")]
2229 Moonpay,
2230}
2231
2232#[derive(Debug, Serialize)]
2234pub struct PrepareBuyBitcoinRequest {
2235 pub provider: BuyBitcoinProvider,
2236 pub amount_sat: u64,
2237}
2238
2239#[derive(Clone, Debug, Serialize)]
2241pub struct PrepareBuyBitcoinResponse {
2242 pub provider: BuyBitcoinProvider,
2243 pub amount_sat: u64,
2244 pub fees_sat: u64,
2245}
2246
2247#[derive(Clone, Debug, Serialize)]
2249pub struct BuyBitcoinRequest {
2250 pub prepare_response: PrepareBuyBitcoinResponse,
2251 pub redirect_url: Option<String>,
2255}
2256
2257#[derive(Clone, Debug)]
2259pub struct LogEntry {
2260 pub line: String,
2261 pub level: String,
2262}
2263
2264#[derive(Clone, Debug, Serialize, Deserialize)]
2265struct InternalLeaf {
2266 pub output: String,
2267 pub version: u8,
2268}
2269impl From<InternalLeaf> for Leaf {
2270 fn from(value: InternalLeaf) -> Self {
2271 Leaf {
2272 output: value.output,
2273 version: value.version,
2274 }
2275 }
2276}
2277impl From<Leaf> for InternalLeaf {
2278 fn from(value: Leaf) -> Self {
2279 InternalLeaf {
2280 output: value.output,
2281 version: value.version,
2282 }
2283 }
2284}
2285
2286#[derive(Clone, Debug, Serialize, Deserialize)]
2287pub(super) struct InternalSwapTree {
2288 claim_leaf: InternalLeaf,
2289 refund_leaf: InternalLeaf,
2290}
2291impl From<InternalSwapTree> for SwapTree {
2292 fn from(value: InternalSwapTree) -> Self {
2293 SwapTree {
2294 claim_leaf: value.claim_leaf.into(),
2295 refund_leaf: value.refund_leaf.into(),
2296 }
2297 }
2298}
2299impl From<SwapTree> for InternalSwapTree {
2300 fn from(value: SwapTree) -> Self {
2301 InternalSwapTree {
2302 claim_leaf: value.claim_leaf.into(),
2303 refund_leaf: value.refund_leaf.into(),
2304 }
2305 }
2306}
2307
2308#[derive(Debug, Serialize)]
2310pub struct PrepareLnUrlPayRequest {
2311 pub data: LnUrlPayRequestData,
2313 pub amount: PayAmount,
2315 pub bip353_address: Option<String>,
2318 pub comment: Option<String>,
2321 pub validate_success_action_url: Option<bool>,
2324}
2325
2326#[derive(Debug, Serialize)]
2328pub struct PrepareLnUrlPayResponse {
2329 pub destination: SendDestination,
2331 pub fees_sat: u64,
2333 pub data: LnUrlPayRequestData,
2335 pub amount: PayAmount,
2337 pub comment: Option<String>,
2340 pub success_action: Option<SuccessAction>,
2343}
2344
2345#[derive(Debug, Serialize)]
2347pub struct LnUrlPayRequest {
2348 pub prepare_response: PrepareLnUrlPayResponse,
2350}
2351
2352#[derive(Serialize)]
2364#[allow(clippy::large_enum_variant)]
2365pub enum LnUrlPayResult {
2366 EndpointSuccess { data: LnUrlPaySuccessData },
2367 EndpointError { data: LnUrlErrorData },
2368 PayError { data: LnUrlPayErrorData },
2369}
2370
2371#[derive(Serialize)]
2372pub struct LnUrlPaySuccessData {
2373 pub payment: Payment,
2374 pub success_action: Option<SuccessActionProcessed>,
2375}
2376
2377#[derive(Debug, Clone)]
2378pub enum Transaction {
2379 Liquid(boltz_client::elements::Transaction),
2380 Bitcoin(boltz_client::bitcoin::Transaction),
2381}
2382
2383impl Transaction {
2384 pub(crate) fn txid(&self) -> String {
2385 match self {
2386 Transaction::Liquid(tx) => tx.txid().to_hex(),
2387 Transaction::Bitcoin(tx) => tx.compute_txid().to_hex(),
2388 }
2389 }
2390}
2391
2392#[derive(Debug, Clone)]
2393pub enum Utxo {
2394 Liquid(
2395 Box<(
2396 boltz_client::elements::OutPoint,
2397 boltz_client::elements::TxOut,
2398 )>,
2399 ),
2400 Bitcoin(
2401 (
2402 boltz_client::bitcoin::OutPoint,
2403 boltz_client::bitcoin::TxOut,
2404 ),
2405 ),
2406}
2407
2408impl Utxo {
2409 pub(crate) fn as_bitcoin(
2410 &self,
2411 ) -> Option<&(
2412 boltz_client::bitcoin::OutPoint,
2413 boltz_client::bitcoin::TxOut,
2414 )> {
2415 match self {
2416 Utxo::Liquid(_) => None,
2417 Utxo::Bitcoin(utxo) => Some(utxo),
2418 }
2419 }
2420
2421 pub(crate) fn as_liquid(
2422 &self,
2423 ) -> Option<
2424 Box<(
2425 boltz_client::elements::OutPoint,
2426 boltz_client::elements::TxOut,
2427 )>,
2428 > {
2429 match self {
2430 Utxo::Bitcoin(_) => None,
2431 Utxo::Liquid(utxo) => Some(utxo.clone()),
2432 }
2433 }
2434}
2435
2436#[derive(Debug, Clone)]
2438pub struct FetchPaymentProposedFeesRequest {
2439 pub swap_id: String,
2440}
2441
2442#[derive(Debug, Clone, Serialize)]
2444pub struct FetchPaymentProposedFeesResponse {
2445 pub swap_id: String,
2446 pub fees_sat: u64,
2447 pub payer_amount_sat: u64,
2449 pub receiver_amount_sat: u64,
2451}
2452
2453#[derive(Debug, Clone)]
2455pub struct AcceptPaymentProposedFeesRequest {
2456 pub response: FetchPaymentProposedFeesResponse,
2457}
2458
2459#[derive(Clone, Debug)]
2460pub struct History<T> {
2461 pub txid: T,
2462 pub height: i32,
2467}
2468pub(crate) type LBtcHistory = History<elements::Txid>;
2469pub(crate) type BtcHistory = History<bitcoin::Txid>;
2470
2471impl<T> History<T> {
2472 pub(crate) fn confirmed(&self) -> bool {
2473 self.height > 0
2474 }
2475}
2476#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
2477impl From<electrum_client::GetHistoryRes> for BtcHistory {
2478 fn from(value: electrum_client::GetHistoryRes) -> Self {
2479 Self {
2480 txid: value.tx_hash,
2481 height: value.height,
2482 }
2483 }
2484}
2485impl From<lwk_wollet::History> for LBtcHistory {
2486 fn from(value: lwk_wollet::History) -> Self {
2487 Self::from(&value)
2488 }
2489}
2490impl From<&lwk_wollet::History> for LBtcHistory {
2491 fn from(value: &lwk_wollet::History) -> Self {
2492 Self {
2493 txid: value.txid,
2494 height: value.height,
2495 }
2496 }
2497}
2498pub(crate) type BtcScript = bitcoin::ScriptBuf;
2499pub(crate) type LBtcScript = elements::Script;
2500
2501#[derive(Clone, Debug)]
2502pub struct BtcScriptBalance {
2503 pub confirmed: u64,
2505 pub unconfirmed: i64,
2509}
2510#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
2511impl From<electrum_client::GetBalanceRes> for BtcScriptBalance {
2512 fn from(val: electrum_client::GetBalanceRes) -> Self {
2513 Self {
2514 confirmed: val.confirmed,
2515 unconfirmed: val.unconfirmed,
2516 }
2517 }
2518}
2519
2520pub(crate) struct GetSyncContextRequest {
2521 pub partial_sync: Option<bool>,
2522 pub last_liquid_tip: u32,
2523 pub last_bitcoin_tip: u32,
2524}
2525
2526pub(crate) struct SyncContext {
2527 pub maybe_liquid_tip: Option<u32>,
2528 pub maybe_bitcoin_tip: Option<u32>,
2529 pub recoverable_swaps: Vec<Swap>,
2530 pub is_new_liquid_block: bool,
2531 pub is_new_bitcoin_block: bool,
2532}
2533
2534#[macro_export]
2535macro_rules! get_updated_fields {
2536 ($($var:ident),* $(,)?) => {{
2537 let mut options = Vec::new();
2538 $(
2539 if $var.is_some() {
2540 options.push(stringify!($var).to_string());
2541 }
2542 )*
2543 match options.len() > 0 {
2544 true => Some(options),
2545 false => None,
2546 }
2547 }};
2548}