Skip to main content

breez_sdk_liquid/wallet/
mod.rs

1pub(crate) mod network_fee;
2pub mod persister;
3pub(crate) mod utxo_select;
4
5use std::collections::HashMap;
6use std::io::Write;
7use std::str::FromStr;
8use std::sync::Arc;
9
10use anyhow::{anyhow, bail, Result};
11use boltz_client::ElementsAddress;
12use log::{debug, error, info, warn};
13use lwk_common::Signer as LwkSigner;
14use lwk_common::{singlesig_desc, Singlesig};
15use lwk_wollet::asyncr::{EsploraClient, EsploraClientBuilder};
16use lwk_wollet::elements::hex::ToHex;
17use lwk_wollet::elements::pset::PartiallySignedTransaction;
18use lwk_wollet::elements::{Address, AssetId, OutPoint, Transaction, TxOut, Txid};
19use lwk_wollet::secp256k1::Message;
20use lwk_wollet::{Network, WalletTx, WalletTxOut, Wollet, WolletDescriptor};
21use persister::SqliteWalletCachePersister;
22use sdk_common::bitcoin::hashes::{sha256, Hash};
23use sdk_common::bitcoin::secp256k1::PublicKey;
24use sdk_common::lightning::util::message_signing::verify;
25use tokio::sync::Mutex;
26use utxo_select::{InOut, WalletUtxoSelectRequest};
27use web_time::Instant;
28
29use crate::model::{BlockchainExplorer, Signer, BREEZ_LIQUID_ESPLORA_URL};
30use crate::persist::Persister;
31use crate::signer::SdkLwkSigner;
32use crate::{ensure_sdk, error::PaymentError, model::Config};
33
34use crate::wallet::persister::WalletCachePersister;
35#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
36use lwk_wollet::blocking::BlockchainBackend;
37
38static LN_MESSAGE_PREFIX: &[u8] = b"Lightning Signed Message:";
39
40#[sdk_macros::async_trait]
41pub trait OnchainWallet: Send + Sync {
42    /// List all transactions in the wallet
43    async fn transactions(&self) -> Result<Vec<WalletTx>, PaymentError>;
44
45    /// List all transactions in the wallet mapped by tx id
46    async fn transactions_by_tx_id(&self) -> Result<HashMap<Txid, WalletTx>, PaymentError>;
47
48    /// List all utxos in the wallet for a given asset
49    async fn asset_utxos(&self, asset: &AssetId) -> Result<Vec<WalletTxOut>, PaymentError>;
50
51    /// Build a transaction to send funds to a recipient
52    async fn build_tx(
53        &self,
54        fee_rate_sats_per_kvb: Option<f32>,
55        recipient_address: &str,
56        asset_id: &str,
57        amount_sat: u64,
58    ) -> Result<Transaction, PaymentError>;
59
60    /// Builds a drain tx.
61    ///
62    /// ### Arguments
63    /// - `fee_rate_sats_per_kvb`: custom drain tx feerate
64    /// - `recipient_address`: drain tx recipient
65    /// - `enforce_amount_sat`: if set, the drain tx will only be built if the amount transferred is
66    ///   this amount, otherwise it will fail with a validation error
67    async fn build_drain_tx(
68        &self,
69        fee_rate_sats_per_kvb: Option<f32>,
70        recipient_address: &str,
71        enforce_amount_sat: Option<u64>,
72    ) -> Result<Transaction, PaymentError>;
73
74    /// Build a transaction to send funds to a recipient. If building a transaction
75    /// results in an InsufficientFunds error, attempt to build a drain transaction
76    /// validating that the `amount_sat` matches the drain output.
77    async fn build_tx_or_drain_tx(
78        &self,
79        fee_rate_sats_per_kvb: Option<f32>,
80        recipient_address: &str,
81        asset_id: &str,
82        amount_sat: u64,
83    ) -> Result<Transaction, PaymentError>;
84
85    /// Sign a partially signed transaction
86    async fn sign_pset(&self, pset: &mut PartiallySignedTransaction) -> Result<(), PaymentError>;
87
88    /// Get the next unused address in the wallet
89    async fn next_unused_address(&self) -> Result<Address, PaymentError>;
90
91    /// Get the next unused change address in the wallet
92    async fn next_unused_change_address(&self) -> Result<Address, PaymentError>;
93
94    /// Get the current tip of the blockchain the wallet is aware of
95    async fn tip(&self) -> u32;
96
97    /// Get the public key of the wallet
98    fn pubkey(&self) -> Result<String>;
99
100    /// Get the fingerprint of the wallet
101    fn fingerprint(&self) -> Result<String>;
102
103    /// Sign given message with the wallet private key. Returns a zbase
104    /// encoded signature.
105    fn sign_message(&self, msg: &str) -> Result<String>;
106
107    /// Check whether given message was signed by the given
108    /// pubkey and the signature (zbase encoded) is valid.
109    fn check_message(&self, message: &str, pubkey: &str, signature: &str) -> Result<bool>;
110
111    /// Perform a full scan of the wallet
112    async fn full_scan(&self) -> Result<(), PaymentError>;
113}
114
115pub enum WalletClient {
116    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
117    Electrum(Box<lwk_wollet::ElectrumClient>),
118    Esplora(Box<EsploraClient>),
119}
120
121impl WalletClient {
122    pub(crate) fn from_config(config: &Config) -> Result<Self> {
123        match &config.liquid_explorer {
124            #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
125            BlockchainExplorer::Electrum { url } => {
126                let client = Box::new(config.electrum_client(url)?);
127                Ok(Self::Electrum(client))
128            }
129            BlockchainExplorer::Esplora {
130                url,
131                use_waterfalls,
132            } => {
133                let waterfalls = *use_waterfalls;
134                let mut builder = EsploraClientBuilder::new(url, config.network.into());
135                if url == BREEZ_LIQUID_ESPLORA_URL {
136                    match &config.breez_api_key {
137                        Some(api_key) => {
138                            builder = builder
139                                .header("authorization".to_string(), format!("Bearer {api_key}"));
140                        }
141                        None => {
142                            let err = "Cannot start Breez Esplora client: Breez API key is not set";
143                            error!("{err}");
144                            bail!(err)
145                        }
146                    };
147                }
148                let client = builder
149                    .timeout(config.onchain_sync_request_timeout_sec as u8)
150                    .waterfalls(waterfalls)
151                    .build()?;
152                Ok(Self::Esplora(Box::new(client)))
153            }
154        }
155    }
156
157    pub(crate) async fn full_scan_to_index(
158        &mut self,
159        wallet: &mut Wollet,
160        index: u32,
161    ) -> Result<(), lwk_wollet::Error> {
162        let maybe_update = match self {
163            #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
164            WalletClient::Electrum(electrum_client) => {
165                electrum_client.full_scan_to_index(&wallet.state(), index)?
166            }
167            WalletClient::Esplora(esplora_client) => {
168                esplora_client.full_scan_to_index(wallet, index).await?
169            }
170        };
171
172        if let Some(update) = maybe_update {
173            debug!(
174                "WalletClient::full_scan_to_index: applying update {}",
175                update.version
176            );
177            wallet.apply_update(update)?;
178        }
179
180        Ok(())
181    }
182}
183
184pub struct LiquidOnchainWallet {
185    config: Config,
186    persister: std::sync::Arc<Persister>,
187    wallet: Arc<Mutex<Wollet>>,
188    client: Mutex<Option<WalletClient>>,
189    pub(crate) signer: SdkLwkSigner,
190    wallet_cache_persister: Arc<dyn WalletCachePersister>,
191}
192
193impl LiquidOnchainWallet {
194    /// Creates a new LiquidOnchainWallet that caches data on the provided `working_dir`.
195    pub(crate) async fn new(
196        config: Config,
197        persister: std::sync::Arc<Persister>,
198        user_signer: Arc<Box<dyn Signer>>,
199    ) -> Result<Self> {
200        let signer = SdkLwkSigner::new(user_signer.clone())?;
201
202        let wallet_cache_persister: Arc<dyn WalletCachePersister> = Arc::new(
203            SqliteWalletCachePersister::new(std::sync::Arc::clone(&persister))?,
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 network: Network = config.network.into();
224        let descriptor = get_descriptor(signer)?;
225        let build_wollet = |persister: persister::LwkPersister| {
226            lwk_wollet::WolletBuilder::new(network, descriptor.clone())
227                .with_updates_store(persister)
228                .build()
229        };
230        let wollet_res = build_wollet(wallet_cache_persister.get_lwk_persister()?);
231        match wollet_res {
232            Ok(wollet) => Ok(wollet),
233            res @ Err(
234                lwk_wollet::Error::UpdateHeightTooOld { .. }
235                | lwk_wollet::Error::UpdateOnDifferentStatus { .. }
236                | lwk_wollet::Error::StoreError(_),
237            ) => {
238                warn!("Update error initialising wollet, wiping cache and retrying: {res:?}");
239                wallet_cache_persister.clear_cache().await?;
240                Ok(build_wollet(wallet_cache_persister.get_lwk_persister()?)?)
241            }
242            Err(e) => Err(e.into()),
243        }
244    }
245
246    async fn get_txout(&self, wallet: &Wollet, outpoint: &OutPoint) -> Result<TxOut> {
247        let wallet_tx = wallet
248            .transaction(&outpoint.txid)?
249            .ok_or(anyhow!("Transaction not found"))?;
250        let tx_out = wallet_tx
251            .tx
252            .output
253            .get(outpoint.vout as usize)
254            .ok_or(anyhow!("Output not found"))?;
255        Ok(tx_out.clone())
256    }
257
258    fn select_wallet_utxos(
259        &self,
260        wallet: &Wollet,
261        policy_asset: AssetId,
262        selection_asset: AssetId,
263        recipient_outputs: Vec<InOut>,
264        fee_rate_sats_per_kvb: Option<f32>,
265    ) -> Result<Vec<OutPoint>, PaymentError> {
266        let mut wallet_utxos = wallet.utxos()?;
267        debug!(
268            "Wallet utxos: {:?}",
269            wallet_utxos
270                .iter()
271                .map(|tx_out| format!(
272                    "{}:{}, value: {}",
273                    tx_out.outpoint.txid, tx_out.outpoint.vout, tx_out.unblinded.value
274                ))
275                .collect::<Vec<_>>()
276        );
277        let fee_rate = fee_rate_sats_per_kvb.map(|rate| rate as f64 / 1000.0);
278        let selected_in_outs = utxo_select::utxo_select(WalletUtxoSelectRequest {
279            policy_asset,
280            selection_asset,
281            wallet_utxos: wallet_utxos.iter().map(Into::into).collect(),
282            recipient_outputs,
283            fee_rate,
284        })?;
285        let selected_utxos = Self::resolve_selected_utxos(&mut wallet_utxos, &selected_in_outs)?;
286        debug!(
287            "Selected wallet outputs: {:?}",
288            selected_utxos
289                .iter()
290                .map(|outpoint| format!("{}:{}", outpoint.txid, outpoint.vout))
291                .collect::<Vec<_>>()
292        );
293        Ok(selected_utxos)
294    }
295
296    /// Selects wallet utxos for a non-L-BTC asset send: enough utxos of `asset`
297    /// to cover `amount_sat`, plus a bounded set of L-BTC utxos to cover the fee.
298    ///
299    /// Without an explicit selection, lwk falls into its "always add all L-BTC
300    /// inputs" path, which can exceed the 256-input surjection-proof limit (and
301    /// fail with `TooManyInputs`) for wallets with many small L-BTC utxos, even
302    /// though an asset send only needs a couple of inputs to pay the fee.
303    fn select_asset_and_fee_utxos(
304        &self,
305        wallet: &Wollet,
306        asset: AssetId,
307        amount_sat: u64,
308        fee_rate_sats_per_kvb: Option<f32>,
309    ) -> Result<Vec<OutPoint>, PaymentError> {
310        let policy_asset = wallet.policy_asset();
311        ensure_sdk!(
312            asset != policy_asset,
313            PaymentError::generic("select_asset_and_fee_utxos called for the policy asset")
314        );
315
316        let mut wallet_utxos = wallet.utxos()?;
317
318        // Select asset utxos to cover the amount being sent.
319        let asset_values = wallet_utxos
320            .iter()
321            .filter(|tx_out| tx_out.unblinded.asset == asset)
322            .map(|tx_out| tx_out.unblinded.value)
323            .collect::<Vec<_>>();
324        let selected_asset_values = utxo_select::utxo_select_best(amount_sat, &asset_values)
325            .ok_or_else(|| PaymentError::generic("Failed to select asset utxos"))?;
326        let asset_input_count = selected_asset_values.len();
327
328        // Select a bounded set of L-BTC utxos to cover the fee. The fee depends on
329        // the total input count, so seed the estimate with the asset inputs above.
330        let fee_rate = fee_rate_sats_per_kvb.map(|rate| rate as f64 / 1000.0);
331        let policy_values = wallet_utxos
332            .iter()
333            .filter(|tx_out| tx_out.unblinded.asset == policy_asset)
334            .map(|tx_out| tx_out.unblinded.value)
335            .collect::<Vec<_>>();
336        let selected_fee_values = utxo_select::utxo_select_dynamic(
337            0,
338            &policy_values,
339            |lbtc_input_count, change_count| {
340                network_fee::TxFee {
341                    native_inputs: asset_input_count + lbtc_input_count,
342                    nested_inputs: 0,
343                    // asset recipient + asset change + L-BTC change
344                    outputs: 2 + change_count,
345                }
346                .fee(fee_rate)
347            },
348        )
349        .ok_or_else(|| PaymentError::generic("Failed to select L-BTC utxos for fee"))?;
350
351        // Resolve the selected asset and fee values to their wallet outpoints.
352        let selected = selected_asset_values
353            .into_iter()
354            .map(|value| InOut {
355                asset_id: asset,
356                value,
357            })
358            .chain(selected_fee_values.into_iter().map(|value| InOut {
359                asset_id: policy_asset,
360                value,
361            }))
362            .collect::<Vec<_>>();
363        Self::resolve_selected_utxos(&mut wallet_utxos, &selected)
364    }
365
366    /// Resolves selected `(asset, value)` pairs to their wallet outpoints,
367    /// removing each match as it is found so that duplicate values resolve to
368    /// distinct utxos. Errors if any selected value has no matching utxo.
369    fn resolve_selected_utxos(
370        wallet_utxos: &mut Vec<WalletTxOut>,
371        selected: &[InOut],
372    ) -> Result<Vec<OutPoint>, PaymentError> {
373        let selected_utxos = selected
374            .iter()
375            .filter_map(|in_out| {
376                wallet_utxos
377                    .iter()
378                    .position(|tx_out| {
379                        tx_out.unblinded.asset == in_out.asset_id
380                            && tx_out.unblinded.value == in_out.value
381                    })
382                    .map(|index| wallet_utxos.remove(index).outpoint)
383            })
384            .collect::<Vec<_>>();
385        ensure_sdk!(
386            selected_utxos.len() == selected.len(),
387            PaymentError::generic("Failed to resolve selected wallet utxos to outpoints")
388        );
389        Ok(selected_utxos)
390    }
391}
392
393pub fn get_descriptor(signer: &SdkLwkSigner) -> Result<WolletDescriptor, PaymentError> {
394    let descriptor_str = singlesig_desc(
395        signer,
396        Singlesig::Wpkh,
397        lwk_common::DescriptorBlindingKey::Slip77,
398    )
399    .map_err(|e| anyhow!("Invalid descriptor: {e}"))?;
400    Ok(descriptor_str.parse()?)
401}
402
403#[sdk_macros::async_trait]
404impl OnchainWallet for LiquidOnchainWallet {
405    /// List all transactions in the wallet
406    async fn transactions(&self) -> Result<Vec<WalletTx>, PaymentError> {
407        let wallet = self.wallet.lock().await;
408        wallet.transactions().map_err(|e| PaymentError::Generic {
409            err: format!("Failed to fetch wallet transactions: {e:?}"),
410        })
411    }
412
413    /// List all transactions in the wallet mapped by tx id
414    async fn transactions_by_tx_id(&self) -> Result<HashMap<Txid, WalletTx>, PaymentError> {
415        let tx_map: HashMap<Txid, WalletTx> = self
416            .transactions()
417            .await?
418            .iter()
419            .map(|tx| (tx.txid, tx.clone()))
420            .collect();
421        Ok(tx_map)
422    }
423
424    async fn asset_utxos(&self, asset: &AssetId) -> Result<Vec<WalletTxOut>, PaymentError> {
425        Ok(self
426            .wallet
427            .lock()
428            .await
429            .utxos()?
430            .into_iter()
431            .filter(|utxo| &utxo.unblinded.asset == asset)
432            .collect())
433    }
434
435    /// Build a transaction to send funds to a recipient
436    async fn build_tx(
437        &self,
438        fee_rate_sats_per_kvb: Option<f32>,
439        recipient_address: &str,
440        asset_id: &str,
441        amount_sat: u64,
442    ) -> Result<Transaction, PaymentError> {
443        let lwk_wollet = self.wallet.lock().await;
444        let address =
445            ElementsAddress::from_str(recipient_address).map_err(|e| PaymentError::Generic {
446                err: format!(
447                    "Recipient address {recipient_address} is not a valid ElementsAddress: {e:?}"
448                ),
449            })?;
450        let mut tx_builder = lwk_wollet::TxBuilder::new(self.config.network.into())
451            .fee_rate(fee_rate_sats_per_kvb)
452            .enable_ct_discount();
453        if asset_id.eq(&self.config.lbtc_asset_id()) {
454            // If the asset is L-BTC, try to select wallet utxos for the recipient amount.
455            // If it fails to select utxos, the LWK wallet will select the utxos for us.
456            let policy_asset = lwk_wollet.policy_asset();
457            // TODO: LWK only supports selecting utxos for the policy asset, in the future
458            // we should be able to select utxos for any asset.
459            match self.select_wallet_utxos(
460                &lwk_wollet,
461                policy_asset,
462                policy_asset,
463                vec![InOut {
464                    asset_id: policy_asset,
465                    value: amount_sat,
466                }],
467                fee_rate_sats_per_kvb,
468            ) {
469                Ok(wallet_utxos) => {
470                    tx_builder = tx_builder.set_wallet_utxos(wallet_utxos);
471                }
472                Err(e) => warn!("Failed to select wallet utxos: {e:?}"),
473            }
474            // Add the L-BTC recipient
475            tx_builder = tx_builder.add_lbtc_recipient(&address, amount_sat)?;
476        } else {
477            // Add the asset recipient
478            let asset = AssetId::from_str(asset_id)?;
479            // Explicitly select the asset utxos plus a bounded set of L-BTC utxos
480            // for the fee. If selection fails, fall back to letting lwk select the
481            // utxos (which adds all L-BTC inputs).
482            match self.select_asset_and_fee_utxos(
483                &lwk_wollet,
484                asset,
485                amount_sat,
486                fee_rate_sats_per_kvb,
487            ) {
488                Ok(wallet_utxos) => {
489                    tx_builder = tx_builder.set_wallet_utxos(wallet_utxos);
490                }
491                Err(e) => warn!("Failed to select asset and fee wallet utxos: {e:?}"),
492            }
493            tx_builder = tx_builder.add_recipient(&address, amount_sat, asset)?;
494        }
495        let mut pset = tx_builder.finish(&lwk_wollet)?;
496        self.signer
497            .sign(&mut pset)
498            .map_err(|e| PaymentError::Generic {
499                err: format!("Failed to sign transaction: {e:?}"),
500            })?;
501        Ok(lwk_wollet.finalize(&mut pset)?)
502    }
503
504    async fn build_drain_tx(
505        &self,
506        fee_rate_sats_per_kvb: Option<f32>,
507        recipient_address: &str,
508        enforce_amount_sat: Option<u64>,
509    ) -> Result<Transaction, PaymentError> {
510        let lwk_wollet = self.wallet.lock().await;
511
512        let address =
513            ElementsAddress::from_str(recipient_address).map_err(|e| PaymentError::Generic {
514                err: format!(
515                    "Recipient address {recipient_address} is not a valid ElementsAddress: {e:?}"
516                ),
517            })?;
518        let mut pset = lwk_wollet
519            .tx_builder()
520            .drain_lbtc_wallet()
521            .drain_lbtc_to(address)
522            .fee_rate(fee_rate_sats_per_kvb)
523            .enable_ct_discount()
524            .finish()?;
525
526        if let Some(enforce_amount_sat) = enforce_amount_sat {
527            let pset_details = lwk_wollet.get_details(&pset)?;
528            let pset_balance_sat = pset_details
529                .balance
530                .balances
531                .get(&lwk_wollet.policy_asset())
532                .unwrap_or(&0);
533            let pset_fees = pset_details.balance.fees_in(&lwk_wollet.policy_asset());
534
535            ensure_sdk!(
536                (*pset_balance_sat * -1) as u64 - pset_fees == enforce_amount_sat,
537                PaymentError::Generic {
538                    err: format!("Drain tx amount {pset_balance_sat} sat doesn't match enforce_amount_sat {enforce_amount_sat} sat")
539                }
540            );
541        }
542
543        self.signer
544            .sign(&mut pset)
545            .map_err(|e| PaymentError::Generic {
546                err: format!("Failed to sign transaction: {e:?}"),
547            })?;
548        Ok(lwk_wollet.finalize(&mut pset)?)
549    }
550
551    async fn build_tx_or_drain_tx(
552        &self,
553        fee_rate_sats_per_kvb: Option<f32>,
554        recipient_address: &str,
555        asset_id: &str,
556        amount_sat: u64,
557    ) -> Result<Transaction, PaymentError> {
558        match self
559            .build_tx(
560                fee_rate_sats_per_kvb,
561                recipient_address,
562                asset_id,
563                amount_sat,
564            )
565            .await
566        {
567            Ok(tx) => Ok(tx),
568            Err(PaymentError::InsufficientFunds) if asset_id.eq(&self.config.lbtc_asset_id()) => {
569                warn!("Cannot build tx due to insufficient funds, attempting to build drain tx");
570                self.build_drain_tx(fee_rate_sats_per_kvb, recipient_address, Some(amount_sat))
571                    .await
572            }
573            Err(e) => Err(e),
574        }
575    }
576
577    async fn sign_pset(&self, pset: &mut PartiallySignedTransaction) -> Result<(), PaymentError> {
578        let lwk_wollet = self.wallet.lock().await;
579
580        // Get the tx_out for each input and add the rangeproof/witness utxo
581        for input in pset.inputs_mut().iter_mut() {
582            let tx_out_res = self
583                .get_txout(
584                    &lwk_wollet,
585                    &OutPoint {
586                        txid: input.previous_txid,
587                        vout: input.previous_output_index,
588                    },
589                )
590                .await;
591            if let Ok(mut tx_out) = tx_out_res {
592                input.in_utxo_rangeproof = tx_out.witness.rangeproof.take();
593                input.witness_utxo = Some(tx_out);
594            }
595        }
596
597        lwk_wollet.add_details(pset)?;
598
599        self.signer.sign(pset).map_err(|e| PaymentError::Generic {
600            err: format!("Failed to sign transaction: {e:?}"),
601        })?;
602
603        // Set the final script witness for each input adding the signature and any missing public key
604        for input in pset.inputs_mut() {
605            if let Some((public_key, input_sign)) = input.partial_sigs.iter().next() {
606                input.final_script_witness = Some(vec![input_sign.clone(), public_key.to_bytes()]);
607            }
608        }
609
610        Ok(())
611    }
612
613    /// Get the next unused address in the wallet
614    async fn next_unused_address(&self) -> Result<Address, PaymentError> {
615        let tip = self.tip().await;
616        let address = match self.persister.next_expired_reserved_address(tip)? {
617            Some(reserved_address) => {
618                debug!(
619                    "Got reserved address {} that expired on block height {}",
620                    reserved_address.address, reserved_address.expiry_block_height
621                );
622                ElementsAddress::from_str(&reserved_address.address)
623                    .map_err(|e| PaymentError::Generic { err: e.to_string() })?
624            }
625            None => {
626                let next_index = self.persister.next_derivation_index()?;
627                let address_result = self.wallet.lock().await.address(next_index)?;
628                let address = address_result.address().clone();
629                let index = address_result.index();
630                debug!("Got unused address {address} with derivation index {index}");
631                if next_index.is_none() {
632                    self.persister.set_last_derivation_index(index)?;
633                }
634                address
635            }
636        };
637
638        Ok(address)
639    }
640
641    /// Get the next unused change address in the wallet
642    async fn next_unused_change_address(&self) -> Result<Address, PaymentError> {
643        let address = self.wallet.lock().await.change(None)?.address().clone();
644
645        Ok(address)
646    }
647
648    /// Get the current tip of the blockchain the wallet is aware of
649    async fn tip(&self) -> u32 {
650        self.wallet.lock().await.tip().height()
651    }
652
653    /// Get the public key of the wallet
654    fn pubkey(&self) -> Result<String> {
655        Ok(self.signer.xpub()?.public_key.to_string())
656    }
657
658    /// Get the fingerprint of the wallet
659    fn fingerprint(&self) -> Result<String> {
660        Ok(self.signer.fingerprint()?.to_hex())
661    }
662
663    /// Perform a full scan of the wallet
664    async fn full_scan(&self) -> Result<(), PaymentError> {
665        debug!("LiquidOnchainWallet::full_scan: start");
666        let full_scan_started = Instant::now();
667
668        // create electrum client if doesn't already exist
669        let mut client = self.client.lock().await;
670        if client.is_none() {
671            *client = Some(WalletClient::from_config(&self.config)?);
672        }
673        let client = client.as_mut().ok_or_else(|| PaymentError::Generic {
674            err: "Wallet client not initialized".to_string(),
675        })?;
676
677        // Use the cached derivation index with a buffer of 5 to perform the scan
678        let last_derivation_index = self
679            .persister
680            .get_last_derivation_index()?
681            .unwrap_or_default();
682        let index_with_buffer = last_derivation_index + 5;
683        let mut wallet = self.wallet.lock().await;
684
685        // Reunblind the wallet txs if there has been a change in the derivation index since the
686        // last full scan
687        if self
688            .persister
689            .get_last_scanned_derivation_index()?
690            .is_some_and(|index| index != last_derivation_index)
691        {
692            debug!("LiquidOnchainWallet::full_scan: reunblinding all transactions");
693            wallet.reunblind()?;
694        }
695
696        let res = match client
697            .full_scan_to_index(&mut wallet, index_with_buffer)
698            .await
699        {
700            Ok(()) => Ok(()),
701            Err(e)
702                if matches!(
703                    e,
704                    lwk_wollet::Error::UpdateHeightTooOld { .. }
705                        | lwk_wollet::Error::UpdateOnDifferentStatus { .. }
706                        | lwk_wollet::Error::StoreError(_)
707                ) =>
708            {
709                warn!("Full scan failed due to {e}, reloading wallet and retrying");
710                let mut new_wallet = Self::create_wallet(
711                    &self.config,
712                    &self.signer,
713                    self.wallet_cache_persister.clone(),
714                )
715                .await?;
716                client
717                    .full_scan_to_index(&mut new_wallet, index_with_buffer)
718                    .await?;
719                *wallet = new_wallet;
720                Ok(())
721            }
722            Err(e) => Err(e.into()),
723        };
724
725        self.persister
726            .set_last_scanned_derivation_index(last_derivation_index)?;
727
728        let duration_ms = Instant::now().duration_since(full_scan_started).as_millis();
729        info!("lwk wallet full_scan duration: ({duration_ms} ms)");
730        debug!("LiquidOnchainWallet::full_scan: end");
731        res
732    }
733
734    fn sign_message(&self, message: &str) -> Result<String> {
735        // Prefix and double hash message
736        let mut engine = sha256::HashEngine::default();
737        engine.write_all(LN_MESSAGE_PREFIX)?;
738        engine.write_all(message.as_bytes())?;
739        let hashed_msg = sha256::Hash::from_engine(engine);
740        let double_hashed_msg = Message::from_digest(sha256::Hash::hash(&hashed_msg).into_inner());
741        // Get message signature and encode to zbase32
742        let recoverable_sig = self.signer.sign_ecdsa_recoverable(&double_hashed_msg)?;
743        Ok(zbase32::encode_full_bytes(recoverable_sig.as_slice()))
744    }
745
746    fn check_message(&self, message: &str, pubkey: &str, signature: &str) -> Result<bool> {
747        let pk = PublicKey::from_str(pubkey)?;
748        Ok(verify(message.as_bytes(), signature, &pk))
749    }
750}
751
752#[cfg(test)]
753mod tests {
754    use super::*;
755    use crate::model::Config;
756    use crate::signer::SdkSigner;
757    use crate::test_utils::persist::create_persister;
758    use crate::wallet::LiquidOnchainWallet;
759    use anyhow::Result;
760
761    #[cfg(feature = "browser-tests")]
762    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
763
764    #[sdk_macros::async_test_all]
765    async fn test_sign_and_check_message() -> Result<()> {
766        let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
767        let sdk_signer: Box<dyn Signer> = Box::new(SdkSigner::new(mnemonic, "", false).unwrap());
768        let sdk_signer = Arc::new(sdk_signer);
769
770        let config = Config::regtest_esplora();
771
772        create_persister!(storage);
773
774        let wallet: Arc<dyn OnchainWallet> = Arc::new(
775            LiquidOnchainWallet::new(config, storage, sdk_signer.clone())
776                .await
777                .unwrap(),
778        );
779
780        // Test message
781        let message = "Hello, Liquid!";
782
783        // Sign the message
784        let signature = wallet.sign_message(message).unwrap();
785
786        // Get the public key
787        let pubkey = wallet.pubkey().unwrap();
788
789        // Check the message
790        let is_valid = wallet.check_message(message, &pubkey, &signature).unwrap();
791        assert!(is_valid, "Message signature should be valid");
792
793        // Check with an incorrect message
794        let incorrect_message = "Wrong message";
795        let is_invalid = wallet
796            .check_message(incorrect_message, &pubkey, &signature)
797            .unwrap();
798        assert!(
799            !is_invalid,
800            "Message signature should be invalid for incorrect message"
801        );
802
803        // Check with an incorrect public key
804        let incorrect_pubkey = "02a1633cafcc01ebfb6d78e39f687a1f0995c62fc95f51ead10a02ee0be551b5dc";
805        let is_invalid = wallet
806            .check_message(message, incorrect_pubkey, &signature)
807            .unwrap();
808        assert!(
809            !is_invalid,
810            "Message signature should be invalid for incorrect public key"
811        );
812
813        // Check with an incorrect signature
814        let incorrect_signature = zbase32::encode_full_bytes(&[0; 65]);
815        let is_invalid = wallet
816            .check_message(message, &pubkey, &incorrect_signature)
817            .unwrap();
818        assert!(
819            !is_invalid,
820            "Message signature should be invalid for incorrect signature"
821        );
822
823        // The temporary directory will be automatically deleted when temp_dir goes out of scope
824        Ok(())
825    }
826}