From 8ce99b09610017108a7d069e26fe4e324d416119 Mon Sep 17 00:00:00 2001 From: Toshi Date: Thu, 9 Apr 2026 10:10:45 +0000 Subject: [PATCH] test(wallet): cover address lifecycle and fee introspection --- CHANGELOG.md | 4 +++ tests/node/integration/esplora.test.ts | 49 ++++++++++++++++++++++++++ tests/node/integration/wallet.test.ts | 45 ++++++++++++++++++++--- 3 files changed, 93 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43f5a56..24d9d14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/tests/node/integration/esplora.test.ts b/tests/node/integration/esplora.test.ts index bf46f66..81609c9 100644 --- a/tests/node/integration/esplora.test.ts +++ b/tests/node/integration/esplora.test.ts @@ -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(); @@ -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. diff --git a/tests/node/integration/wallet.test.ts b/tests/node/integration/wallet.test.ts index dc276f5..2b20e93 100644 --- a/tests/node/integration/wallet.test.ts +++ b/tests/node/integration/wallet.test.ts @@ -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", () => {