breez_sdk_liquid/chain/liquid/
electrum.rs1#![cfg(not(all(target_family = "wasm", target_os = "unknown")))]
2
3use std::{sync::OnceLock, time::Duration};
4
5use anyhow::{anyhow, bail, Context as _, Result};
6use tokio::sync::RwLock;
7
8use crate::{
9 elements::{Address, OutPoint, Script, Transaction, Txid},
10 model::{BlockchainExplorer, Config, Utxo},
11 utils,
12};
13
14use log::info;
15use lwk_wollet::{
16 clients::blocking::BlockchainBackend as _, elements::hex::FromHex as _, ElectrumClient,
17};
18use sdk_common::bitcoin::hashes::hex::ToHex as _;
19
20use super::{History, LiquidChainService};
21
22pub(crate) struct ElectrumLiquidChainService {
23 config: Config,
24 client: OnceLock<RwLock<ElectrumClient>>,
25}
26
27impl ElectrumLiquidChainService {
28 pub(crate) fn new(config: Config) -> Self {
29 Self {
30 config,
31 client: OnceLock::new(),
32 }
33 }
34
35 fn get_client(&self) -> Result<&RwLock<ElectrumClient>> {
36 if let Some(c) = self.client.get() {
37 return Ok(c);
38 }
39
40 let client = match &self.config.liquid_explorer {
41 BlockchainExplorer::Electrum { url } => self.config.electrum_client(url)?,
42 _ => bail!("Cannot start Liquid Electrum chain service without an Electrum url"),
43 };
44 let client = self.client.get_or_init(|| RwLock::new(client));
45 Ok(client)
46 }
47}
48
49#[sdk_macros::async_trait]
50impl LiquidChainService for ElectrumLiquidChainService {
51 async fn tip(&self) -> Result<u32> {
52 Ok(self
53 .get_client()?
54 .write()
55 .await
56 .tip()
57 .map(|header| header.height)?)
58 }
59
60 async fn broadcast(&self, tx: &Transaction) -> Result<Txid> {
61 Ok(self.get_client()?.read().await.broadcast(tx)?)
62 }
63
64 async fn get_transaction_hex(&self, txid: &Txid) -> Result<Option<Transaction>> {
65 Ok(self.get_transactions(&[*txid]).await?.first().cloned())
66 }
67
68 async fn get_transactions(&self, txids: &[Txid]) -> Result<Vec<Transaction>> {
69 Ok(self.get_client()?.read().await.get_transactions(txids)?)
70 }
71
72 async fn get_script_history(&self, script: &Script) -> Result<Vec<History>> {
73 self.get_scripts_history(&[script.clone()])
74 .await?
75 .into_iter()
76 .nth(0)
77 .context("History not found")
78 }
79
80 async fn get_scripts_history(&self, scripts: &[Script]) -> Result<Vec<Vec<History>>> {
81 let scripts: Vec<&Script> = scripts.iter().collect();
82 Ok(self
83 .get_client()?
84 .read()
85 .await
86 .get_scripts_history(&scripts)?
87 .into_iter()
88 .map(|h| h.into_iter().map(Into::into).collect())
89 .collect())
90 }
91
92 async fn get_script_history_with_retry(
93 &self,
94 script: &Script,
95 retries: u64,
96 ) -> Result<Vec<History>> {
97 info!("Fetching script history for {script:x}");
98 let mut script_history = vec![];
99
100 let mut retry = 0;
101 while retry <= retries {
102 script_history = self.get_script_history(script).await?;
103 match script_history.is_empty() {
104 true => {
105 retry += 1;
106 info!("Script history for {script:x} is empty, retrying in 1 second... ({retry} of {retries})");
107 tokio::time::sleep(Duration::from_secs(1)).await;
109 }
110 false => break,
111 }
112 }
113 Ok(script_history)
114 }
115
116 async fn get_script_utxos(&self, script: &Script) -> Result<Vec<Utxo>> {
117 let history = self.get_script_history_with_retry(script, 10).await?;
118
119 let mut utxos: Vec<Utxo> = vec![];
120 for history_item in history {
121 match self.get_transaction_hex(&history_item.txid).await {
122 Ok(Some(tx)) => {
123 let mut new_utxos = tx
124 .output
125 .iter()
126 .enumerate()
127 .map(|(vout, output)| {
128 Utxo::Liquid(Box::new((
129 OutPoint::new(history_item.txid, vout as u32),
130 output.clone(),
131 )))
132 })
133 .collect();
134 utxos.append(&mut new_utxos);
135 }
136 _ => {
137 log::warn!("Could not retrieve transaction from history item");
138 continue;
139 }
140 }
141 }
142
143 Ok(utxos)
144 }
145
146 async fn verify_tx(
147 &self,
148 address: &Address,
149 tx_id: &str,
150 tx_hex: &str,
151 verify_confirmation: bool,
152 ) -> Result<Transaction> {
153 let script = Script::from_hex(
154 hex::encode(address.to_unconfidential().script_pubkey().as_bytes()).as_str(),
155 )
156 .map_err(|e| anyhow!("Failed to get script from address {e:?}"))?;
157
158 let script_history = self.get_script_history_with_retry(&script, 30).await?;
159 let lockup_tx_history = script_history.iter().find(|h| h.txid.to_hex().eq(tx_id));
160
161 match lockup_tx_history {
162 Some(history) => {
163 info!("Liquid transaction found, verifying transaction content...");
164 let tx: Transaction = utils::deserialize_tx_hex(tx_hex)?;
165 if !tx.txid().to_hex().eq(&history.txid.to_hex()) {
166 return Err(anyhow!(
167 "Liquid transaction id and hex do not match: {} vs {}",
168 tx_id,
169 tx.txid().to_hex()
170 ));
171 }
172
173 if verify_confirmation && history.height <= 0 {
174 return Err(anyhow!(
175 "Liquid transaction was not confirmed, txid={} waiting for confirmation",
176 tx_id,
177 ));
178 }
179 Ok(tx)
180 }
181 None => Err(anyhow!(
182 "Liquid transaction was not found, txid={} waiting for broadcast",
183 tx_id,
184 )),
185 }
186 }
187}