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::{ElementsNetwork, 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 = Box::new(
149                    builder
150                        .timeout(config.onchain_sync_request_timeout_sec as u8)
151                        .waterfalls(waterfalls)
152                        .build(),
153                );
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            debug!(
176                "WalletClient::full_scan_to_index: applying update {}",
177                update.version
178            );
179            wallet.apply_update(update)?;
180        }
181
182        Ok(())
183    }
184}
185
186pub struct LiquidOnchainWallet {
187    config: Config,
188    persister: std::sync::Arc<Persister>,
189    wallet: Arc<Mutex<Wollet>>,
190    client: Mutex<Option<WalletClient>>,
191    pub(crate) signer: SdkLwkSigner,
192    wallet_cache_persister: Arc<dyn WalletCachePersister>,
193}
194
195impl LiquidOnchainWallet {
196    /// Creates a new LiquidOnchainWallet that caches data on the provided `working_dir`.
197    pub(crate) async fn new(
198        config: Config,
199        persister: std::sync::Arc<Persister>,
200        user_signer: Arc<Box<dyn Signer>>,
201    ) -> Result<Self> {
202        let signer = SdkLwkSigner::new(user_signer.clone())?;
203
204        let wallet_cache_persister: Arc<dyn WalletCachePersister> =
205            Arc::new(SqliteWalletCachePersister::new(
206                std::sync::Arc::clone(&persister),
207                get_descriptor(&signer)?,
208            )?);
209
210        let wollet = Self::create_wallet(&config, &signer, wallet_cache_persister.clone()).await?;
211
212        Ok(Self {
213            config,
214            persister,
215            wallet: Arc::new(Mutex::new(wollet)),
216            client: Mutex::new(None),
217            signer,
218            wallet_cache_persister,
219        })
220    }
221
222    async fn create_wallet(
223        config: &Config,
224        signer: &SdkLwkSigner,
225        wallet_cache_persister: Arc<dyn WalletCachePersister>,
226    ) -> Result<Wollet> {
227        let elements_network: ElementsNetwork = config.network.into();
228        let descriptor = get_descriptor(signer)?;
229        let wollet_res = Wollet::new(
230            elements_network,
231            wallet_cache_persister.get_lwk_persister()?,
232            descriptor.clone(),
233        );
234        match wollet_res {
235            Ok(wollet) => Ok(wollet),
236            res @ Err(
237                lwk_wollet::Error::PersistError(_)
238                | lwk_wollet::Error::UpdateHeightTooOld { .. }
239                | lwk_wollet::Error::UpdateOnDifferentStatus { .. },
240            ) => {
241                warn!("Update error initialising wollet, wiping cache and retrying: {res:?}");
242                wallet_cache_persister.clear_cache().await?;
243                Ok(Wollet::new(
244                    elements_network,
245                    wallet_cache_persister.get_lwk_persister()?,
246                    descriptor.clone(),
247                )?)
248            }
249            Err(e) => Err(e.into()),
250        }
251    }
252
253    async fn get_txout(&self, wallet: &Wollet, outpoint: &OutPoint) -> Result<TxOut> {
254        let wallet_tx = wallet
255            .transaction(&outpoint.txid)?
256            .ok_or(anyhow!("Transaction not found"))?;
257        let tx_out = wallet_tx
258            .tx
259            .output
260            .get(outpoint.vout as usize)
261            .ok_or(anyhow!("Output not found"))?;
262        Ok(tx_out.clone())
263    }
264
265    fn select_wallet_utxos(
266        &self,
267        wallet: &Wollet,
268        policy_asset: AssetId,
269        selection_asset: AssetId,
270        recipient_outputs: Vec<InOut>,
271        fee_rate_sats_per_kvb: Option<f32>,
272    ) -> Result<Vec<OutPoint>, PaymentError> {
273        let mut wallet_utxos = wallet.utxos()?;
274        debug!(
275            "Wallet utxos: {:?}",
276            wallet_utxos
277                .iter()
278                .map(|tx_out| format!(
279                    "{}:{}, value: {}",
280                    tx_out.outpoint.txid, tx_out.outpoint.vout, tx_out.unblinded.value
281                ))
282                .collect::<Vec<_>>()
283        );
284        let fee_rate = fee_rate_sats_per_kvb.map(|rate| rate as f64 / 1000.0);
285        let selected_in_outs = utxo_select::utxo_select(WalletUtxoSelectRequest {
286            policy_asset,
287            selection_asset,
288            wallet_utxos: wallet_utxos.iter().map(Into::into).collect(),
289            recipient_outputs,
290            fee_rate,
291        })?;
292        let selected_utxos = selected_in_outs
293            .iter()
294            .filter_map(|in_out| {
295                wallet_utxos
296                    .iter()
297                    .position(|tx_out| {
298                        tx_out.unblinded.asset == in_out.asset_id
299                            && tx_out.unblinded.value == in_out.value
300                    })
301                    .map(|index| wallet_utxos.remove(index).outpoint)
302            })
303            .collect::<Vec<_>>();
304        ensure_sdk!(
305            selected_utxos.len() == selected_in_outs.len(),
306            PaymentError::generic("Failed to select wallet utxos")
307        );
308        debug!(
309            "Selected wallet outputs: {:?}",
310            selected_utxos
311                .iter()
312                .map(|outpoint| format!("{}:{}", outpoint.txid, outpoint.vout))
313                .collect::<Vec<_>>()
314        );
315        Ok(selected_utxos)
316    }
317}
318
319pub fn get_descriptor(signer: &SdkLwkSigner) -> Result<WolletDescriptor, PaymentError> {
320    let descriptor_str = singlesig_desc(
321        signer,
322        Singlesig::Wpkh,
323        lwk_common::DescriptorBlindingKey::Slip77,
324    )
325    .map_err(|e| anyhow!("Invalid descriptor: {e}"))?;
326    Ok(descriptor_str.parse()?)
327}
328
329#[sdk_macros::async_trait]
330impl OnchainWallet for LiquidOnchainWallet {
331    /// List all transactions in the wallet
332    async fn transactions(&self) -> Result<Vec<WalletTx>, PaymentError> {
333        let wallet = self.wallet.lock().await;
334        wallet.transactions().map_err(|e| PaymentError::Generic {
335            err: format!("Failed to fetch wallet transactions: {e:?}"),
336        })
337    }
338
339    /// List all transactions in the wallet mapped by tx id
340    async fn transactions_by_tx_id(&self) -> Result<HashMap<Txid, WalletTx>, PaymentError> {
341        let tx_map: HashMap<Txid, WalletTx> = self
342            .transactions()
343            .await?
344            .iter()
345            .map(|tx| (tx.txid, tx.clone()))
346            .collect();
347        Ok(tx_map)
348    }
349
350    async fn asset_utxos(&self, asset: &AssetId) -> Result<Vec<WalletTxOut>, PaymentError> {
351        Ok(self
352            .wallet
353            .lock()
354            .await
355            .utxos()?
356            .into_iter()
357            .filter(|utxo| &utxo.unblinded.asset == asset)
358            .collect())
359    }
360
361    /// Build a transaction to send funds to a recipient
362    async fn build_tx(
363        &self,
364        fee_rate_sats_per_kvb: Option<f32>,
365        recipient_address: &str,
366        asset_id: &str,
367        amount_sat: u64,
368    ) -> Result<Transaction, PaymentError> {
369        let lwk_wollet = self.wallet.lock().await;
370        let address =
371            ElementsAddress::from_str(recipient_address).map_err(|e| PaymentError::Generic {
372                err: format!(
373                    "Recipient address {recipient_address} is not a valid ElementsAddress: {e:?}"
374                ),
375            })?;
376        let mut tx_builder = lwk_wollet::TxBuilder::new(self.config.network.into())
377            .fee_rate(fee_rate_sats_per_kvb)
378            .enable_ct_discount();
379        if asset_id.eq(&self.config.lbtc_asset_id()) {
380            // If the asset is L-BTC, try to select wallet utxos for the recipient amount.
381            // If it fails to select utxos, the LWK wallet will select the utxos for us.
382            let policy_asset = lwk_wollet.policy_asset();
383            // TODO: LWK only supports selecting utxos for the policy asset, in the future
384            // we should be able to select utxos for any asset.
385            match self.select_wallet_utxos(
386                &lwk_wollet,
387                policy_asset,
388                policy_asset,
389                vec![InOut {
390                    asset_id: policy_asset,
391                    value: amount_sat,
392                }],
393                fee_rate_sats_per_kvb,
394            ) {
395                Ok(wallet_utxos) => {
396                    tx_builder = tx_builder.set_wallet_utxos(wallet_utxos);
397                }
398                Err(e) => warn!("Failed to select wallet utxos: {e:?}"),
399            }
400            // Add the L-BTC recipient
401            tx_builder = tx_builder.add_lbtc_recipient(&address, amount_sat)?;
402        } else {
403            // Add the asset recipient
404            let asset = AssetId::from_str(asset_id)?;
405            tx_builder = tx_builder.add_recipient(&address, amount_sat, asset)?;
406        }
407        let mut pset = tx_builder.finish(&lwk_wollet)?;
408        self.signer
409            .sign(&mut pset)
410            .map_err(|e| PaymentError::Generic {
411                err: format!("Failed to sign transaction: {e:?}"),
412            })?;
413        Ok(lwk_wollet.finalize(&mut pset)?)
414    }
415
416    async fn build_drain_tx(
417        &self,
418        fee_rate_sats_per_kvb: Option<f32>,
419        recipient_address: &str,
420        enforce_amount_sat: Option<u64>,
421    ) -> Result<Transaction, PaymentError> {
422        let lwk_wollet = self.wallet.lock().await;
423
424        let address =
425            ElementsAddress::from_str(recipient_address).map_err(|e| PaymentError::Generic {
426                err: format!(
427                    "Recipient address {recipient_address} is not a valid ElementsAddress: {e:?}"
428                ),
429            })?;
430        let mut pset = lwk_wollet
431            .tx_builder()
432            .drain_lbtc_wallet()
433            .drain_lbtc_to(address)
434            .fee_rate(fee_rate_sats_per_kvb)
435            .enable_ct_discount()
436            .finish()?;
437
438        if let Some(enforce_amount_sat) = enforce_amount_sat {
439            let pset_details = lwk_wollet.get_details(&pset)?;
440            let pset_balance_sat = pset_details
441                .balance
442                .balances
443                .get(&lwk_wollet.policy_asset())
444                .unwrap_or(&0);
445            let pset_fees = pset_details.balance.fee;
446
447            ensure_sdk!(
448                (*pset_balance_sat * -1) as u64 - pset_fees == enforce_amount_sat,
449                PaymentError::Generic {
450                    err: format!("Drain tx amount {pset_balance_sat} sat doesn't match enforce_amount_sat {enforce_amount_sat} sat")
451                }
452            );
453        }
454
455        self.signer
456            .sign(&mut pset)
457            .map_err(|e| PaymentError::Generic {
458                err: format!("Failed to sign transaction: {e:?}"),
459            })?;
460        Ok(lwk_wollet.finalize(&mut pset)?)
461    }
462
463    async fn build_tx_or_drain_tx(
464        &self,
465        fee_rate_sats_per_kvb: Option<f32>,
466        recipient_address: &str,
467        asset_id: &str,
468        amount_sat: u64,
469    ) -> Result<Transaction, PaymentError> {
470        match self
471            .build_tx(
472                fee_rate_sats_per_kvb,
473                recipient_address,
474                asset_id,
475                amount_sat,
476            )
477            .await
478        {
479            Ok(tx) => Ok(tx),
480            Err(PaymentError::InsufficientFunds) if asset_id.eq(&self.config.lbtc_asset_id()) => {
481                warn!("Cannot build tx due to insufficient funds, attempting to build drain tx");
482                self.build_drain_tx(fee_rate_sats_per_kvb, recipient_address, Some(amount_sat))
483                    .await
484            }
485            Err(e) => Err(e),
486        }
487    }
488
489    async fn sign_pset(&self, pset: &mut PartiallySignedTransaction) -> Result<(), PaymentError> {
490        let lwk_wollet = self.wallet.lock().await;
491
492        // Get the tx_out for each input and add the rangeproof/witness utxo
493        for input in pset.inputs_mut().iter_mut() {
494            let tx_out_res = self
495                .get_txout(
496                    &lwk_wollet,
497                    &OutPoint {
498                        txid: input.previous_txid,
499                        vout: input.previous_output_index,
500                    },
501                )
502                .await;
503            if let Ok(mut tx_out) = tx_out_res {
504                input.in_utxo_rangeproof = tx_out.witness.rangeproof.take();
505                input.witness_utxo = Some(tx_out);
506            }
507        }
508
509        lwk_wollet.add_details(pset)?;
510
511        self.signer.sign(pset).map_err(|e| PaymentError::Generic {
512            err: format!("Failed to sign transaction: {e:?}"),
513        })?;
514
515        // Set the final script witness for each input adding the signature and any missing public key
516        for input in pset.inputs_mut() {
517            if let Some((public_key, input_sign)) = input.partial_sigs.iter().next() {
518                input.final_script_witness = Some(vec![input_sign.clone(), public_key.to_bytes()]);
519            }
520        }
521
522        Ok(())
523    }
524
525    /// Get the next unused address in the wallet
526    async fn next_unused_address(&self) -> Result<Address, PaymentError> {
527        let tip = self.tip().await;
528        let address = match self.persister.next_expired_reserved_address(tip)? {
529            Some(reserved_address) => {
530                debug!(
531                    "Got reserved address {} that expired on block height {}",
532                    reserved_address.address, reserved_address.expiry_block_height
533                );
534                ElementsAddress::from_str(&reserved_address.address)
535                    .map_err(|e| PaymentError::Generic { err: e.to_string() })?
536            }
537            None => {
538                let next_index = self.persister.next_derivation_index()?;
539                let address_result = self.wallet.lock().await.address(next_index)?;
540                let address = address_result.address().clone();
541                let index = address_result.index();
542                debug!("Got unused address {address} with derivation index {index}");
543                if next_index.is_none() {
544                    self.persister.set_last_derivation_index(index)?;
545                }
546                address
547            }
548        };
549
550        Ok(address)
551    }
552
553    /// Get the next unused change address in the wallet
554    async fn next_unused_change_address(&self) -> Result<Address, PaymentError> {
555        let address = self.wallet.lock().await.change(None)?.address().clone();
556
557        Ok(address)
558    }
559
560    /// Get the current tip of the blockchain the wallet is aware of
561    async fn tip(&self) -> u32 {
562        self.wallet.lock().await.tip().height()
563    }
564
565    /// Get the public key of the wallet
566    fn pubkey(&self) -> Result<String> {
567        Ok(self.signer.xpub()?.public_key.to_string())
568    }
569
570    /// Get the fingerprint of the wallet
571    fn fingerprint(&self) -> Result<String> {
572        Ok(self.signer.fingerprint()?.to_hex())
573    }
574
575    /// Perform a full scan of the wallet
576    async fn full_scan(&self) -> Result<(), PaymentError> {
577        debug!("LiquidOnchainWallet::full_scan: start");
578        let full_scan_started = Instant::now();
579
580        // create electrum client if doesn't already exist
581        let mut client = self.client.lock().await;
582        if client.is_none() {
583            *client = Some(WalletClient::from_config(&self.config)?);
584        }
585        let client = client.as_mut().ok_or_else(|| PaymentError::Generic {
586            err: "Wallet client not initialized".to_string(),
587        })?;
588
589        // Use the cached derivation index with a buffer of 5 to perform the scan
590        let last_derivation_index = self
591            .persister
592            .get_last_derivation_index()?
593            .unwrap_or_default();
594        let index_with_buffer = last_derivation_index + 5;
595        let mut wallet = self.wallet.lock().await;
596
597        // Reunblind the wallet txs if there has been a change in the derivation index since the
598        // last full scan
599        if self
600            .persister
601            .get_last_scanned_derivation_index()?
602            .is_some_and(|index| index != last_derivation_index)
603        {
604            debug!("LiquidOnchainWallet::full_scan: reunblinding all transactions");
605            wallet.reunblind()?;
606        }
607
608        let res = match client
609            .full_scan_to_index(&mut wallet, index_with_buffer)
610            .await
611        {
612            Ok(()) => Ok(()),
613            Err(e)
614                if matches!(
615                    e,
616                    lwk_wollet::Error::UpdateHeightTooOld { .. }
617                        | lwk_wollet::Error::PersistError(_)
618                ) =>
619            {
620                warn!("Full scan failed due to {e}, reloading wallet and retrying");
621                let mut new_wallet = Self::create_wallet(
622                    &self.config,
623                    &self.signer,
624                    self.wallet_cache_persister.clone(),
625                )
626                .await?;
627                client
628                    .full_scan_to_index(&mut new_wallet, index_with_buffer)
629                    .await?;
630                *wallet = new_wallet;
631                Ok(())
632            }
633            Err(e) => Err(e.into()),
634        };
635
636        self.persister
637            .set_last_scanned_derivation_index(last_derivation_index)?;
638
639        let duration_ms = Instant::now().duration_since(full_scan_started).as_millis();
640        info!("lwk wallet full_scan duration: ({duration_ms} ms)");
641        debug!("LiquidOnchainWallet::full_scan: end");
642        res
643    }
644
645    fn sign_message(&self, message: &str) -> Result<String> {
646        // Prefix and double hash message
647        let mut engine = sha256::HashEngine::default();
648        engine.write_all(LN_MESSAGE_PREFIX)?;
649        engine.write_all(message.as_bytes())?;
650        let hashed_msg = sha256::Hash::from_engine(engine);
651        let double_hashed_msg = Message::from_digest(sha256::Hash::hash(&hashed_msg).into_inner());
652        // Get message signature and encode to zbase32
653        let recoverable_sig = self.signer.sign_ecdsa_recoverable(&double_hashed_msg)?;
654        Ok(zbase32::encode_full_bytes(recoverable_sig.as_slice()))
655    }
656
657    fn check_message(&self, message: &str, pubkey: &str, signature: &str) -> Result<bool> {
658        let pk = PublicKey::from_str(pubkey)?;
659        Ok(verify(message.as_bytes(), signature, &pk))
660    }
661}
662
663#[cfg(test)]
664mod tests {
665    use super::*;
666    use crate::model::Config;
667    use crate::signer::SdkSigner;
668    use crate::test_utils::persist::create_persister;
669    use crate::wallet::LiquidOnchainWallet;
670    use anyhow::Result;
671
672    #[cfg(feature = "browser-tests")]
673    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
674
675    #[sdk_macros::async_test_all]
676    async fn test_sign_and_check_message() -> Result<()> {
677        let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
678        let sdk_signer: Box<dyn Signer> = Box::new(SdkSigner::new(mnemonic, "", false).unwrap());
679        let sdk_signer = Arc::new(sdk_signer);
680
681        let config = Config::regtest_esplora();
682
683        create_persister!(storage);
684
685        let wallet: Arc<dyn OnchainWallet> = Arc::new(
686            LiquidOnchainWallet::new(config, storage, sdk_signer.clone())
687                .await
688                .unwrap(),
689        );
690
691        // Test message
692        let message = "Hello, Liquid!";
693
694        // Sign the message
695        let signature = wallet.sign_message(message).unwrap();
696
697        // Get the public key
698        let pubkey = wallet.pubkey().unwrap();
699
700        // Check the message
701        let is_valid = wallet.check_message(message, &pubkey, &signature).unwrap();
702        assert!(is_valid, "Message signature should be valid");
703
704        // Check with an incorrect message
705        let incorrect_message = "Wrong message";
706        let is_invalid = wallet
707            .check_message(incorrect_message, &pubkey, &signature)
708            .unwrap();
709        assert!(
710            !is_invalid,
711            "Message signature should be invalid for incorrect message"
712        );
713
714        // Check with an incorrect public key
715        let incorrect_pubkey = "02a1633cafcc01ebfb6d78e39f687a1f0995c62fc95f51ead10a02ee0be551b5dc";
716        let is_invalid = wallet
717            .check_message(message, incorrect_pubkey, &signature)
718            .unwrap();
719        assert!(
720            !is_invalid,
721            "Message signature should be invalid for incorrect public key"
722        );
723
724        // Check with an incorrect signature
725        let incorrect_signature = zbase32::encode_full_bytes(&[0; 65]);
726        let is_invalid = wallet
727            .check_message(message, &pubkey, &incorrect_signature)
728            .unwrap();
729        assert!(
730            !is_invalid,
731            "Message signature should be invalid for incorrect signature"
732        );
733
734        // The temporary directory will be automatically deleted when temp_dir goes out of scope
735        Ok(())
736    }
737}