Skip to content

Commit 9467cad

Browse files
feat(wallet): introduce block-by-block api
* methods `process_block` and `process_unconfirmed_txs` are added * amend stage method docs Co-authored-by: Vladimir Fomene <[email protected]> Co-authored-by: 志宇 <[email protected]>
1 parent d3e5095 commit 9467cad

File tree

1 file changed

+135
-4
lines changed

1 file changed

+135
-4
lines changed

crates/bdk/src/wallet/mod.rs

Lines changed: 135 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,18 @@ pub use bdk_chain::keychain::Balance;
2323
use bdk_chain::{
2424
indexed_tx_graph,
2525
keychain::{self, KeychainTxOutIndex},
26-
local_chain::{self, CannotConnectError, CheckPoint, CheckPointIter, LocalChain},
26+
local_chain::{
27+
self, ApplyHeaderError, CannotConnectError, CheckPoint, CheckPointIter, LocalChain,
28+
},
2729
tx_graph::{CanonicalTx, TxGraph},
2830
Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeHeightAnchor, FullTxOut,
2931
IndexedTxGraph, Persist, PersistBackend,
3032
};
3133
use bitcoin::secp256k1::{All, Secp256k1};
3234
use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
3335
use bitcoin::{
34-
absolute, Address, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxOut, Txid,
35-
Weight, Witness,
36+
absolute, Address, Block, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxOut,
37+
Txid, Weight, Witness,
3638
};
3739
use bitcoin::{consensus::encode::serialize, BlockHash};
3840
use bitcoin::{constants::genesis_block, psbt};
@@ -428,6 +430,55 @@ pub enum InsertTxError {
428430
},
429431
}
430432

