Skip to content

fix(bitcoin): support sending to taproot (bc1p) addresses - #811

Open
KTibow wants to merge 1 commit into
enkryptcom:developfrom
KTibow:fix/taproot-address-support
Open

fix(bitcoin): support sending to taproot (bc1p) addresses#811
KTibow wants to merge 1 commit into
enkryptcom:developfrom
KTibow:fix/taproot-address-support

Conversation

@KTibow

@KTibow KTibow commented Jul 24, 2026

Copy link
Copy Markdown

Problem

Taproot addresses are rejected as invalid in the Bitcoin send screen. Pasting any bc1p... address turns the input red and leaves the Send button disabled, and there is no setting that changes it.

Root cause

isAddress in providers/bitcoin/libs/utils.ts validates by round-tripping through bitcoinjs-lib:

const isAddress = (address: string, network: BitcoinNetworkInfo): boolean => {
  try {
    BTCAddress.toOutputScript(address, network);
    return true;
  } catch {
    return false;
  }
};

bitcoinjs-lib v6 does not bundle an elliptic curve implementation. It requires initEccLib() before it can build a P2TR output script, and nothing in the repo ever calls it:

$ grep -rn "initEccLib" packages/
$

So toOutputScript throws, the catch swallows the error, and the address is reported invalid. Against bitcoinjs-lib@6.1.7, the version in the lockfile:

taproot   -> No ECC Library provided. You must call initEccLib() with a valid TinySecp256k1Interface instance
p2wpkh    -> OK  0014751e76e8199196d454941c45d1b3a323f1433bd6
p2sh      -> OK  a914b472a266d0bd89c13706a4132ccfb16f7c3b9fcb87
p2pkh     -> OK  76a91477bff20c60e522dfaa3350c39b030a5d004e839a88ac

This is a missing initialisation rather than a validation bug: the version of bitcoinjs-lib in use supports taproot fine.

Fix

Register an ECC implementation once, from a small side-effect module imported by the address validation and PSBT paths.

On the dependency: @bitcoinerlab/secp256k1@^1.2.0 is already resolved in yarn.lock at exactly that version, pulled in by ledger-bitcoin via @enkryptcom/hw-wallets:

$ yarn why @bitcoinerlab/secp256k1
└─ ledger-bitcoin@npm:0.3.0
   └─ @bitcoinerlab/secp256k1@npm:1.2.0 (via npm:^1.2.0)

Adding it as a direct dependency of the extension is a one line change to yarn.lock and adds no package to the bundle. It is pure JavaScript with a single dependency, @noble/curves ^1.7.0, which the lockfile already resolves. I avoided tiny-secp256k1 because v2 ships a secp256k1.wasm binary and browser wasm loaders, which is a real integration cost in the Vite build for identical functionality here.

Fee sizing. Registering the curve alone is not enough to make a taproot send succeed. calculateSizeBasedOnType sized every output as the account's own payment type, so a P2TR output was charged as 31 vB when it is 43 vB. Fee rates come back from BTCFeeHandler as Math.ceil(sat_per_vbyte) with an effective floor of 1 sat/vB, so on a 1-in/2-out transaction that 12 vB shortfall drops the real rate to roughly 0.92 sat/vB — under the minimum relay fee, and the broadcast fails. Outputs are now sized from their address type where it is known.

Only bech32/bech32m outputs are classified, since those are the ones whose size differs enough from the account's own type to matter. Anything else keeps the previous behaviour and is at most 3 vB out.

The change output and a partially typed recipient have no entry and are sized as before, so nothing changes for existing send flows. send-transaction/index.vue now recomputes the fee when addressTo changes, which it previously did not do at all.

This PR deliberately does not add P2TR to PaymentType or derive taproot accounts. It only makes taproot valid as a recipient.

Testing

  • yarn test in packages/extension: 18 passing, up from 11. New cases in providers/bitcoin/tests/bitcoin.address.validation.test.ts cover taproot, the other standard address types, malformed and wrong-network addresses, output classification, and the 12 vB size delta.
  • Reverting just the initEccLib import fails the new taproot case with expected false to equal true, so it is a genuine regression test.
  • vue-tsc --build --force produces exactly the same 309 pre-existing errors as develop; none added.
  • eslint and prettier --check clean on the touched files.

On the test environment. The new test file carries // @vitest-environment node. bitcoinjs-lib verifies an ECC library by feeding it Buffer.from(...) vectors and @noble/curves rejects anything that is not a Uint8Array; under the default jsdom environment those two come from different realms, so instanceof fails and initEccLib throws even though the library is fine.

