breez_sdk_liquid/wallet/
mod.rs

1pub mod persister;
2
3use std::collections::HashMap;
4use std::io::Write;
5use std::str::FromStr;
6
7use anyhow::{anyhow, bail, Result};
8use boltz_client::ElementsAddress;
9use log::{debug, error, info, warn};
10use lwk_common::Signer as LwkSigner;
11use lwk_common::{singlesig_desc, Singlesig};
12use lwk_wollet::asyncr::{EsploraClient, EsploraClientBuilder};
13use lwk_wollet::elements::hex::ToHex;
14use lwk_wollet::elements::pset::PartiallySignedTransaction;
15use lwk_wollet::elements::{Address, AssetId, OutPoint, Transaction, TxOut, Txid};
16use lwk_wollet::secp256k1::Message;
17use lwk_wollet::{ElementsNetwork, WalletTx, WalletTxOut, Wollet, WolletDescriptor};
18use maybe_sync::{MaybeSend, MaybeSync};
19use persister::SqliteWalletCachePersister;
20use sdk_common::bitcoin::hashes::{sha256, Hash};
21use sdk_common::bitcoin::secp256k1::PublicKey;
22use sdk_common::lightning::util::message_signing::verify;
23use tokio::sync::Mutex;
24use web_time::Instant;
25
26use crate::model::{BlockchainExplorer, Signer, BREEZ_LIQUID_ESPLORA_URL};
27use crate::persist::Persister;
28use crate::signer::SdkLwkSigner;
29use crate::{
30    ensure_sdk,
31    error::PaymentError,
32    model::{Config, LiquidNetwork},
33};
34use sdk_common::utils::Arc;
35
36use crate::wallet::persister::WalletCachePersister;
37#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
38use lwk_wollet::blocking::BlockchainBackend;
39
40static LN_MESSAGE_PREFIX: &[u8] = b"Lightning Signed Message:";
41
42#[sdk_macros::async_trait]
43pub trait OnchainWallet: MaybeSend + MaybeSync {
44    /// List all transactions in the wallet
45    async fn transactions(&self) -> Result<Vec<WalletTx>, PaymentError>;
46
47    /// List all transactions in the wallet mapped by tx id
48    async fn transactions_by_tx_id(&self) -> Result<HashMap<Txid, WalletTx>, PaymentError>;
49
50    /// List all utxos in the wallet for a given asset
51    async fn asset_utxos(&self, asset: &AssetId) -> Result<Vec<WalletTxOut>, PaymentError>;
52
53    /// Build a transaction to send funds to a recipient
54    async fn build_tx(
55        &self,
56        fee_rate_sats_per_kvb: Option<f32>,
57        recipient_address: &str,
58        asset_id: &str,
59        amount_sat: u64,
60    ) -> Result<Transaction, PaymentError>;
61
62    /// Builds a drain tx.
63    ///
64    /// ### Arguments
65    /// - `fee_rate_sats_per_kvb`: custom drain tx feerate
66    /// - `recipient_address`: drain tx recipient
67    /// - `enforce_amount_sat`: if set, the drain tx will only be built if the amount transferred is
68    ///   this amount, otherwise it will fail with a validation error
69    async fn build_drain_tx(
70        &self,
71        fee_rate_sats_per_kvb: Option<f32>,
72        recipient_address: &str,
73        enforce_amount_sat: Option<u64>,
74    ) -> Result<Transaction, PaymentError>;
75
76    /// Build a transaction to send funds to a recipient. If building a transaction
77    /// results in an InsufficientFunds error, attempt to build a drain transaction
78    /// validating that the `amount_sat` matches the drain output.
79    async fn build_tx_or_drain_tx(
80        &self,
81        fee_rate_sats_per_kvb: Option<f32>,
82        recipient_address: &str,
83        asset_id: &str,
84        amount_sat: u64,
85    ) -> Result<Transaction, PaymentError>;
86
87    /// Sign a partially signed transaction
88    async fn sign_pset(
89        &self,
90        pset: PartiallySignedTransaction,
91    ) -> Result<Transaction, PaymentError>;
92
93    /// Get the next unused address in the wallet
94    async fn next_unused_address(&self) -> Result<Address, PaymentError>;
95
96    /// Get the next unused change address in the wallet
97    async fn next_unused_change_address(&self) -> Result<Address, PaymentError>;
98
99    /// Get the current tip of the blockchain the wallet is aware of
100    async fn tip(&self) -> u32;
101
102    /// Get the public key of the wallet
103    fn pubkey(&self) -> Result<String>;
104
105    /// Get the fingerprint of the wallet
106    fn fingerprint(&self) -> Result<String>;
107
108    /// Sign given message with the wallet private key. Returns a zbase
109    /// encoded signature.
110    fn sign_message(&self, msg: &str) -> Result<String>;
111
112    /// Check whether given message was signed by the given
113    /// pubkey and the signature (zbase encoded) is valid.
114    fn check_message(&self, message: &str, pubkey: &str, signature: &str) -> Result<bool>;
115
116    /// Perform a full scan of the wallet
117    async fn full_scan(&self) -> Result<(), PaymentError>;
118}
119
120pub enum WalletClient {
121    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
122    Electrum(Box<lwk_wollet::ElectrumClient>),
123    Esplora(Box<EsploraClient>),
124}
125
126impl WalletClient {
127    pub(crate) fn from_config(config: &Config) -> Result<Self> {
128        match &config.liquid_explorer {
129            #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
130            BlockchainExplorer::Electrum { url } => {
131                let client = Box::new(config.electrum_client(url)?);
132                Ok(Self::Electrum(client))
133            }
134            BlockchainExplorer::Esplora {
135                url,
136                use_waterfalls,
137            } => {
138                let waterfalls = *use_waterfalls;
139                let mut builder = EsploraClientBuilder::new(url, config.network.into());
140                if url == BREEZ_LIQUID_ESPLORA_URL {
141                    match &config.breez_api_key {
142                        Some(api_key) => {
143                            builder = builder
144                                .header("authorization".to_string(), format!("Bearer {api_key}"));
145                        }
146                        None => {
147                            let err = "Cannot start Breez Esplora client: Breez API key is not set";
148                            error!("{err}");
149                            bail!(err)
150                        }
151                    };
152                }
153                let client = Box::new(builder.timeout(3).waterfalls(waterfalls).build());
154                Ok(Self::Esplora(client))
155            }
156        }
157    }
158
159    pub(crate) async fn full_scan_to_index(
160        &mut self,
161        wallet: &mut Wollet,
162        index: u32,
163    ) -> Result<(), lwk_wollet::Error> {
164        let maybe_update = match self {
165            #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
166            WalletClient::Electrum(electrum_client) => {
167                electrum_client.full_scan_to_index(&wallet.state(), index)?
168            }
169            WalletClient::Esplora(esplora_client) => {
170                esplora_client.full_scan_to_index(wallet, index).await?
171            }
172        };
173
174        if let Some(update) = maybe_update {
175            wallet.apply_update(update)?;
176        }
177
178        Ok(())
179    }
180}
181
182pub struct LiquidOnchainWallet {
183    config: Config,
184    persister: std::sync::Arc<Persister>,
185    wallet: Arc<Mutex<Wollet>>,
186    client: Mutex<Option<WalletClient>>,
187    pub(crate) signer: SdkLwkSigner,
188    wallet_cache_persister: Arc<dyn WalletCachePersister>,
189}
190
191impl LiquidOnchainWallet {
192    /// Creates a new LiquidOnchainWallet that caches data on the provided `working_dir`.
193    pub(crate) async fn new(
194        config: Config,
195        persister: std::sync::Arc<Persister>,
196        user_signer: Arc<Box<dyn Signer>>,
197    ) -> Result<Self> {
198        let signer = SdkLwkSigner::new(user_signer.clone())?;
199
200        let wallet_cache_persister: Arc<dyn WalletCachePersister> =
201            Arc::new(SqliteWalletCachePersister::new(
202                std::sync::Arc::clone(&persister),
203                get_descriptor(&signer, config.network)?,
204            )?);
205
206        let wollet = Self::create_wallet(&config, &signer, wallet_cache_persister.clone()).await?;
207
208        Ok(Self {
209            config,
210            persister,
211            wallet: Arc::new(Mutex::new(wollet)),
212            client: Mutex::new(None),
213            signer,
214            wallet_cache_persister,
215        })
216    }
217
218    async fn create_wallet(
219        config: &Config,
220        signer: &SdkLwkSigner,
221        wallet_cache_persister: Arc<dyn WalletCachePersister>,
222    ) -> Result<Wollet> {
223        let elements_network: ElementsNetwork = config.network.into();
224        let descriptor = get_descriptor(signer, config.network)?;
225        let wollet_res = Wollet::new(
226            elements_network,
227            wallet_cache_persister.get_lwk_persister()?,
228            descriptor.clone(),
229        );
230        match wollet_res {
231            Ok(wollet) => Ok(wollet),
232            res @ Err(
233                lwk_wollet::Error::PersistError(_)
234                | lwk_wollet::Error::UpdateHeightTooOld { .. }
235                | lwk_wollet::Error::UpdateOnDifferentStatus { .. },
236            ) => {
237                warn!("Update error initialising wollet, wiping cache and retrying: {res:?}");
238                wallet_cache_persister.clear_cache().await?;
239                Ok(Wollet::new(
240                    elements_network,
241                    wallet_cache_persister.get_lwk_persister()?,
242                    descriptor.clone(),
243                )?)
244            }
245            Err(e) => Err(e.into()),
246        }
247    }
248
249    async fn get_txout(&self, wallet: &Wollet, outpoint: &OutPoint) -> Result<TxOut> {
250        let wallet_tx = wallet
251            .transaction(&outpoint.txid)?
252            .ok_or(anyhow!("Transaction not found"))?;
253        let tx_out = wallet_tx
254            .tx
255            .output
256            .get(outpoint.vout as usize)
257            .ok_or(anyhow!("Output not found"))?;
258        Ok(tx_out.clone())
259    }
260}
261
262pub fn get_descriptor(
263    signer: &SdkLwkSigner,
264    network: LiquidNetwork,
265) -> Result<WolletDescriptor, PaymentError> {
266    let is_mainnet = network == LiquidNetwork::Mainnet;
267    let descriptor_str = singlesig_desc(
268        signer,
269        Singlesig::Wpkh,
270        lwk_common::DescriptorBlindingKey::Slip77,
271        is_mainnet,
272    )
273    .map_err(|e| anyhow!("Invalid descriptor: {e}"))?;
274    Ok(descriptor_str.parse()?)
275}
276
277#[sdk_macros::async_trait]
278impl OnchainWallet for LiquidOnchainWallet {
279    /// List all transactions in the wallet
280    async fn transactions(&self) -> Result<Vec<WalletTx>, PaymentError> {
281        let wallet = self.wallet.lock().await;
282        wallet.transactions().map_err(|e| PaymentError::Generic {
283            err: format!("Failed to fetch wallet transactions: {e:?}"),
284        })
285    }
286
287    /// List all transactions in the wallet mapped by tx id
288    async fn transactions_by_tx_id(&self) -> Result<HashMap<Txid, WalletTx>, PaymentError> {
289        let tx_map: HashMap<Txid, WalletTx> = self
290            .transactions()
291            .await?
292            .iter()
293            .map(|tx| (tx.txid, tx.clone()))
294            .collect();
295        Ok(tx_map)
296    }
297
298    async fn asset_utxos(&self, asset: &AssetId) -> Result<Vec<WalletTxOut>, PaymentError> {
299        Ok(self
300            .wallet
301            .lock()
302            .await
303            .utxos()?
304            .into_iter()
305            .filter(|utxo| &utxo.unblinded.asset == asset)
306            .collect())
307    }
308
309    /// Build a transaction to send funds to a recipient
310    async fn build_tx(
311        &self,
312        fee_rate_sats_per_kvb: Option<f32>,
313        recipient_address: &str,
314        asset_id: &str,
315        amount_sat: u64,
316    ) -> Result<Transaction, PaymentError> {
317        let lwk_wollet = self.wallet.lock().await;
318        let address =
319            ElementsAddress::from_str(recipient_address).map_err(|e| PaymentError::Generic {
320                err: format!(
321                    "Recipient address {recipient_address} is not a valid ElementsAddress: {e:?}"
322                ),
323            })?;
324        let mut tx_builder = lwk_wollet::TxBuilder::new(self.config.network.into())
325            .fee_rate(fee_rate_sats_per_kvb)
326            .enable_ct_discount();
327        if asset_id.eq(&self.config.lbtc_asset_id()) {
328            tx_builder = tx_builder.add_lbtc_recipient(&address, amount_sat)?;
329        } else {
330            let asset = AssetId::from_str(asset_id)?;
331            tx_builder = tx_builder.add_recipient(&address, amount_sat, asset)?;
332        }
333        let mut pset = tx_builder.finish(&lwk_wollet)?;
334        self.signer
335            .sign(&mut pset)
336            .map_err(|e| PaymentError::Generic {
337                err: format!("Failed to sign transaction: {e:?}"),
338            })?;
339        Ok(lwk_wollet.finalize(&mut pset)?)
340    }
341
342    async fn build_drain_tx(
343        &self,
344        fee_rate_sats_per_kvb: Option<f32>,
345        recipient_address: &str,
346        enforce_amount_sat: Option<u64>,
347    ) -> Result<Transaction, PaymentError> {
348        let lwk_wollet = self.wallet.lock().await;
349
350        let address =
351            ElementsAddress::from_str(recipient_address).map_err(|e| PaymentError::Generic {
352                err: format!(
353                    "Recipient address {recipient_address} is not a valid ElementsAddress: {e:?}"
354                ),
355            })?;
356        let mut pset = lwk_wollet
357            .tx_builder()
358            .drain_lbtc_wallet()
359            .drain_lbtc_to(address)
360            .fee_rate(fee_rate_sats_per_kvb)
361            .enable_ct_discount()
362            .finish()?;
363
364        if let Some(enforce_amount_sat) = enforce_amount_sat {
365            let pset_details = lwk_wollet.get_details(&pset)?;
366            let pset_balance_sat = pset_details
367                .balance
368                .balances
369                .get(&lwk_wollet.policy_asset())
370                .unwrap_or(&0);
371            let pset_fees = pset_details.balance.fee;
372
373            ensure_sdk!(
374                (*pset_balance_sat * -1) as u64 - pset_fees == enforce_amount_sat,
375                PaymentError::Generic {
376                    err: format!("Drain tx amount {pset_balance_sat} sat doesn't match enforce_amount_sat {enforce_amount_sat} sat")
377                }
378            );
379        }
380
381        self.signer
382            .sign(&mut pset)
383            .map_err(|e| PaymentError::Generic {
384                err: format!("Failed to sign transaction: {e:?}"),
385            })?;
386        Ok(lwk_wollet.finalize(&mut pset)?)
387    }
388
389    async fn build_tx_or_drain_tx(
390        &self,
391        fee_rate_sats_per_kvb: Option<f32>,
392        recipient_address: &str,
393        asset_id: &str,
394        amount_sat: u64,
395    ) -> Result<Transaction, PaymentError> {
396        match self
397            .build_tx(
398                fee_rate_sats_per_kvb,
399                recipient_address,
400                asset_id,
401                amount_sat,
402            )
403            .await
404        {
405            Ok(tx) => Ok(tx),
406            Err(PaymentError::InsufficientFunds) if asset_id.eq(&self.config.lbtc_asset_id()) => {
407                warn!("Cannot build tx due to insufficient funds, attempting to build drain tx");
408                self.build_drain_tx(fee_rate_sats_per_kvb, recipient_address, Some(amount_sat))
409                    .await
410            }
411            Err(e) => Err(e),
412        }
413    }
414
415    async fn sign_pset(
416        &self,
417        mut pset: PartiallySignedTransaction,
418    ) -> Result<Transaction, PaymentError> {
419        let lwk_wollet = self.wallet.lock().await;
420
421        // Get the tx_out for each input and add the rangeproof/witness utxo
422        for input in pset.inputs_mut().iter_mut() {
423            let tx_out_res = self
424                .get_txout(
425                    &lwk_wollet,
426                    &OutPoint {
427                        txid: input.previous_txid,
428                        vout: input.previous_output_index,
429                    },
430                )
431                .await;
432            if let Ok(mut tx_out) = tx_out_res {
433                input.in_utxo_rangeproof = tx_out.witness.rangeproof.take();
434                input.witness_utxo = Some(tx_out);
435            }
436        }
437
438        lwk_wollet.add_details(&mut pset)?;
439
440        self.signer
441            .sign(&mut pset)
442            .map_err(|e| PaymentError::Generic {
443                err: format!("Failed to sign transaction: {e:?}"),
444            })?;
445
446        // Set the final script witness for each input adding the signature and any missing public key
447        for input in pset.inputs_mut() {
448            if let Some((public_key, input_sign)) = input.partial_sigs.iter().next() {
449                input.final_script_witness = Some(vec![input_sign.clone(), public_key.to_bytes()]);
450            }
451        }
452
453        let tx = pset.extract_tx().map_err(|e| PaymentError::Generic {
454            err: format!("Failed to extract transaction: {e:?}"),
455        })?;
456        Ok(tx)
457    }
458
459    /// Get the next unused address in the wallet
460    async fn next_unused_address(&self) -> Result<Address, PaymentError> {
461        let tip = self.tip().await;
462        let address = match self.persister.next_expired_reserved_address(tip)? {
463            Some(reserved_address) => {
464                debug!(
465                    "Got reserved address {} that expired on block height {}",
466                    reserved_address.address, reserved_address.expiry_block_height
467                );
468                ElementsAddress::from_str(&reserved_address.address)
469                    .map_err(|e| PaymentError::Generic { err: e.to_string() })?
470            }
471            None => {
472                let next_index = self.persister.next_derivation_index()?;
473                let address_result = self.wallet.lock().await.address(next_index)?;
474                let address = address_result.address().clone();
475                let index = address_result.index();
476                debug!(
477                    "Got unused address {} with derivation index {}",
478                    address, index
479                );
480                if next_index.is_none() {
481                    self.persister.set_last_derivation_index(index)?;
482                }
483                address
484            }
485        };
486
487        Ok(address)
488    }
489
490    /// Get the next unused change address in the wallet
491    async fn next_unused_change_address(&self) -> Result<Address, PaymentError> {
492        let address = self.wallet.lock().await.change(None)?.address().clone();
493
494        Ok(address)
495    }
496
497    /// Get the current tip of the blockchain the wallet is aware of
498    async fn tip(&self) -> u32 {
499        self.wallet.lock().await.tip().height()
500    }
501
502    /// Get the public key of the wallet
503    fn pubkey(&self) -> Result<String> {
504        Ok(self.signer.xpub()?.public_key.to_string())
505    }
506
507    /// Get the fingerprint of the wallet
508    fn fingerprint(&self) -> Result<String> {
509        Ok(self.signer.fingerprint()?.to_hex())
510    }
511
512    /// Perform a full scan of the wallet
513    async fn full_scan(&self) -> Result<(), PaymentError> {
514        let full_scan_started = Instant::now();
515
516        // create electrum client if doesn't already exist
517        let mut client = self.client.lock().await;
518        if client.is_none() {
519            *client = Some(WalletClient::from_config(&self.config)?);
520        }
521        let client = client.as_mut().ok_or_else(|| PaymentError::Generic {
522            err: "Wallet client not initialized".to_string(),
523        })?;
524
525        // Use the cached derivation index with a buffer of 5 to perform the scan
526        let last_derivation_index = self
527            .persister
528            .get_last_derivation_index()?
529            .unwrap_or_default();
530        let index_with_buffer = last_derivation_index + 5;
531        let mut wallet = self.wallet.lock().await;
532
533        let res = match client
534            .full_scan_to_index(&mut wallet, index_with_buffer)
535            .await
536        {
537            Ok(()) => Ok(()),
538            Err(e)
539                if matches!(
540                    e,
541                    lwk_wollet::Error::UpdateHeightTooOld { .. }
542                        | lwk_wollet::Error::PersistError(_)
543                ) =>
544            {
545                warn!("Full scan failed due to {e}, reloading wallet and retrying");
546                let mut new_wallet = Self::create_wallet(
547                    &self.config,
548                    &self.signer,
549                    self.wallet_cache_persister.clone(),
550                )
551                .await?;
552                client
553                    .full_scan_to_index(&mut new_wallet, index_with_buffer)
554                    .await?;
555                *wallet = new_wallet;
556                Ok(())
557            }
558            Err(e) => Err(e.into()),
559        };
560
561        self.persister
562            .set_last_scanned_derivation_index(last_derivation_index)?;
563
564        let duration_ms = Instant::now().duration_since(full_scan_started).as_millis();
565        info!("lwk wallet full_scan duration: ({duration_ms} ms)");
566        res
567    }
568
569    fn sign_message(&self, message: &str) -> Result<String> {
570        // Prefix and double hash message
571        let mut engine = sha256::HashEngine::default();
572        engine.write_all(LN_MESSAGE_PREFIX)?;
573        engine.write_all(message.as_bytes())?;
574        let hashed_msg = sha256::Hash::from_engine(engine);
575        let double_hashed_msg = Message::from_digest(sha256::Hash::hash(&hashed_msg).into_inner());
576        // Get message signature and encode to zbase32
577        let recoverable_sig = self.signer.sign_ecdsa_recoverable(&double_hashed_msg)?;
578        Ok(zbase32::encode_full_bytes(recoverable_sig.as_slice()))
579    }
580
581    fn check_message(&self, message: &str, pubkey: &str, signature: &str) -> Result<bool> {
582        let pk = PublicKey::from_str(pubkey)?;
583        Ok(verify(message.as_bytes(), signature, &pk))
584    }
585}
586
587#[cfg(test)]
588mod tests {
589    use super::*;
590    use crate::model::Config;
591    use crate::signer::SdkSigner;
592    use crate::test_utils::persist::create_persister;
593    use crate::wallet::LiquidOnchainWallet;
594    use anyhow::Result;
595
596    #[cfg(feature = "browser-tests")]
597    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
598
599    #[sdk_macros::async_test_all]
600    async fn test_sign_and_check_message() -> Result<()> {
601        let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
602        let sdk_signer: Box<dyn Signer> = Box::new(SdkSigner::new(mnemonic, "", false).unwrap());
603        let sdk_signer = Arc::new(sdk_signer);
604
605        let config = Config::testnet_esplora(None);
606
607        create_persister!(storage);
608
609        let wallet: Arc<dyn OnchainWallet> = Arc::new(
610            LiquidOnchainWallet::new(config, storage, sdk_signer.clone())
611                .await
612                .unwrap(),
613        );
614
615        // Test message
616        let message = "Hello, Liquid!";
617
618        // Sign the message
619        let signature = wallet.sign_message(message).unwrap();
620
621        // Get the public key
622        let pubkey = wallet.pubkey().unwrap();
623
624        // Check the message
625        let is_valid = wallet.check_message(message, &pubkey, &signature).unwrap();
626        assert!(is_valid, "Message signature should be valid");
627
628        // Check with an incorrect message
629        let incorrect_message = "Wrong message";
630        let is_invalid = wallet
631            .check_message(incorrect_message, &pubkey, &signature)
632            .unwrap();
633        assert!(
634            !is_invalid,
635            "Message signature should be invalid for incorrect message"
636        );
637
638        // Check with an incorrect public key
639        let incorrect_pubkey = "02a1633cafcc01ebfb6d78e39f687a1f0995c62fc95f51ead10a02ee0be551b5dc";
640        let is_invalid = wallet
641            .check_message(message, incorrect_pubkey, &signature)
642            .unwrap();
643        assert!(
644            !is_invalid,
645            "Message signature should be invalid for incorrect public key"
646        );
647
648        // Check with an incorrect signature
649        let incorrect_signature = zbase32::encode_full_bytes(&[0; 65]);
650        let is_invalid = wallet
651            .check_message(message, &pubkey, &incorrect_signature)
652            .unwrap();
653        assert!(
654            !is_invalid,
655            "Message signature should be invalid for incorrect signature"
656        );
657
658        // The temporary directory will be automatically deleted when temp_dir goes out of scope
659        Ok(())
660    }
661}