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