To confirm that is only a harness artifact and not something users would hit, I built a bundle with the production nodePolyfills config from vite.config.ts and ran it in a page with no Node Buffer present at all:

Node Buffer leaked into realm? undefined
--- result ---
Buffer ctor: Buffer2
Buffer instanceof Uint8Array: true
initEccLib: OK
toOutputScript(bc1p): 5120a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c
psbt.addOutput(bc1p): OK

In a browser there is one realm and the bundled buffer polyfill extends that realm's Uint8Array, so the check passes.

Notes

I could not find an existing issue or PR covering this. Happy to adjust the approach, split out the fee sizing, or swap the ECC dependency if you would rather go a different way.


Written by Claude Opus 5 with human oversight. Every claim above was checked against the source and the failure modes reproduced.

Summary by CodeRabbit

  • New Features

    • Added support for Taproot (P2TR) Bitcoin addresses.
    • Bitcoin transaction fees and size estimates now account for each recipient address type.
    • Fees refresh automatically when the recipient address changes.
  • Bug Fixes

    • Prevented Taproot address processing failures during Bitcoin transactions.
    • Improved validation for Bitcoin addresses across supported address formats and networks.
  • Tests

    • Added coverage for address validation, output sizing, and Taproot fee calculations.

bitcoinjs-lib requires initEccLib() before it can build a P2TR output
script, and nothing in the extension ever called it. address.toOutputScript()
therefore threw "No ECC Library provided" for every bc1p... address,
isAddress() swallowed that in its catch and returned false, and taproot
recipients were rejected as invalid with no setting able to change it.

Register @bitcoinerlab/secp256k1 once from a shared module and import it
from the address validation and PSBT paths. That package is already in the
lockfile at the same version by way of ledger-bitcoin, so this adds no new
package to the bundle.

Also size the recipient output from its own address type. Fee rates are
quoted per vByte and floored at 1 sat/vB, so charging for a 43 vB taproot
output as though it were a 31 vB P2WPKH one leaves the effective rate under
the minimum relay fee and the broadcast fails.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 61268ba7-b6cd-4a96-ac27-1006eec23f6c

📥 Commits

Reviewing files that changed from the base of the PR and between df1cd61 and 68714e9.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (8)
  • packages/extension/package.json
  • packages/extension/src/providers/bitcoin/libs/init-ecc.ts
  • packages/extension/src/providers/bitcoin/libs/utils.ts
  • packages/extension/src/providers/bitcoin/tests/bitcoin.address.validation.test.ts
  • packages/extension/src/providers/bitcoin/ui/libs/signer.ts
  • packages/extension/src/providers/bitcoin/ui/libs/tx-size.ts
  • packages/extension/src/providers/bitcoin/ui/send-transaction/index.vue
  • packages/extension/src/ui/action/views/swap/libs/bitcoin-gasvals.ts

Walkthrough

Adds secp256k1 initialization, recognizes Taproot and other Bitcoin address formats, classifies transaction outputs by address type, and propagates output addresses through send and swap fee calculations.

Changes

Bitcoin Taproot sizing

Layer / File(s) Summary
ECC registration and loading
packages/extension/package.json, packages/extension/src/providers/bitcoin/libs/init-ecc.ts, packages/extension/src/providers/bitcoin/libs/utils.ts, packages/extension/src/providers/bitcoin/ui/libs/signer.ts
Adds the secp256k1 dependency and registers it with bitcoinjs-lib through module side effects used by Bitcoin utilities and signer code.
Address-aware output sizing
packages/extension/src/providers/bitcoin/ui/libs/tx-size.ts, packages/extension/src/providers/bitcoin/tests/bitcoin.address.validation.test.ts
Decodes supported Bech32/Bech32m addresses, maps output counters for P2WPKH, P2WSH, and P2TR, preserves fallback sizing, and tests validation and size calculations.
Fee calculation integration
packages/extension/src/providers/bitcoin/ui/send-transaction/index.vue, packages/extension/src/ui/action/views/swap/libs/bitcoin-gasvals.ts
Passes recipient and transaction output addresses into transaction sizing and recalculates send fees when the recipient changes.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SendTransaction
  participant TxSize
  participant FeeCalculator
  SendTransaction->>TxSize: calculate size using addressTo
  TxSize->>TxSize: classify Taproot and SegWit outputs
  TxSize-->>SendTransaction: return transaction size
  SendTransaction->>FeeCalculator: request fee values
  FeeCalculator-->>SendTransaction: return updated fees
Loading

Suggested reviewers: kvhnuke

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: enabling Bitcoin sends to Taproot (bc1p) recipient addresses.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant