Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `Wallet::descriptor_checksum` for getting the descriptor checksum string for a keychain
- `Wallet::next_derivation_index` for getting the next unused derivation index for a keychain
- `TxDetails` type with getters for `txid`, `sent`, `received`, `fee`, `fee_rate`, `balance_delta_sat`, `chain_position`, and `tx`
- Block-by-block processing with events ([#20](https://github.com/bitcoindevkit/bdk-wasm/issues/20)):
- `Wallet::apply_block_events` for applying a block and connecting via `prev_blockhash`, returns `WalletEvent`s
- `Wallet::apply_block_connected_to_events` for applying a block with explicit chain connection point, returns `WalletEvent`s
- `Wallet::apply_evicted_txs` for marking unconfirmed transactions as evicted from the mempool
- `Wallet::checkpoints` for listing all checkpoints in the wallet's internal chain
- `Block` type with `from_bytes()` deserialization and accessors (`block_hash`, `prev_blockhash`, `time`, `txdata`, `tx_count`)
- `BlockId` constructor from height and hash string
- `EvictedTx` type for pairing a transaction ID with an eviction timestamp
- `BdkErrorCode::CannotConnect` and `BdkErrorCode::UnexpectedConnectedToHash` error codes for block application errors

## [0.3.0] - 2026-03-16

Expand Down
84 changes: 81 additions & 3 deletions src/bitcoin/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@ use crate::{
bitcoin::WalletTx,
result::JsResult,
types::{
AddressInfo, Amount, Balance, ChangeSet, CheckPoint, FeeRate, FullScanRequest, KeychainKind, LocalOutput,
Network, NetworkKind, OutPoint, Psbt, ScriptBuf, SentAndReceived, SpkIndexed, SyncRequest, Transaction,
TxDetails, TxOut, Txid, Update, WalletEvent,
AddressInfo, Amount, Balance, Block, ChangeSet, CheckPoint, EvictedTx, FeeRate, FullScanRequest, KeychainKind,
LocalOutput, Network, NetworkKind, OutPoint, Psbt, ScriptBuf, SentAndReceived, SpkIndexed, SyncRequest,
Transaction, TxDetails, TxOut, Txid, Update, WalletEvent,
},
};

use super::{TxBuilder, UnconfirmedTx};

use crate::types::{BdkError, BdkErrorCode, BlockId};
use bdk_wallet::chain::local_chain::{ApplyHeaderError, CannotConnectError};

// We wrap a `BdkWallet` in `Rc<RefCell<...>>` because `wasm_bindgen` do not
// support Rust's lifetimes. This allows us to forward a reference to the
// internal wallet when using `build_tx` and to enforce the lifetime at runtime
Expand Down Expand Up @@ -313,6 +316,65 @@ impl Wallet {
.borrow_mut()
.apply_unconfirmed_txs(unconfirmed_txs.into_iter().map(Into::into))
}

/// Apply a block to the wallet, connecting it via its `prev_blockhash`.
///
/// This is a convenience method that introduces a `Block` at the given `height`
/// and connects it to the chain using the block's `prev_blockhash` header field.
///
/// Returns a list of `WalletEvent`s describing what changed (new transactions,
/// confirmations, etc.).
pub fn apply_block_events(&self, block: &Block, height: u32) -> Result<Vec<WalletEvent>, BdkError> {
let events = self
.0
.borrow_mut()
.apply_block_events(block, height)
.map_err(BdkError::from)?;
Ok(events.into_iter().map(WalletEvent::from).collect())
}

/// Apply a block to the wallet, explicitly specifying the connection point.
///
/// The `connected_to` parameter tells the wallet how this block connects to the
/// internal chain. Use this when you have explicit knowledge of the chain topology,
/// e.g. when processing blocks out of order or from a specific fork.
///
/// Returns a list of `WalletEvent`s describing what changed.
pub fn apply_block_connected_to_events(
&self,
block: &Block,
height: u32,
connected_to: BlockId,
) -> Result<Vec<WalletEvent>, BdkError> {
let events = self
.0
.borrow_mut()
.apply_block_connected_to_events(block, height, connected_to.into())
.map_err(BdkError::from)?;
Ok(events.into_iter().map(WalletEvent::from).collect())
}

/// Mark unconfirmed transactions as evicted from the mempool.
///
/// Evicted transactions are no longer considered canonical and won't appear in
/// `transactions()`. This is useful when a blockchain backend reports that certain
/// transactions have been dropped (e.g. due to low fees or conflicts).
///
/// Only unconfirmed, canonical transactions are affected. An evicted transaction
/// can become canonical again if it is later observed on-chain or in the mempool
/// with higher priority.
pub fn apply_evicted_txs(&self, evicted_txs: Vec<EvictedTx>) {
self.0
.borrow_mut()
.apply_evicted_txs(evicted_txs.into_iter().map(|e| (e.txid, e.evicted_at)));
}

/// List all checkpoints in the wallet's internal chain, ordered by height.
///
/// Returns an array of `CheckPoint`s representing the wallet's view of the blockchain.
pub fn checkpoints(&self) -> Vec<CheckPoint> {
self.0.borrow().checkpoints().map(Into::into).collect()
}
}

/// Options for signing a PSBT.
Expand Down Expand Up @@ -406,3 +468,19 @@ impl Default for SignOptions {
Self::new()
}
}

impl From<CannotConnectError> for BdkError {
fn from(e: CannotConnectError) -> Self {
BdkError::new(BdkErrorCode::CannotConnect, e.to_string(), ())
}
}

