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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `TxBuilder::set_exact_sequence` for fine-grained nSequence control
- `TxIn::sequence` getter for reading the nSequence value of transaction inputs

### Changed

- Expand Node and regtest integration coverage for wallet address lifecycle, output introspection, and fee calculation APIs ([#22](https://github.com/bitcoindevkit/bdk-wasm/issues/22))

## [0.3.0] - 2026-03-16

### Added
Expand Down
49 changes: 49 additions & 0 deletions tests/node/integration/esplora.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,26 @@ describe(`Esplora client (${network})`, () => {
expect(wallet.latest_checkpoint.height).toBeGreaterThan(0);
}, 30000);

it("lists scanned outputs and resolves known UTXOs", () => {
const utxos = wallet.list_unspent();

expect(utxos.length).toBeGreaterThan(0);

const firstUtxo = utxos[0];
const resolved = wallet.get_utxo(firstUtxo.outpoint);

expect(resolved).toBeDefined();
expect(resolved!.outpoint.toString()).toBe(firstUtxo.outpoint.toString());
expect(resolved!.derivation_index).toBe(firstUtxo.derivation_index);
expect(resolved!.txout.value.to_sat()).toBe(firstUtxo.txout.value.to_sat());
expect(wallet.is_mine(firstUtxo.txout.script_pubkey)).toBe(true);
expect(
wallet
.list_output()
.some((output) => output.outpoint.toString() === firstUtxo.outpoint.toString())
).toBe(true);
});

it("fetches fee estimates", async () => {
const confirmationTarget = 2;
const feeEstimates = await esploraClient.get_fee_estimates();
Expand Down Expand Up @@ -232,6 +252,35 @@ describe(`Esplora client (${network})`, () => {
);
});

it("calculates fee and fee rate for signed wallet transactions", () => {
const recipientAddress = wallet.peek_address("external", 15);
const sendAmount = Amount.from_sat(BigInt(800));

const psbt = wallet
.build_tx()
.fee_rate(new FeeRate(BigInt(1)))
.add_recipient(
new Recipient(recipientAddress.address.script_pubkey, sendAmount)
)
.finish();

const expectedFee = psbt.fee().to_sat();

expect(wallet.sign(psbt, new SignOptions())).toBe(true);

const expectedFeeRate = psbt.fee_rate();
expect(expectedFeeRate).toBeDefined();

const tx = psbt.extract_tx();
const calculatedFee = wallet.calculate_fee(tx);
const calculatedFeeRate = wallet.calculate_fee_rate(tx);

expect(calculatedFee.to_sat()).toBe(expectedFee);
expect(calculatedFeeRate.to_sat_per_vb_ceil()).toBe(
expectedFeeRate!.to_sat_per_vb_ceil()
);
});

describe("TxBuilder advanced options (funded wallet)", () => {
// Note: FeeRate is consumed by wasm-bindgen when passed to a builder method,
// so we create fresh instances for each test instead of using the shared feeRate.
Expand Down
45 changes: 40 additions & 5 deletions tests/node/integration/wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,50 @@ describe("Wallet", () => {

it("marks and unmarks addresses as used", () => {
const freshWallet = Wallet.create(network, externalDesc, internalDesc);
freshWallet.reveal_addresses_to("external", 2);

expect(
freshWallet.list_unused_addresses("external").map((address) => address.index)
).toEqual([0, 1, 2]);

// mark_used returns whether the index was present in unused set
const marked = freshWallet.mark_used("external", 0);
// The first address should have been in the unused set
expect(typeof marked).toBe("boolean");
const marked = freshWallet.mark_used("external", 1);
expect(marked).toBe(true);
expect(
freshWallet.list_unused_addresses("external").map((address) => address.index)
).toEqual([0, 2]);

// unmark_used returns whether the index was inserted back
const unmarked = freshWallet.unmark_used("external", 0);
expect(typeof unmarked).toBe("boolean");
const unmarked = freshWallet.unmark_used("external", 1);
expect(unmarked).toBe(true);
expect(
freshWallet.list_unused_addresses("external").map((address) => address.index)
).toEqual([0, 1, 2]);
});

it("reveals address ranges and advances the next derivation index", () => {
const freshWallet = Wallet.create(network, externalDesc, internalDesc);

const revealed = freshWallet.reveal_addresses_to("external", 2);

expect(revealed.map((address) => address.index)).toEqual([0, 1, 2]);
expect(revealed.map((address) => address.keychain)).toEqual([
"external",
"external",
"external",
]);
expect(freshWallet.next_derivation_index("external")).toBe(3);
expect(
freshWallet.list_unused_addresses("external").map((address) => address.index)
).toEqual([0, 1, 2]);
});

it("detects wallet-owned scripts", () => {
const freshWallet = Wallet.create(network, externalDesc, internalDesc);
const ownAddress = freshWallet.reveal_next_address("external");

expect(freshWallet.is_mine(ownAddress.address.script_pubkey)).toBe(true);
expect(freshWallet.is_mine(recipientAddress.script_pubkey)).toBe(false);
});

describe("TxBuilder options", () => {
Expand Down
Loading