@babylonlabs-io/babylon-tbv-rust-wasm
v2.1.0
Published
BTC Vault WASM bindings for Babylon Trustless Bitcoin Vaults
Readme
@babylonlabs-io/babylon-tbv-rust-wasm
WASM bindings for Babylon Trustless Bitcoin Vaults (TBV), providing TypeScript/JavaScript interfaces for creating Bitcoin peg-in transactions.
The package entry loads the .wasm binary on demand. Importing the entry loads
the wasm-bindgen glue, but it does not download or instantiate the binary. The
binary loads on the first facade call that needs it, or on initWasm().
Overview
This package provides WebAssembly bindings built from the vault-wasm facade — a single binary bundling every supported btc-vault tx-graph version behind version-taking constructors, enabling browser and Node.js applications to construct Bitcoin transactions for TBV.
Installation
pnpm add @babylonlabs-io/babylon-tbv-rust-wasmUsage
Creating a Peg-In Transaction
import {
createPegInTransaction,
type PegInParams,
} from '@babylonlabs-io/babylon-tbv-rust-wasm';
const params: PegInParams = {
depositTxid: 'abc123...',
depositVout: 0,
depositValue: 100000n,
depositScriptPubKey: '76a914...',
depositorPubkey: '02abc...',
claimerPubkey: '03def...',
challengerPubkeys: ['04ghi...'],
pegInAmount: 95000n,
fee: 5000n,
network: 'testnet',
};
const result = await createPegInTransaction(params);
console.log(result.txHex); // Transaction hex
console.log(result.txid); // Transaction IDCreating a Payout Connector
The payout connector generates taproot scripts needed for signing payout transactions (both optimistic and regular payout paths).
import {
createPayoutConnector,
TAP_INTERNAL_KEY,
type PayoutConnectorParams,
} from '@babylonlabs-io/babylon-tbv-rust-wasm';
const params: PayoutConnectorParams = {
txGraphVersion: 1, // fresh: activeVaultCoreVersion(); resume: the vault's stamped version
depositor: 'abc123...', // X-only pubkey (hex)
vaultProvider: 'def456...', // X-only pubkey (hex)
vaultKeepers: ['ghi789...'], // Array of vault keeper x-only pubkeys (hex)
universalChallengers: ['jkl012...'], // Array of universal challenger x-only pubkeys (hex)
timelockPegin: 50, // CSV timelock in blocks for PegIn output
};
const payoutInfo = await createPayoutConnector(params, 'testnet');
// Use taprootScriptHash for PSBT signing (this is the tapLeafHash)
console.log(payoutInfo.taprootScriptHash);
// Use tapInternalKey constant for PSBT signing
console.log(TAP_INTERNAL_KEY); // "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
// Other available fields:
console.log(payoutInfo.payoutScript); // Full payout script (hex)
console.log(payoutInfo.scriptPubKey); // Taproot script pubkey (hex)
console.log(payoutInfo.address); // P2TR addressAssert Payout/NoPayout Connector
The Assert Payout/NoPayout connector generates taproot scripts for the depositor-as-claimer path. It produces payout scripts from Assert output 0 and per-challenger NoPayout scripts.
import {
getAssertPayoutScriptInfo,
getAssertNoPayoutScriptInfo,
type AssertPayoutNoPayoutConnectorParams,
} from '@babylonlabs-io/babylon-tbv-rust-wasm';
const params: AssertPayoutNoPayoutConnectorParams = {
txGraphVersion: 1, // fresh: activeVaultCoreVersion(); resume: the vault's stamped version
claimer: 'abc123...', // Depositor acting as claimer, x-only pubkey (hex)
localChallengers: ['def456...'], // Local challenger x-only pubkeys (hex)
universalChallengers: ['ghi789...'], // Universal challenger x-only pubkeys (hex)
timelockAssert: 144, // CSV timelock in blocks for Assert output
councilMembers: ['jkl012...'], // Security council member x-only pubkeys (hex)
councilQuorum: 3, // Council quorum (N-of-N)
};
// Payout script from Assert output 0
const payoutInfo = await getAssertPayoutScriptInfo(params);
console.log(payoutInfo.payoutScript); // Payout script (hex)
console.log(payoutInfo.payoutControlBlock); // Control block (hex)
// NoPayout script for a specific challenger
const noPayoutInfo = await getAssertNoPayoutScriptInfo(params, 'def456...');
console.log(noPayoutInfo.noPayoutScript); // NoPayout script (hex)
console.log(noPayoutInfo.noPayoutControlBlock); // Control block (hex)ChallengeAssert Connector
The ChallengeAssert connector generates scripts for ChallengeAssert transactions in the depositor-as-claimer path. It uses WOTS public keys and GC WOTS public keys provided by the vault provider.
import {
getChallengeAssertScriptInfo,
type ChallengeAssertConnectorParams,
} from '@babylonlabs-io/babylon-tbv-rust-wasm';
const params: ChallengeAssertConnectorParams = {
txGraphVersion: 1, // fresh: activeVaultCoreVersion(); resume: the vault's stamped version
claimer: 'abc123...', // Depositor acting as claimer, x-only pubkey (hex)
challenger: 'def456...', // Challenger x-only pubkey (hex)
claimerWotsKeysJson: '[[...]]', // JSON string of WOTS public keys (blocks 0-1) from VP
gcWotsKeysJson: '[[...]]', // JSON string of GC WOTS public keys from VP
};
const scriptInfo = await getChallengeAssertScriptInfo(params);
console.log(scriptInfo.script); // ChallengeAssert script (hex)
console.log(scriptInfo.controlBlock); // Control block (hex)Constants
The package exports the taproot internal key constant used for vault transactions:
import { TAP_INTERNAL_KEY, tapInternalPubkey } from '@babylonlabs-io/babylon-tbv-rust-wasm';
// As hex string
console.log(TAP_INTERNAL_KEY);
// "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
// As Buffer
console.log(tapInternalPubkey);
// Buffer containing the x-only pubkeyDevelopment
Prerequisites
For normal development (building TypeScript):
- Node.js >= 24
- pnpm >= 10
For updating WASM bindings (rare):
- Rust 1.94.1 (via
rustup) - Required for reproducible builds wasm-pack>= 0.13.1LLVM(forsecp256k1compilation)
On macOS with Homebrew:
# Install Rust via rustup (not Homebrew)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Install wasm32 target
rustup target add wasm32-unknown-unknown
# Install wasm-pack
cargo install wasm-pack
# Install LLVM
brew install llvm
# The rust-toolchain.toml file in this directory will automatically
# install and use Rust 1.94.1 when you enter this directory.
# Just cd into the directory and rustup will handle it:
cd packages/babylon-tbv-rust-wasm/
rustc --version # Will automatically be 1.94.1How version pinning works:
This package includes a rust-toolchain.toml file that pins Rust to version 1.94.1:
[toolchain]
channel = "1.94.1"When you cd into this directory, rustup automatically:
- Downloads Rust 1.94.1 if not already installed
- Switches to use 1.94.1 for all commands in this directory
- Ensures everyone on the team uses the exact same version
The build script also reports the resolved rustc version and warns on a mismatch; the vault-wasm rust-toolchain.toml is what actually selects the build toolchain.
Building
Regular Build (TypeScript only) - Fast ⚡
pnpm run buildBuild time: very fast
This compiles the TypeScript wrapper (src/index.ts → dist/index.js).
The WASM files are already pre-built and checked into git at dist/generated/.
Use this for:
- Normal development
- Making changes to TypeScript code
- CI/CD pipelines
- Publishing the package
Rebuilding WASM (only when updating vault-wasm) - Slow 🐌
pnpm run build-wasmBuild time: slower
This script:
- Clones the vault-wasm repository
- Checks out a specific commit on a branch
- Builds the
Rustcode toWebAssemblyusingwasm-pack - Outputs generated files to
dist/generated/
You only need to run build-wasm when:
- Updating to a new
btc-vaultcommit/tag/release - The WASM bindings API changes in
btc-vault
Project Structure
packages/babylon-tbv-rust-wasm/
├── src/
│ ├── index.ts # Main entry point
│ ├── types.ts # TypeScript type definitions
│ ├── constants.ts # Taproot constants
│ ├── payoutConnector.ts # Payout connector wrapper
│ ├── assertPayoutNoPayoutConnector.ts # Assert Payout/NoPayout connector
│ └── challengeAssertConnector.ts # ChallengeAssert connector
├── dist/
│ ├── generated/ # WASM files (pre-built)
│ │ ├── vault_wasm.js
│ │ ├── vault_wasm.d.ts
│ │ ├── vault_wasm_bg.wasm
│ │ └── vault_wasm_bg.wasm.d.ts
│ ├── *.js # Compiled TypeScript
│ ├── *.d.ts # Type declarations
│ └── *.map # Source maps
├── scripts/
│ └── build-wasm.js # Rebuild WASM from vault-wasm
└── package.jsonKey points:
src/contains TypeScript source codedist/generated/contains pre-built WASM bindings
Updating the vault-wasm Version
The binary is built from the vault-wasm facade repo — a single artifact
bundling every supported btc-vault tx-graph version behind version-taking
constructors. To bump the pin:
Edit configuration in
scripts/build-wasm.js:const VAULT_WASM_BRANCH = 'main'; const VAULT_WASM_COMMIT = '<new-commit-sha>'; const REQUIRED_RUSTC_VERSION = '1.94'; // vault-wasm rust-toolchain.toml governs the buildRebuild WASM (rustup auto-installs the toolchain from vault-wasm's
rust-toolchain.toml):pnpm run build-wasmThe build script will:
- Clone vault-wasm at the specified commit
- Build the crate root with wasm-pack (web target)
- Copy generated files to
dist/generated/
Test the build:
pnpm run buildRun the frozen golden-vector gate — a pin bump swaps the binary that produces the on-chain-binding secrets. The JS golden vectors (
babylon-ts-sdkvault-secrets/__tests__/expand.test.ts) and the v1 tx-parity test (primitives/psbt/__tests__/pegin.test.ts) must stay byte-identical; any drift is a hard fork and must not ship.Commit the updated WASM files to git (force-add — the root
.gitignoreexcludesdist):git add scripts/build-wasm.js git add -f dist/generated/ git commit -m "chore: update vault-wasm to <commit-sha>"
Reproducible Builds
Why WASM binaries may differ between developers:
The WASM binary includes debug information with file paths, which contain usernames:
- Developer A:
/Users/alice/.cargo/...(21 chars) - Developer B:
/Users/bob/.cargo/...(18 chars) - 29 embedded paths × 3 byte difference = ~87 byte size difference
This causes ~64-100 byte size differences due to different path lengths. This is expected and normal - the binaries are functionally equivalent and work identically.
Why we can't remove paths (yet):
- Rust's
trim-pathsfeature (RFC 3127) would solve this - ❌ Still nightly-only, not available in stable Rust 1.94.1
- ✅ Will be monitored and adopted when stabilized
- Alternative (Docker) adds complexity without sufficient benefit
To minimize differences:
- ✅ All developers use Rust 1.94.1 (selected by the vault-wasm
rust-toolchain.toml; the build script warns on mismatch) - ✅ Same
wasm-packversion - ✅ Same build flags and optimization levels
- ⏰ When
trim-pathsstabilizes: Add to Cargo.toml profile
Bottom line: Small size differences are cosmetic only. Functionality is identical.
API Reference
Functions
createPegInTransaction(params: PegInParams): Promise<PegInResult>
Creates a Bitcoin peg-in transaction for the vault system.
Parameters:
params.depositTxid- Transaction ID of the depositparams.depositVout- Output index of the depositparams.depositValue- Value in satoshisparams.depositScriptPubKey- Script pubkey (hex)params.depositorPubkey- Depositor's x-only pubkey (hex)params.claimerPubkey- Claimer's x-only pubkey (hex)params.challengerPubkeys- Array of challenger x-only pubkeys (hex)params.pegInAmount- Amount to peg-in in satoshisparams.fee- Transaction fee in satoshisparams.network- Bitcoin network ("bitcoin","testnet","regtest", or"signet")
Returns:
txHex- Transaction hex stringtxid- Transaction IDvaultScriptPubKey- Vault script pubkey (hex)vaultValue- Vault output value in satoshischangeValue- Change output value in satoshis (0 if no change)
createPayoutConnector(params: PayoutConnectorParams, network: Network): Promise<PayoutConnectorInfo>
Creates a payout connector for signing payout transactions.
Parameters:
params.txGraphVersion- Tx graph version selecting the builder (fails closed if unsupported)params.depositor- Depositor's x-only pubkey (hex)params.vaultProvider- Vault provider's x-only pubkey (hex)params.vaultKeepers- Array of vault keeper x-only pubkeys (hex)params.universalChallengers- Array of universal challenger x-only pubkeys (hex)params.timelockPegin- CSV timelock in blocks for PegIn outputnetwork- Bitcoin network
Returns:
payoutScript- Full payout script (hex)taprootScriptHash- Taproot script hash / tapLeafHash for PSBT signingscriptPubKey- Taproot script pubkey (hex)address- P2TR addresspayoutControlBlock- Serialized control block for Taproot script path spend (hex)
getPeginPayoutScriptInfo(params: PayoutConnectorParams): Promise<{ payoutScript: string; payoutControlBlock: string }>
Returns the payout script and control block from the PeginPayoutConnector without requiring a network parameter. Useful for building payout PSBTs where you need the script and control block for Taproot script path spending.
Parameters:
params.txGraphVersion- Tx graph version selecting the builder (fails closed if unsupported)params.depositor- Depositor's x-only pubkey (hex)params.vaultProvider- Vault provider's x-only pubkey (hex)params.vaultKeepers- Array of vault keeper x-only pubkeys (hex)params.universalChallengers- Array of universal challenger x-only pubkeys (hex)params.timelockPegin- CSV timelock in blocks for PegIn output
Returns:
payoutScript- Payout script hex stringpayoutControlBlock- Serialized control block hex string
getAssertPayoutScriptInfo(params: AssertPayoutNoPayoutConnectorParams): Promise<AssertPayoutScriptInfo>
Generates the Payout script and control block from Assert output 0 for the depositor-as-claimer path.
Parameters:
params.txGraphVersion- Tx graph version selecting the builder (fails closed if unsupported)params.claimer- Claimer (depositor) x-only pubkey (hex)params.localChallengers- Array of local challenger x-only pubkeys (hex)params.universalChallengers- Array of universal challenger x-only pubkeys (hex)params.timelockAssert- CSV timelock in blocks for Assert outputparams.councilMembers- Array of security council member x-only pubkeys (hex)params.councilQuorum- Council quorum (N-of-N)
Returns:
payoutScript- Payout script (hex)payoutControlBlock- Control block for the payout script (hex)
getAssertNoPayoutScriptInfo(params: AssertPayoutNoPayoutConnectorParams, challengerPubkey: string): Promise<AssertNoPayoutScriptInfo>
Generates the NoPayout script and control block for a specific challenger. Each challenger has a distinct NoPayout script.
Parameters:
params- Same asgetAssertPayoutScriptInfochallengerPubkey- The challenger's x-only pubkey (hex)
Returns:
noPayoutScript- NoPayout script (hex)noPayoutControlBlock- Control block for the NoPayout script (hex)
getChallengeAssertScriptInfo(params: ChallengeAssertConnectorParams): Promise<ChallengeAssertScriptInfo>
Generates the ChallengeAssert script and control block for a specific challenger. Uses WOTS public keys and GC WOTS public keys from the vault provider.
Parameters:
params.txGraphVersion- Tx graph version selecting the builder (fails closed if unsupported)params.claimer- Claimer (depositor) x-only pubkey (hex)params.challenger- Challenger x-only pubkey (hex)params.claimerWotsKeysJson- JSON string of WOTS public keys (blocks 0-1) from VPparams.gcWotsKeysJson- JSON string of GC WOTS public keys from VP
Returns:
script- ChallengeAssert script (hex)controlBlock- Control block for the ChallengeAssert script (hex)
Constants
TAP_INTERNAL_KEY: string
The unspendable taproot internal key used in vault transactions (BIP-341 nothing-up-my-sleeve number).
Value: "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
tapInternalPubkey: Buffer
The same as TAP_INTERNAL_KEY but as a Buffer for convenience.
WASM Classes
The package also exports these wasm-bindgen classes:
WasmPeginTx- Low-level peg-in transaction classWasmPrePeginTx- Low-level Pre-PegIn transaction classWasmPeginPayoutConnector- Low-level payout connector classWasmPrePeginHtlcConnector- Low-level Pre-PegIn HTLC connector class
The classes have no value guards. Call initWasm() before you construct a
class, and check every value a class returns before you use it.
Check amounts before you convert them. new BigUint64Array() wraps negative
and oversized values without an error. Pass pegInAmounts through
assertPositiveBigintArray first. Call free() on each instance when you are
done with it:
import {
assertPositiveBigintArray,
initWasm,
WasmPrePeginTx,
} from "@babylonlabs-io/babylon-tbv-rust-wasm";
await initWasm();
const amounts = new BigUint64Array(
assertPositiveBigintArray(pegInAmounts, "pegInAmounts"),
);
const transaction = new WasmPrePeginTx(/* ..., amounts, ... */);
try {
// Check every value that you read from transaction.
} finally {
transaction.free();
}To build transactions, prefer the guarded builders in
@babylonlabs-io/ts-sdk/tbv/core/primitives: buildPrePeginPsbt,
buildPeginTxFromFundedPrePegin, buildRefundPsbt, buildPeginInputPsbt and
buildPayoutPsbt.
If you sign a transaction from a class, derive the expected outputs and signing
data independently. Compare them with the values the class returns before you
sign. initWasm() and the facade's amount bounds do not prove that transaction
bytes match your request. WasmPeginTx.fromJson takes no trusted inputs, so a
check against the same JSON proves nothing.
Releases 0.17.0 to 0.19.0 exported these classes from
@babylonlabs-io/babylon-tbv-rust-wasm/raw. That entry is removed. Import the
classes from the package root. They are the same class objects.
