Skip to content

Commit 4a6861e

Browse files
committed
Add support for electrum TxOutSet queries
1 parent 469b22a commit 4a6861e

File tree

4 files changed

+322
-114
lines changed

4 files changed

+322
-114
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ repository = "https://github.com/bitcoindevkit/bdk-reserves"
1212
[dependencies]
1313
bdk = { version = "0.28", default-features = false }
1414
bitcoinconsensus = "0.19.0-3"
15+
electrum-client = { version = "0.12", optional = true }
1516
esplora-client = { version = "0.4", default-features = false, optional = true }
1617
log = "^0.4"
1718

1819
[features]
20+
electrum = ["electrum-client", "bdk/electrum"]
1921
use-esplora-blocking = ["esplora-client/blocking", "bdk/use-esplora-blocking"]
2022

2123
[dev-dependencies]

src/txout_set.rs

Lines changed: 105 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ use bdk::bitcoin::{OutPoint, Transaction, TxOut, Txid};
22
use bdk::database::BatchDatabase;
33
use bdk::wallet::Wallet;
44

5+
#[cfg(feature = "electrum")]
6+
use electrum_client::{Client as ElectrumClient, ElectrumApi};
7+
58
#[cfg(feature = "use-esplora-blocking")]
6-
use esplora_client::BlockingClient;
9+
use esplora_client::BlockingClient as EsploraClient;
710

811
use std::collections::BTreeMap;
912
use std::collections::BTreeSet;
@@ -196,15 +199,111 @@ where
196199
}
197200
}
198201

202+
#[cfg(feature = "electrum")]
203+
pub struct ElectrumAtHeight<'a> {
204+
client: &'a ElectrumClient,
205+
maximum_txout_height: Option<u32>,
206+
}
207+
208+
#[cfg(feature = "electrum")]
209+
impl TxOutSet for ElectrumClient {
210+
type Error = electrum_client::Error;
211+
212+
fn get_prevouts<'a, I, T>(&self, outpoints: I) -> Result<T, Self::Error>
213+
where
214+
I: IntoIterator<Item = &'a OutPoint>,
215+
T: FromIterator<Option<TxOut>> {
216+
let electrum_at_height = ElectrumAtHeight {
217+
client: self,
218+
maximum_txout_height: None,
219+
};
220+
221+
electrum_at_height.get_prevouts(outpoints)
222+
}
223+
}
224+
225+
#[cfg(feature = "electrum")]
226+
impl<'a> MaxHeightTxOutQuery<'a> for ElectrumClient {
227+
type Target = ElectrumAtHeight<'a>;
228+
229+
fn txout_set_confirmed_by_height(&'a self, height: u32) -> Self::Target {
230+
ElectrumAtHeight {
231+
client: self,
232+
maximum_txout_height: Some(height),
233+
}
234+
}
235+
}
236+
237+
#[cfg(feature = "electrum")]
238+
impl<'a> TxOutSet for ElectrumAtHeight<'a> {
239+
type Error = electrum_client::Error;
240+
241+
fn get_prevouts<'b, I, T>(&self, outpoints: I) -> Result<T, Self::Error>
242+
where
243+
I: IntoIterator<Item = &'b OutPoint>,
244+
T: FromIterator<Option<TxOut>> {
245+
let outpoints: Vec<_> = outpoints.into_iter().collect();
246+
247+
let input_txids: BTreeSet<Txid> = outpoints.iter().map(|outpoint| outpoint.txid).collect();
248+
249+
// avoiding the obvious batch_transaction_get optimization because
250+
// I'm not sure how it handles cases where some transactions are present but not others
251+
// FIXME: Probably should retain some types of errors here
252+
// and report them later
253+
let transactions: BTreeMap<&Txid, Transaction> = input_txids
254+
.iter()
255+
.filter_map(|txid| {
256+
self.client.transaction_get(txid)
257+
.map(|tx| Some((txid, tx)))
258+
.unwrap_or(None)
259+
})
260+
.collect();
261+
262+
let iter = outpoints.iter()
263+
.map(|outpoint| {
264+
let previous_tx = match transactions.get(&outpoint.txid) {
265+
Some(previous_tx) => previous_tx,
266+
None => {
267+
return Ok(None);
268+
},
269+
};
270+
271+
let output = match previous_tx.output.get(outpoint.vout as usize) {
272+
Some(output) => output,
273+
None => {
274+
return Ok(None);
275+
},
276+
};
277+
278+
let unspent = self.client.script_list_unspent(&output.script_pubkey)?;
279+
280+
let output_in_unspent_list = unspent
281+
.iter()
282+
.find(|unspent_info|
283+
unspent_info.tx_hash == outpoint.txid &&
284+
unspent_info.tx_pos == outpoint.vout as usize &&
285+
unspent_info.height <= (self.maximum_txout_height.unwrap_or(u32::MAX) as usize)
286+
);
287+
288+
match output_in_unspent_list {
289+
Some(_) => Ok(Some(output.to_owned())),
290+
None => Ok(None),
291+
}
292+
});
293+
294+
Result::<T, Self::Error>::from_iter(iter)
295+
}
296+
}
297+
199298
#[cfg(feature = "use-esplora-blocking")]
200299
pub struct EsploraAtHeight<'a> {
201-
client: &'a BlockingClient,
300+
client: &'a EsploraClient,
202301
height: Option<u32>,
203302
}
204303

205304
#[cfg(feature = "use-esplora-blocking")]
206305
impl<'a> EsploraAtHeight<'a> {
207-
pub fn new(client: &'a BlockingClient, height: Option<u32>) -> Self {
306+
pub fn new(client: &'a EsploraClient, height: Option<u32>) -> Self {
208307
Self { client, height }
209308
}
210309
}
@@ -220,7 +319,7 @@ impl<'a> TxOutSet for EsploraAtHeight<'a> {
220319
{
221320
let outpoints: Vec<_> = outpoints.into_iter().collect();
222321

223-
// Remove duplicate txids since the
322+
// Remove duplicate txids
224323
let input_txids: BTreeSet<Txid> = outpoints.iter().map(|outpoint| outpoint.txid).collect();
225324

226325
let transactions: BTreeMap<&Txid, Transaction> = input_txids
@@ -306,7 +405,7 @@ impl<'a> TxOutSet for EsploraAtHeight<'a> {
306405
}
307406

308407
#[cfg(feature = "use-esplora-blocking")]
309-
impl<'a> HistoricalTxOutQuery<'a> for BlockingClient {
408+
impl<'a> HistoricalTxOutQuery<'a> for EsploraClient {
310409
type Target = EsploraAtHeight<'a>;
311410

312411
fn txout_set_at_height(&'a self, height: u32) -> Self::Target {
@@ -318,7 +417,7 @@ impl<'a> HistoricalTxOutQuery<'a> for BlockingClient {
318417
}
319418

320419
#[cfg(feature = "use-esplora-blocking")]
321-
impl<'a> TxOutSet for BlockingClient {
420+
impl<'a> TxOutSet for EsploraClient {
322421
type Error = esplora_client::Error;
323422

324423
fn get_prevouts<'b, I, T>(&self, outpoints: I) -> Result<T, Self::Error>

tests/point_in_time.rs

Lines changed: 0 additions & 108 deletions
This file was deleted.

0 commit comments

Comments
 (0)