impl From<ApplyHeaderError> for BdkError {
fn from(e: ApplyHeaderError) -> Self {
use ApplyHeaderError::*;
match &e {
InconsistentBlocks => BdkError::new(BdkErrorCode::UnexpectedConnectedToHash, e.to_string(), ()),
CannotConnect(inner) => BdkError::new(BdkErrorCode::CannotConnect, inner.to_string(), ()),
}
}
}
124 changes: 121 additions & 3 deletions src/types/block.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
use std::ops::Deref;
use std::{ops::Deref, str::FromStr};

use bdk_wallet::chain::{BlockId as BdkBlockId, ConfirmationBlockTime as BdkConfirmationBlockTime};
use wasm_bindgen::prelude::wasm_bindgen;
use bdk_wallet::{
bitcoin::{consensus::deserialize, Block as BdkBlock, BlockHash as BdkBlockHash},
chain::{BlockId as BdkBlockId, ConfirmationBlockTime as BdkConfirmationBlockTime},
};
use wasm_bindgen::{prelude::wasm_bindgen, JsError};

use crate::result::JsResult;

use super::Transaction;

/// A reference to a block in the canonical chain.
#[wasm_bindgen]
Expand All @@ -10,6 +17,16 @@ pub struct BlockId(BdkBlockId);

#[wasm_bindgen]
impl BlockId {
/// Create a new `BlockId` from a height and block hash string.
#[wasm_bindgen(constructor)]
pub fn new(height: u32, hash: &str) -> JsResult<BlockId> {
let block_hash = BdkBlockHash::from_str(hash).map_err(|e| JsError::new(&format!("Invalid block hash: {e}")))?;
Ok(BlockId(BdkBlockId {
height,
hash: block_hash,
}))
}

/// The height of the block.
#[wasm_bindgen(getter)]
pub fn height(&self) -> u32 {
Expand All @@ -29,6 +46,107 @@ impl From<BdkBlockId> for BlockId {
}
}

impl From<BlockId> for BdkBlockId {
fn from(block_id: BlockId) -> Self {
block_id.0
}
}

/// A full Bitcoin block (header + transactions).
///
/// Construct from consensus-encoded bytes using `Block.from_bytes()`.
#[wasm_bindgen]
#[derive(Clone)]
pub struct Block(BdkBlock);

impl Deref for Block {
type Target = BdkBlock;

fn deref(&self) -> &Self::Target {
&self.0
}
}

#[wasm_bindgen]
impl Block {
/// Deserialize a block from consensus-encoded bytes.
///
/// Accepts a `Uint8Array` of raw block bytes as produced by Bitcoin Core
/// or any standard Bitcoin serializer.
pub fn from_bytes(bytes: &[u8]) -> JsResult<Block> {
let block: BdkBlock =
deserialize(bytes).map_err(|e| JsError::new(&format!("Failed to deserialize block: {e}")))?;
Ok(Block(block))
}

/// Returns the block hash.
#[wasm_bindgen(getter)]
pub fn block_hash(&self) -> String {
self.0.block_hash().to_string()
}

/// Returns the previous block hash from the header.
#[wasm_bindgen(getter)]
pub fn prev_blockhash(&self) -> String {
self.0.header.prev_blockhash.to_string()
}

/// Returns the block header timestamp.
#[wasm_bindgen(getter)]
pub fn time(&self) -> u32 {
self.0.header.time
}

/// Returns the list of transactions in the block.
#[wasm_bindgen(getter)]
pub fn txdata(&self) -> Vec<Transaction> {
self.0.txdata.clone().into_iter().map(Into::into).collect()
}

/// Returns the number of transactions in the block.
#[wasm_bindgen(getter)]
pub fn tx_count(&self) -> usize {
self.0.txdata.len()
}
}

impl From<BdkBlock> for Block {
fn from(inner: BdkBlock) -> Self {
Block(inner)
}
}

impl From<&Block> for BdkBlock {
fn from(block: &Block) -> Self {
block.0.clone()
}
}

/// A transaction ID paired with an eviction timestamp.
///
/// Used with `Wallet::apply_evicted_txs` to mark unconfirmed transactions
/// as evicted from the mempool.
#[wasm_bindgen]
pub struct EvictedTx {
pub(crate) txid: bdk_wallet::bitcoin::Txid,
pub(crate) evicted_at: u64,
}

#[wasm_bindgen]
impl EvictedTx {
/// Create a new `EvictedTx`.
///
/// - `txid`: The transaction ID to evict
/// - `evicted_at`: Unix timestamp of when the transaction was evicted
#[wasm_bindgen(constructor)]
pub fn new(txid: super::Txid, evicted_at: u64) -> EvictedTx {
EvictedTx {
txid: txid.into(),
evicted_at,
}
}
}

/// Represents the observed position of some chain data.
#[wasm_bindgen]
pub struct ConfirmationBlockTime(BdkConfirmationBlockTime);
Expand Down
7 changes: 7 additions & 0 deletions src/types/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ pub enum BdkErrorCode {
/// Invalid character in input.
InvalidCharacter,

/// ------- Block application errors -------

/// Cannot connect block to the existing chain
CannotConnect,
/// Connected-to hash does not match the expected hash
UnexpectedConnectedToHash,

/// ------- Other errors -------
/// Unexpected error, should never happen
Unexpected,
Expand Down
Loading
Loading