breez_sdk_liquid/chain/liquid/
electrum.rs

1#![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                    // Waiting 1s between retries, so we detect the new tx as soon as possible
108                    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}