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 async fn transactions(&self) -> Result<Vec<WalletTx>, PaymentError>;
44
45 async fn transactions_by_tx_id(&self) -> Result<HashMap<Txid, WalletTx>, PaymentError>;
47
48 async fn asset_utxos(&self, asset: &AssetId) -> Result<Vec<WalletTxOut>, PaymentError>;
50
51 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 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 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 async fn sign_pset(&self, pset: &mut PartiallySignedTransaction) -> Result<(), PaymentError>;
87
88 async fn next_unused_address(&self) -> Result<Address, PaymentError>;
90
91 async fn next_unused_change_address(&self) -> Result<Address, PaymentError>;
93
94 async fn tip(&self) -> u32;
96
97 fn pubkey(&self) -> Result<String>;
99
100 fn fingerprint(&self) -> Result<String>;
102
103 fn sign_message(&self, msg: &str) -> Result<String>;
106
107 fn check_message(&self, message: &str, pubkey: &str, signature: &str) -> Result<bool>;
110
111 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 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 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 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 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 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 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 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 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 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 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 let policy_asset = lwk_wollet.policy_asset();
457 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 tx_builder = tx_builder.add_lbtc_recipient(&address, amount_sat)?;
476 } else {
477 let asset = AssetId::from_str(asset_id)?;
479 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 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 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 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 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 async fn tip(&self) -> u32 {
650 self.wallet.lock().await.tip().height()
651 }
652
653 fn pubkey(&self) -> Result<String> {
655 Ok(self.signer.xpub()?.public_key.to_string())
656 }
657
658 fn fingerprint(&self) -> Result<String> {
660 Ok(self.signer.fingerprint()?.to_hex())
661 }
662
663 async fn full_scan(&self) -> Result<(), PaymentError> {
665 debug!("LiquidOnchainWallet::full_scan: start");
666 let full_scan_started = Instant::now();
667
668 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 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 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 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 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 let message = "Hello, Liquid!";
782
783 let signature = wallet.sign_message(message).unwrap();
785
786 let pubkey = wallet.pubkey().unwrap();
788
789 let is_valid = wallet.check_message(message, &pubkey, &signature).unwrap();
791 assert!(is_valid, "Message signature should be valid");
792
793 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 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 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 Ok(())
825 }
826}