433+
impl fmt::Display for InsertTxError {
434+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
435+
match self {
436+
InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
437+
tip_height,
438+
tx_height,
439+
} => {
440+
write!(f, "cannot insert tx with confirmation height ({}) higher than internal tip height ({})", tx_height, tip_height)
441+
}
442+
}
443+
}
444+
}
445+
446+
#[cfg(feature = "std")]
447+
impl std::error::Error for InsertTxError {}
448+
449+
/// An error that may occur when applying a block to [`Wallet`].
450+
#[derive(Debug)]
451+
pub enum ApplyBlockError {
452+
/// Occurs when the update chain cannot connect with original chain.
453+
CannotConnect(CannotConnectError),
454+
/// Occurs when the `connected_to` hash does not match the hash derived from `block`.
455+
UnexpectedConnectedToHash {
456+
/// Block hash of `connected_to`.
457+
connected_to_hash: BlockHash,
458+
/// Expected block hash of `connected_to`, as derived from `block`.
459+
expected_hash: BlockHash,
460+
},
461+
}
462+
463+
impl fmt::Display for ApplyBlockError {
464+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
465+
match self {
466+
ApplyBlockError::CannotConnect(err) => err.fmt(f),
467+
ApplyBlockError::UnexpectedConnectedToHash {
468+
expected_hash: block_hash,
469+
connected_to_hash: checkpoint_hash,
470+
} => write!(
471+
f,
472+
"`connected_to` hash {} differs from the expected hash {} (which is derived from `block`)",
473+
checkpoint_hash, block_hash
474+
),
475+
}
476+
}
477+
}
478+
479+
#[cfg(feature = "std")]
480+
impl std::error::Error for ApplyBlockError {}
481+
431482
impl<D> Wallet<D> {
432483
/// Initialize an empty [`Wallet`].
433484
pub fn new<E: IntoWalletDescriptor>(
@@ -2302,7 +2353,7 @@ impl<D> Wallet<D> {
23022353
self.persist.commit().map(|c| c.is_some())
23032354
}
23042355

2305-
/// Returns the changes that will be staged with the next call to [`commit`].
2356+
/// Returns the changes that will be committed with the next call to [`commit`].
23062357
///
23072358
/// [`commit`]: Self::commit
23082359
pub fn staged(&self) -> &ChangeSet
@@ -2326,6 +2377,86 @@ impl<D> Wallet<D> {
23262377
pub fn local_chain(&self) -> &LocalChain {
23272378
&self.chain
23282379
}
2380+
2381+
/// Introduces a `block` of `height` to the wallet, and tries to connect it to the
2382+
/// `prev_blockhash` of the block's header.
2383+
///
2384+
/// This is a convenience method that is equivalent to calling [`apply_block_connected_to`]
2385+
/// with `prev_blockhash` and `height-1` as the `connected_to` parameter.
2386+
///
2387+
/// [`apply_block_connected_to`]: Self::apply_block_connected_to
2388+
pub fn apply_block(&mut self, block: Block, height: u32) -> Result<(), CannotConnectError>
2389+
where
2390+
D: PersistBackend<ChangeSet>,
2391+
{
2392+
let connected_to = match height.checked_sub(1) {
2393+
Some(prev_height) => BlockId {
2394+
height: prev_height,
2395+
hash: block.header.prev_blockhash,
2396+
},
2397+
None => BlockId {
2398+
height,
2399+
hash: block.block_hash(),
2400+
},
2401+
};
2402+
self.apply_block_connected_to(block, height, connected_to)
2403+
.map_err(|err| match err {
2404+
ApplyHeaderError::InconsistentBlocks => {
2405+
unreachable!("connected_to is derived from the block so must be consistent")
2406+
}
2407+
ApplyHeaderError::CannotConnect(err) => err,
2408+
})
2409+
}
2410+
2411+
/// Applies relevant transactions from `block` of `height` to the wallet, and connects the
2412+
/// block to the internal chain.
2413+
///
2414+
/// The `connected_to` parameter informs the wallet how this block connects to the internal
2415+
/// [`LocalChain`]. Relevant transactions are filtered from the `block` and inserted into the
2416+
/// internal [`TxGraph`].
2417+
pub fn apply_block_connected_to(
2418+
&mut self,
2419+
block: Block,
2420+
height: u32,
2421+
connected_to: BlockId,
2422+
) -> Result<(), ApplyHeaderError>
2423+
where
2424+
D: PersistBackend<ChangeSet>,
2425+
{
2426+
let mut changeset = ChangeSet::default();
2427+
changeset.append(
2428+
self.chain
2429+
.apply_header_connected_to(&block.header, height, connected_to)?
2430+
.into(),
2431+
);
2432+
changeset.append(
2433+
self.indexed_graph
2434+
.apply_block_relevant(block, height)
2435+
.into(),
2436+
);
2437+
self.persist.stage(changeset);
2438+
Ok(())
2439+
}
2440+
2441+
/// Apply relevant unconfirmed transactions to the wallet.
2442+
///
2443+
/// Transactions that are not relevant are filtered out.
2444+
///
2445+
/// This method takes in an iterator of `(tx, last_seen)` where `last_seen` is the timestamp of
2446+
/// when the transaction was last seen in the mempool. This is used for conflict resolution
2447+
/// when there is conflicting unconfirmed transactions. The transaction with the later
2448+
/// `last_seen` is prioritied.
2449+
pub fn apply_unconfirmed_txs<'t>(
2450+
&mut self,
2451+
unconfirmed_txs: impl IntoIterator<Item = (&'t Transaction, u64)>,
2452+
) where
2453+
D: PersistBackend<ChangeSet>,
2454+
{
2455+
let indexed_graph_changeset = self
2456+
.indexed_graph
2457+
.batch_insert_relevant_unconfirmed(unconfirmed_txs);
2458+
self.persist.stage(ChangeSet::from(indexed_graph_changeset));
2459+
}
23292460
}
23302461

23312462
impl<D> AsRef<bdk_chain::tx_graph::TxGraph<ConfirmationTimeHeightAnchor>> for Wallet<D> {

0 commit comments

Comments
 (0)