Skip to content

Expose TxBuilder::policy_path to disambiguate multipath descriptors with or_i branches #54

@scopo911dev

Description

@scopo911dev

Hi — first, thanks for bdk-wasm. We've shipped a fair chunk of a browser-first Bitcoin multisig vault on top of 0.3.0 and it's been excellent, receive-address derivation, full scans via EsploraClient, balance reads, all working cleanly end-to-end.

We've hit a wall building the Send flow and wanted to surface it for any guidance and/or as a feature request.

Use case

Our custody descriptor follows the BIP-389 multipath form Marko Bencun (BitBox lead) walked us through earlier this year for BIP-388 compliance:

wsh(or_i(
  and_v(v:pk(@0/<0;1>/*),pk(@1/<0;1>/*)),
  or_i(
    and_v(v:pk(@0/<2;3>/*),and_v(v:pk(@2/<2;3>/*),older(1008))),
    and_v(v:pk(@1/<4;5>/*),and_v(v:pk(@2/<4;5>/*),older(52560)))
  )
))

Three spending paths under one wsh policy:

  • Path 1: Owner + Cosigner, anytime.
  • Path 2: Owner + Inheritance, after a short timelock.
  • Path 3: Cosigner + Inheritance, after a long timelock.

Receive-address derivation against this descriptor (via Wallet::create_from_two_path_descriptor + peek_address) works perfectly under bdk-wasm 0.3.0. BitBox02 verifies the addresses byte-for-byte under the same BIP-388 policy registered on-device. So far so good.

The wall

Calling Wallet::build_tx().add_recipient(...).fee_rate(...).finish() against a UTXO at this descriptor throws:

SpendingPolicyRequired { keychain: External }

That's BDK correctly saying "I have three valid or_i branches to satisfy and you need to tell me which one." In the Rust API the fix is:

let mut tx_builder = wallet.build_tx();
let policy_path: BTreeMap<String, Vec<usize>> = /* ... */;
tx_builder.policy_path(policy_path, KeychainKind::External);

TxBuilder::policy_path exists in BDK 1.0 — but it's not exposed via wasm_bindgen. I cross-referenced the methods bound through to JS in src/bitcoin/tx_builder.rs on main and confirmed policy_path isn't in the list. Adjacent methods like change_policy and add_utxo are bound, so the absence looks like a coverage gap rather than a deliberate exclusion.

Why we can't work around it from JS

  • A wallet built from just the Path 1 sub-descriptor (wsh(and_v(v:pk(...),pk(...)))) produces a different scriptPubKey hash, so it can't recognise UTXOs at the original multipath address.
  • Pre-selecting UTXOs via add_utxo doesn't disambiguate the witness-script satisfaction — BDK still throws SpendingPolicyRequired.
  • Manually constructing the PSBT outside BDK works but means reimplementing UTXO selection, change derivation, fee math, and witness-script reconstruction. Substantial code surface to maintain.

Ask

Bind TxBuilder::policy_path through to wasm with a JS-friendly signature — likely:

policy_path(policy_path: Record<string, Uint32Array>, keychain: KeychainKind): TxBuilder

(The Rust signature is BTreeMap<String, Vec<usize>>; a JS Record<string, number[]> or Record<string, Uint32Array> should marshal cleanly through serde-wasm-bindgen.)

Also helpful, but secondary: expose Wallet::policies(keychain) so callers can introspect the policy tree to build the policy_path map without hardcoding policy IDs.

Thanks for any assistance! SP

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions