npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tachibtc/taurus-wallet-aggregator

v0.4.5

Published

Taurus wallet aggregator

Downloads

396

Readme

taurus-wallet-aggregator

A modular Bitcoin wallet aggregator in TypeScript. HD key derivation, PSBT construction and signing, UTXO scanning, coin selection, and a BIP32/39 keystore driven by a user-supplied seed phrase. Ships an optional browser/React entry point with two wallet-import paths — the Xverse extension and a locally-derived seed phrase — plus a useTestnetWallet hook with live chain switching.

Status: 0.4.4 — Testnet wallet — signet (default) and regtest only, p2wpkh-only (native segwit, BIP84) for the built-in HD signer. The library works end-to-end against a bitcoind regtest/devnet node. Mainnet and public testnet are not selectable networks. Support for p2sh-p2wpkh, p2pkh, and p2tr was deliberately removed in 0.3.x and will be re-introduced in a future release. External wallets (Xverse) can still sign their native segwit/taproot inputs via the browser entry's PSBT bridge. Treat the package as pre-release until you have validated it in your own integration tests.

⚠️ Upgrading from 0.2.x? Sweep your legacy funds first. The 0.3.0 signer derives only native-segwit m/84'/… addresses, so any balance still sitting on the old legacy m/44'/… P2PKH chain becomes invisible to balance, scan, and send — it is not lost on-chain, but the library will not see or spend it. Before cutting over, run assertNoStrandedLegacyFunds() (or scanLegacyP2pkh()) to check, and sweepLegacy() to move any legacy UTXOs onto the new p2wpkh chain. See Legacy P2PKH migration.


Features

  • HD key derivation — BIP32 / BIP39 / BIP84
  • One address type todayp2wpkh (native segwit). Other types removed pending re-introduction
  • Networkssignet (default) and regtest only, plus arbitrary custom networks via registerNetwork(). Mainnet and public testnet are not selectable networks.
  • PSBT build / sign / finalize — single-sig p2wpkh. buildPsbt() verifies fetched prevouts against the scanner-reported UTXO before inputs are added, so tampering surfaces from PSBT construction rather than from signing
  • UTXO discoveryscantxoutset with automatic gap-limit extension and a bounded rescan loop (maxSyncIterations) to defend against adversarial RPC endpoints
  • Largest-first coin selection — with dust absorption and sweep mode
  • Pluggable providers — built-in KeystoreProvider plus a WalletProvider interface for external/hardware wallets, including a shipped XverseProvider
  • Browser / React entry@tachibtc/taurus-wallet-aggregator/browser exports two import paths (Xverse extension and BIP39 seed phrase), a useTestnetWallet React hook with live chain switching, a fetch-based gateway RPC adapter, and a PSBT-to-wallet signing bridge
  • Esplora-preferred data plane — where a network config carries an rpc.esplora indexer (signet does), getUtxos() / broadcastTx() use it and fall back to the authoritative JSON-RPC node when it is unreachable
  • Vault user keysderiveUserKey() records the scheme/path/fingerprint behind a key, and resolveUserKey() recovers records written before that metadata existed (including legacy m/44' ones)
  • Lock-contention retryscantxoutset takes a node-global lock, so sync() retries Core error -8 and scan timeouts with jittered backoff (tunable via scanRetry)
  • BIP-322 verificationverifyBip322Signature() validates the "simple" P2WPKH / P2TR signatures returned by browser wallets
  • Serialized sends — concurrent send() calls are queued so UTXOs cannot be double-selected across an in-flight sync
  • BDK-compatible output descriptorswpkh([fp/84'/.../0']xpub.../0/*) for interop with bdk-cli, Sparrow, or Core's importdescriptors
  • Bitcoin Core JSON-RPC client — typed, auth-redacting, AbortController timeouts, distinguishes RPC errors from transport failures
  • Safety — private fields hold secrets; lock() wipes the seed and account-node private key bytes; the BIP39 passphrase is consumed at construction and never retained; custom toJSON / util.inspect hide sensitive material

Install

npm install @tachibtc/taurus-wallet-aggregator

The package is published to GitHub Packages under the @tachibtc scope — see Publishing (GitHub Packages) below for the .npmrc you need.

Requires Node.js ≥ 18 (declared in package.json engines), which provides a global fetch. Users running an unsupported older Node can still use the RPC client by passing their own fetchImpl (e.g. undici.fetch), but versions below 18 are not officially supported.

Entry points

| Subpath | Import | Contains | |------------|-----------------------------------------------------|--------------------------------------------------------------------------| | core | @tachibtc/taurus-wallet-aggregator | WalletAggregator, Wallet, Keystore, RPC client, networks, PSBT | | browser | @tachibtc/taurus-wallet-aggregator/browser | Xverse connector, seed-phrase connector (connectSeed), XverseProvider, useTestnetWallet hook, mnemonic/HD derivation, gateway RPC adapter, BIP-322 verify, PSBT bridge |

The browser entry pulls in react (an optional peer dependency — only required if you use useTestnetWallet) and sats-connect (the Xverse SDK, bundled as a regular dependency). The core entry stays free of both, so a Node-only consumer never loads them.

Integrating the browser flow? docs/testnet-wallet-integration.md is the step-by-step guide: pinned versions, GitHub Packages auth, endpoint env vars, the hook API, both import paths, and a verification checklist.


Quick start

import {
  BitcoinCoreRpcClient,
  WalletAggregator,
} from "@tachibtc/taurus-wallet-aggregator";

const rpc = new BitcoinCoreRpcClient({
  url: "http://127.0.0.1:18443",
  username: "foo",
  password: "bar",
});

const aggregator = WalletAggregator.fromMnemonic(
  "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
  { network: "regtest", rpc },
);

// Add a BIP84 (native segwit p2wpkh) account and start using it.
const wallet = aggregator.addAccount({ addressType: "p2wpkh" });

console.log(wallet.receiveAddress);
// regtest p2wpkh, e.g. "bcrt1qxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

await wallet.sync();
console.log(wallet.balance); // { confirmed, unconfirmed, total } in sats

// Destination must also be p2wpkh — other script types are rejected.
const { txid } = await wallet.send({
  to: "bcrt1qyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
  amount: 50_000n, // sats, bigint
  feeRate: 2,     // sat/vB
});

// Done. Lock to wipe the seed + private keys from memory.
aggregator.lock();

A runnable version is in examples/basic.ts:

BITCOIND_URL=http://127.0.0.1:18443 \
BITCOIND_USER=foo BITCOIND_PASS=bar \
npx tsx examples/basic.ts

API reference

WalletAggregator

The top-level entry point. Owns the seed, manages one or more Wallet instances keyed by (provider, addressType, account), and exposes aggregate-level helpers.

// Construction
WalletAggregator.fromMnemonic(mnemonic, { network, rpc, passphrase?, gapLimit?, maxSyncIterations?, scanRetry? })
WalletAggregator.fromSeed(seedBytes, { network, rpc, gapLimit?, maxSyncIterations?, scanRetry? })
WalletAggregator.createNew({ network, rpc }, strength?) // { aggregator, mnemonic }

// HD accounts (built-in keystore provider)
aggregator.addAccount({ addressType, account? })
aggregator.getWallet({ addressType, account?, provider? })  // alias: aggregator.wallet(...)

// External providers (hardware/extension wallets)
aggregator.addProvider(walletProvider)  // returns the Wallet; throws on reserved name "keystore"

// Aggregate views
aggregator.wallets             // readonly Wallet[]
aggregator.signers             // readonly SignerInfo[]
aggregator.info                // { network, signers, wallets }
aggregator.aggregateBalance()  // { confirmed, unconfirmed, total }
aggregator.aggregateUtxos()    // Utxo[] across every wallet

// Lifecycle
await aggregator.syncAll()     // sync every registered wallet in parallel
aggregator.lock()              // wipe the seed + lock every child wallet
aggregator.locked              // boolean

addAccount is idempotent: calling it twice with the same (addressType, account) returns the same Wallet instance. The BIP39 passphrase is applied once during seed derivation and not retained on the aggregator. Once lock() has wiped the seed, further addAccount() calls throw WalletError("AGGREGATOR_LOCKED"). scanRetry is propagated to every child Wallet (see sync() retry).

Wallet

A single account on a single provider.

new Wallet({ provider, rpc, gapLimit?, maxSyncIterations?, scanRetry? })
// or, for backwards compatibility:
new Wallet({ keystore, rpc, gapLimit?, maxSyncIterations?, scanRetry? })

wallet.info            // WalletInfo (network, addressType, descriptors, accountXpub, ...)
wallet.provider        // the underlying WalletProvider
wallet.receiveAddress  // next unused external address
wallet.changeAddress   // next unused internal address
wallet.balance         // { confirmed, unconfirmed, total } in sats (bigint)
wallet.utxos           // readonly Utxo[] (mature, spendable)
wallet.immatureUtxos   // coinbase outputs with < 100 confirmations
wallet.signers         // SignerInfo[] (single entry)
wallet.locked          // boolean
wallet.state           // "new" | "synced"

await wallet.sync()
await wallet.send({ to, amount, feeRate })  // { txid }
wallet.lock()

send() auto-syncs on first call, validates the destination against the wallet's network, rejects non-p2wpkh destinations, selects coins largest-first, builds a PSBT, signs it via the provider, and broadcasts via sendrawtransaction. Concurrent calls are serialized internally, so an in-flight send cannot have its UTXOs reselected by a parallel call. The change pointer only advances after a successful broadcast, so a failed send will not waste a fresh change address.

sync() retry and scan-lock contention

scantxoutset takes a node-global, single-threaded lock: only one scan runs on a node at a time, across every client of a shared hosted gateway. An overlapping scan fails with Bitcoin Core error -8 ("Scan already in progress"), and a long scan (~14 s on signet) can outrun the request timeout. Both are transient, so scanForUtxos() — and therefore sync() — retries them with exponential backoff plus ±20% jitter, and surfaces everything else (bad descriptor, auth failure, malformed response) on the first attempt.

new Wallet({
  provider,
  rpc,
  scanRetry: {
    attempts: 4,          // total attempts including the first; 1 disables retrying
    baseDelayMs: 2_000,   // doubles per attempt
    maxDelayMs: 8_000,    // backoff ceiling
  },
});

Pass the same scanRetry on AggregatorConfig to apply it to every child wallet, or { attempts: 1 } to opt out and handle -8 yourself.

WalletProvider and KeystoreProvider

Wallet is built on a WalletProvider abstraction so the same wallet code can drive the built-in HD keystore or an external signer.

interface WalletProvider {
  readonly name: string;
  readonly network: NetworkConfig;
  readonly addressType: AddressType;
  readonly account: number;
  readonly fingerprint: string;
  // null when the provider has no BIP32 account behind it — see below.
  readonly accountPath: string | null;
  readonly accountXpub: string | null;
  readonly locked: boolean;
  // Optional. When `false`, the wallet rejects the internal p2wpkh `send()`
  // builder and the caller must sign externally (e.g. XverseProvider).
  readonly supportsInternalSend?: boolean;

  deriveReceiveAddresses(count: number): DerivedKey[];
  deriveChangeAddresses(count: number): DerivedKey[];
  signPsbt(psbt: Psbt, inputs: readonly Utxo[]): Promise<void> | void;
  buildDescriptor(change: boolean): string;
  getSignerInfo(): SignerInfo;
  lock(): void;
}

KeystoreProvider is the built-in implementation (used implicitly by aggregator.addAccount()). External providers are registered via aggregator.addProvider(provider) — the name "keystore" is reserved so a plugin can't shadow the built-in HD slot.

Providers without a derivation. accountPath, accountXpub, and DerivedKey.path (hence Utxo.derivationPath and SignerInfo.accountPath) are null for a provider whose key lives outside any derivation the SDK can see — XverseProvider is the one that ships. Null is the only way that absence is signalled: these fields never carry a descriptive placeholder, so code that records or parses a derivation can branch on path === null up front instead of discovering mid-parse that the string isn't a path. Everything the SDK derives locally (Keystore.deriveAddress, deriveAddressFromAccountNode, the seed import, the migration scan) returns the narrower HdDerivedKey, whose path is always a real path — use that type when a path is required.

Keystore

Low-level BIP32/39 HD wallet. You don't normally construct this directly — WalletAggregator owns it via KeystoreProvider — but it's exported for advanced use.

Keystore.fromMnemonic(mnemonic, passphrase, networkConfig, addressType, account)
Keystore.fromSeedBytes(seed, networkConfig, addressType, account)

keystore.fingerprint      // master key fingerprint (8 hex chars)
keystore.accountPath      // e.g. "m/84'/1'/0'"
keystore.accountXpub      // base58 xpub / tpub at the account level
keystore.deriveAddress(change, index) // DerivedKey (public-only, works while locked)
keystore.watchOnlyAccountNode()       // BIP32Interface (neutered)
keystore.lock()                       // best-effort: zeros the account node's
                                      // private bytes, drops the reference

Keystore does not retain the BIP39 seed: only the BIP84 account node is kept, so lock() only has to wipe a single subtree of private material. Secrets never appear in JSON.stringify, util.inspect, or structured clones.

Errors

All errors inherit from WalletError and expose a string code:

| Class | code | Thrown when | |--------------------------|-------------------------|-------------------------------------------------------------------| | InvalidMnemonicError | INVALID_MNEMONIC | BIP39 checksum fails | | InvalidNetworkError | INVALID_NETWORK | Unknown network name | | InvalidAddressError | INVALID_ADDRESS | Address not valid on the network, or destination is not p2wpkh | | InvalidAmountError | INVALID_AMOUNT | Amount below dust / bad decimal / non-integer sats | | InsufficientFundsError | INSUFFICIENT_FUNDS | Selected UTXOs don't cover amount + fee | | WalletLockedError | WALLET_LOCKED | Signing after lock() | | FeeRateError | FEE_RATE_ERROR | feeRate < 1 or non-finite | | PsbtError | PSBT_ERROR | PSBT build / finalize failure (incl. parent-tx verification) | | RpcError | RPC_ERROR | JSON-RPC error response | | RpcTransportError | RPC_TRANSPORT_ERROR | HTTP/network failure | | WalletError | AGGREGATOR_LOCKED | addAccount() after the aggregator seed has been wiped | | WalletError | RESERVED_PROVIDER_NAME| addProvider() called with a name reserved for internal use | | WalletError | WALLET_NOT_FOUND | getWallet() for an unregistered external provider | | WalletError | INVALID_ACCOUNT_SPEC | addAccount() called with provider other than "keystore" | | WalletError | INVALID_WALLET_OPTIONS| new Wallet(...) without a provider or keystore | | WalletError | SYNC_LIMIT_EXCEEDED | sync() exceeded maxSyncIterations rescan iterations | | WalletError | INTERNAL_SEND_UNSUPPORTED | wallet.send() on a provider with supportsInternalSend === false (e.g. XverseProvider) | | WalletError | INVALID_RPC_CONFIG | BitcoinCoreRpcClient constructed with a malformed URL | | WalletError | LEGACY_FUNDS_DETECTED | assertNoStrandedLegacyFunds() found a non-empty m/44' chain | | WalletError | NO_LEGACY_FUNDS | sweepLegacy() with nothing to sweep | | WalletError | INVALID_USER_KEY | deriveUserKey() / resolveUserKey() given a malformed pubkey or index | | WalletError | USER_KEY_NOT_FOUND | requireUserKey() exhausted the search window without a match | | WalletError | UNSUPPORTED_USER_KEY_SCHEME | assertSupportedUserKeyScheme() on a legacy bip44-p2pkh descriptor |


Custom network guide

Every network — built-in or custom — is an entry in a runtime registry. You can register your own:

import * as bitcoin from "bitcoinjs-lib";
import {
  registerNetwork,
  WalletAggregator,
} from "@tachibtc/taurus-wallet-aggregator";

registerNetwork("mutinynet", {
  network: bitcoin.networks.testnet, // bitcoinjs-lib Network object
  coinType: 1,                        // BIP-44 coin type
  bech32: "tb",                       // human-readable bech32 prefix
  name: "mutinynet",
});

const aggregator = WalletAggregator.fromMnemonic(mnemonic, {
  network: "mutinynet",
  rpc,
});

Guardrails:

  • registerNetwork(name, ...) throws if name is already registered — you must unregisterNetwork(name) first to replace it.
  • unregisterNetwork(name) throws for built-in names (signet, regtest).
  • listNetworks() returns every currently registered name, built-in or not.
  • getNetwork(name) returns a defensive copy, so mutations don't leak back into the registry.

Use cases: sidechains with their own bech32 prefix, custom signets, a rebranded regtest, BIP-44 coin-type forks, staging environments.

signet / regtest gateway endpoints

The built-in SIGNET and REGTEST configs (importable, and registered as the signet / regtest networks) default to the tachibtc hosted validator nodes. Both are Bitcoin Core JSON-RPC 1.0 proxies (POST /), not Esplora servers. Every method is served without authentication, so no API key is required. Override the endpoints with env vars so you aren't pinned to a tenant (rate limits, self-hosted gateways):

| Variable | Default | Maps to | |-----------------------------|-----------------------------------------|------------| | TAURUS_SIGNET_RPC | https://rpc-signet.tachibtc.com | rpc.jsonRpc (and rpc.rest) | | TAURUS_SIGNET_ESPLORA (legacy name: TAURUS_SIGNET_ELECTRS) | https://btc-signet.xverse.app | rpc.esplora | | TAURUS_SIGNET_EXPLORER | https://explorer.bc-2.jp | rpc.explorer (links only) | | TAURUS_REGTEST_RPC | https://rpc-regtest.tachibtc.com | rpc.jsonRpc (and rpc.rest) | | TAURUS_REGTEST_EXPLORER | https://explorer-regtest.tachibtc.com | rpc.explorer (links only) |

rpc.rest is a deprecated compat field. The Tachi daemons are not Esplora servers, so it has no endpoint or env var of its own — it mirrors rpc.jsonRpc, and nothing in the data plane reads it. Point rpc.esplora at a real indexer instead. (TAURUS_SIGNET_REST / TAURUS_REGTEST_REST used to exist and are now ignored — see the 0.4.2 notes.)

Two data sources on signet. SIGNET carries both an Esplora indexer (Xverse's public signet infrastructure) and the Tachi JSON-RPC node. They serve the same chain, so getUtxos() / broadcastTx() prefer the indexer — ~0.4 s per address query versus ~14 s for a full scantxoutset, no node-global lock, and real mempool visibility (status.confirmed === false for unconfirmed outputs, which scantxoutset cannot report). The JSON-RPC node stays authoritative and is the automatic fallback when the indexer is unreachable (DNS/connection failure, timeout, 5xx, non-JSON proxy page). A malformed indexer response does not fall back — it throws, so a broken or tampered indexer surfaces instead of hiding behind the node; likewise a broadcast rejection (4xx) is reported as-is rather than retried, since the node would reject it identically. Force the node explicitly with { preferEsplora: false }.

REGTEST has no indexer configured, so it runs scantxoutset / sendrawtransaction over rpc.jsonRpc directly — already fast on a small chain. Both endpoints accept every method the SDK calls unauthenticated. All URLs are validated by requireSecureUrl (https required; http allowed only for loopback regtest hosts).

These are read at module-load time. In Node they come from process.env; in the browser your bundler inlines process.env.X at build time, so set them in the build environment. See .env.example. For full control you can also skip these configs entirely and pass your own WalletNetworkConfig / registerNetwork().


Browser / React — Xverse or seed phrase

The @tachibtc/taurus-wallet-aggregator/browser entry offers two import paths: the Xverse browser extension, and a locally-derived BIP39 seed phrase. It re-exports everything node-safe (networks, the gateway RPC adapter, BIP-322 verify, the PSBT bridge) plus both connectors, the XverseProvider, the mnemonic/derivation primitives, and the React hook.

Import path × chain support

The import-wallet flow supports signet (default) and regtest, both p2wpkh — but the two import paths do not cover them equally:

| Import path | signet | regtest | Signing | BIP-322 message signing | |---|:---:|:---:|---|---| | Xverse extension | ✅ | ❌ | via extension | ✅ | | Seed phrase | ✅ | ✅ | local keystore | ❌ |

The Xverse extension has no regtest mode, so regtest is imported with a seed phrase (importSeedPhrase / connectSeed).

Both import paths share one data plane per chain. Signet is configured with an Esplora indexer (Xverse's public signet infrastructure) in front of the Tachi JSON-RPC node — the same chain, but ~0.4 s per balance query instead of ~14 s for a full scantxoutset scan, plus real mempool visibility. The node remains authoritative and is the automatic fallback if the indexer is unreachable. Regtest uses scantxoutset directly, which is already fast on a small chain.

Because signet and regtest share BIP-44 coin type 1, a seed wallet switching chains re-encodes the same account locally — same key, same m/84'/1'/0'/0/0 path, only the address HRP changes (tb1…bcrt1…) — so the user is never asked for the mnemonic twice.

// Seed-phrase import — the only path that reaches regtest.
const w = useTestnetWallet();
await w.importSeedPhrase(mnemonic, "regtest");
w.address;         // bcrt1…
w.derivationPath;  // m/84'/1'/0'/0/0
await w.switchNetwork("signet");   // tb1… — no re-prompt
w.disconnect();    // locks + zeros the keystore

Gate a chain selector with w.chainsFor(walletType) rather than w.supportedChains; connect("xverse", "regtest") fails fast with an explanatory error instead of reaching the extension.

React hook

connect() takes an optional chain name and defaults to signet; switchNetwork() makes changing chains one call.

import { useTestnetWallet } from "@tachibtc/taurus-wallet-aggregator/browser";

function WalletPanel() {
  const w = useTestnetWallet(); // w.chain defaults to "signet"

  if (w.status !== "connected") {
    // Defaults to signet. (The Xverse connector supports signet only today.)
    return <button onClick={() => w.connect("xverse")}>Connect Xverse</button>;
  }
  return (
    <>
      {/* Easy chain switching between the supported chains */}
      <select value={w.chain} onChange={(e) => w.switchNetwork(e.target.value)}>
        {w.supportedChains.map((c) => (
          <option key={c} value={c}>{c}</option>
        ))}
      </select>
      <p>{w.address}</p>
      <button onClick={w.refreshBalance}>Balance: {w.balance?.total ?? 0} sats</button>
      <button onClick={() => w.signMessage("hello")}>Sign message</button>
      <button onClick={w.disconnect}>Disconnect</button>
    </>
  );
}

The hook exposes:

{
  // state
  walletType,      // "xverse" | "seed" | null
  chain,           // "signet" | "regtest" — defaults to signet, never null
  network,         // the full WalletNetworkConfig for `chain`
  address, publicKey,
  derivationPath,  // "m/84'/1'/0'/0/0" for a seed import; null for Xverse
  balance,         // { confirmed, total } in sats (numbers) — DISPLAY ONLY
  status,          // "idle" | "connecting" | "connected" | "error"
  error,

  // constants
  supportedChains, // ["signet", "regtest"] — every chain in the flow
  chainsFor,       // (walletType) => the chains THAT type can connect on

  // actions
  connect, importSeedPhrase, switchNetwork, disconnect,
  refreshBalance, signPsbt, signMessage,
}

signMessage returns a BIP-322 signature you can check with verifyBip322Signature(); it is Xverse-only and throws for a seed wallet, since the SDK ships a verifier but no BIP-322 signer. The hook's balance is a pair of plain number sats from the gateway — distinct from the core WalletBalance (bigint confirmed/unconfirmed/total) — and is display-only (see Security model).

Connector and provider (no React)

import {
  connectXverse,
  isXverseInstalled,
  isXverseSupportedChain,
  signPsbtXverse,
  signMessageXverse,
  XverseProvider,
  verifyBip322Signature,
  SIGNET,
} from "@tachibtc/taurus-wallet-aggregator/browser";

if (!isXverseInstalled()) throw new Error("Install the Xverse extension");
if (!isXverseSupportedChain("signet")) throw new Error("unsupported chain");

const connection = await connectXverse(SIGNET); // { paymentAddress, ordinalsAddress, publicKey, walletType }

// Verify a signed message. Pass the expected network to bind the address to it
// — signet and testnet share an encoding, so without it the result reports the
// family name ("testnet") rather than the exact chain.
const { signature } = await signMessageXverse("hello", connection.paymentAddress, SIGNET);
const result = verifyBip322Signature(
  connection.paymentAddress,
  "hello",
  signature,
  "signet",
); // { valid, reason?, network? }

// Bridge the connection into a WalletAggregator for balance / UTXO aggregation:
const provider = new XverseProvider(connection, SIGNET, { name: "xverse", account: 0 });
aggregator.addProvider(provider);

The seed-phrase connector is the same shape without an extension, and is the only path that reaches regtest:

import {
  connectSeed,
  deriveSeedAddress,
  signPsbtSeed,
  getWalletNetwork,
  getUtxos,
} from "@tachibtc/taurus-wallet-aggregator/browser";

const network = getWalletNetwork("regtest");
const { connection, keystore } = connectSeed(mnemonic, network, { account: 0, index: 0 });

connection.paymentAddress;  // bcrt1…
connection.path;            // m/84'/1'/0'/0/0
connection.accountXpub;     // watch-only descriptor material
connection.fingerprint;     // which seed this came from

const utxos = await getUtxos(network, connection.paymentAddress);
const signed = signPsbtSeed(psbtBase64, keystore, network);

// Same account re-encoded for signet — no mnemonic needed, works after lock().
const onSignet = deriveSeedAddress(keystore, getWalletNetwork("signet"));

keystore.lock();  // ALWAYS: zeros the account node's private bytes

connectSeed hands you a live Keystore holding private key material — you own its lifetime. The hook locks it for you on disconnect() and on unmount.

Capability boundary. XverseProvider supports registration, balance / UTXO aggregation, and external PSBT signing. It does not support the aggregator's internal wallet.send() builder — the aggregator has no local signer for Xverse's externally-controlled key. The provider sets supportsInternalSend = false, so wallet.send() throws WalletError("INTERNAL_SEND_UNSUPPORTED") and you must spend through the browser flow below. Its addressType is verified, not nominal: the constructor resolves the connected account's scriptPubKey and throws InvalidAddressError unless it is a real native p2wpkh (0014…), so nested-segwit and taproot accounts are rejected until the AddressType union widens rather than being mislabelled. Because Xverse controls its key outside any derivation the SDK can see, accountPath, accountXpub, and every derived key's path are null — see Providers without a derivation.

Signing an externally-built PSBT

prepareForWalletSigning() serializes an unsigned PSBT into the base64 / hex shapes browser wallets expect (it does not sign — the internal-signer path is untouched):

import { prepareForWalletSigning } from "@tachibtc/taurus-wallet-aggregator";

const req = prepareForWalletSigning(psbt, connection.paymentAddress);
// req: { psbtBase64, psbtHex, inputsToSign: [{ address, signingIndexes }] }
const signedBase64 = await signPsbtXverse(req.psbtBase64, req.inputsToSign, SIGNET);

Gateway RPC adapter

rpcCall, getUtxos, and broadcastTx are a dependency-free fetch-based client over a WalletNetworkConfig's endpoints. They are distinct from the core BitcoinCoreRpcClient and work against SIGNET / REGTEST or any network defined with the same rpc shape:

import { getUtxos, broadcastTx, rpcCall, SIGNET } from "@tachibtc/taurus-wallet-aggregator";

const utxos = await getUtxos(SIGNET, address);        // RestUtxo[]
const txid = await broadcastTx(SIGNET, signedTxHex);

// Force the authoritative node and skip the indexer:
await getUtxos(SIGNET, address, { preferEsplora: false, scanRetries: 4 });
await broadcastTx(SIGNET, signedTxHex, { preferEsplora: false });

// Any Bitcoin Core JSON-RPC method, against rpc.jsonRpc:
const info = await rpcCall(SIGNET, "getblockchaininfo", []);

rpcCall always uses rpc.jsonRpc. getUtxos / broadcastTx prefer rpc.esplora when it is configured and fall back to the node — see signet / regtest gateway endpoints. All requests time out after 30 s.


Network + address type support matrix

| Address type | Purpose | signet | regtest | custom | |---------------|---------|:------:|:-------:|:------:| | p2wpkh | 84 | ✅ | ✅ | ✅ | | p2sh-p2wpkh | 49 | — | — | — | | p2pkh | 44 | — | — | — | | p2tr | 86 | — | — | — |

Only p2wpkh is wired through derivation, descriptors, PSBT building, and coin selection as of 0.4.0. The other purposes will be re-added in a later release; trying to use them today is a TypeScript error.

signet and regtest are also exported as ready-to-use gateway configs (SIGNET / REGTEST, registered so getNetwork("signet") returns the full WalletNetworkConfig). signet reuses bitcoinjs-lib's testnet encoding. The browser import-wallet flow (connect / switchNetwork) supports signet (default) and regtest only — see Browser / React — Xverse or seed phrase.


Security model

  • Private fields — the seed and account node are held in #private class fields on WalletAggregator and Keystore. They can't be read via reflection, serialized via JSON.stringify, or leak into util.inspect / structured clones.
  • Seed minimizationWalletAggregator retains the seed only long enough to derive new accounts; Keystore retains only the account node, not the full seed. The BIP39 passphrase is consumed at construction and never retained.
  • lock() — wipes the seed in place (fill(0)), drops the SeedRef, and locks every child wallet. On Keystore it is best-effort memory hygiene: the account node's private key bytes are zeroed where the buffer is writable, and the reference is dropped. After lock(), Keystore.signerFor() throws WalletLockedError. Public-only derivation still works via the neutered account xpub, so you can lock a wallet and continue to generate addresses without exposing private keys.
  • toJSON / inspect overrides — serialize only public fields (addressType, accountPath, accountXpub, fingerprint, locked). No secret material is emitted, even transitively.
  • PSBT parent verification — during PSBT construction (buildPsbt()), every input's parent transaction is fetched and its referenced prevout is verified to match the scanner-reported value and scriptPubKey before the input is added, guarding against an input's value being misstated before it is signed over. Mismatches surface as PsbtError from construction, not from signing.
  • Serialized sends — concurrent wallet.send() calls queue rather than race, so UTXOs can't be double-selected across an in-flight sync.
  • Bounded syncsync() extends the gap limit on demand, but is capped by maxSyncIterations (default 32) so a malicious RPC can't drive unbounded address derivation.
  • RPC auth redactionBitcoinCoreRpcClient strips inline user:pass out of the URL, stores the auth header separately, and hides it from toJSON + inspect. HTTP error messages never echo the Authorization header. Transport failures surface as RpcTransportError distinct from JSON-RPC errors.
  • Reserved provider namesaddProvider() rejects the name "keystore", so an external plugin cannot shadow the built-in HD slot and redirect HD account creation.
  • Scope — this library holds secrets in memory. It does not persist them. Encrypting and storing the mnemonic is your application's job.

BDK compatibility notes

  • Descriptors emitted by buildDescriptor() follow the BDK format: wpkh([fp/84'/1'/0']tpub.../0/*). They are directly importable into bdk-cli, Sparrow, Specter, or Core via importdescriptors.
  • Coin selection uses the BDK weight tables for input/output vsize estimation. The numbers match BDK's defaults for p2wpkh.
  • Change is emitted as a second output only when it exceeds SAFE_DUST_THRESHOLD_SATS (1000 sats). Anything smaller is absorbed into the fee to avoid creating uneconomic UTXOs.
  • Segwit (wpkh / sh-wpkh) and taproot (tr) descriptors are not emitted as of 0.4.0 — they will return when the corresponding address types are re-introduced.

What this package does NOT do

  • Persistence. Nothing is written to disk. Storing the mnemonic or encrypted wallet state is your application's concern.
  • UI. No React components, address QR rendering, or UX helpers.
  • Mempool watching. There's no subscription layer — sync() is a scantxoutset pull. Run it on demand or on a schedule.
  • RBF / CPFP. Transactions built by wallet.send() are marked opt-in RBF (RBF_SEQUENCE), but there is no bump-fee or cancel API yet; CPFP logic is also not provided and can be implemented using buildPsbt() and selectCoins().
  • Multi-sig / miniscript. Only single-sig wallets are supported today.
  • Other address types in the built-in signer. The internal HD keystore and wallet.send() builder are native-segwit p2wpkh only as of 0.4.0; p2sh-p2wpkh, p2pkh, and p2tr (taproot) will return in a future release. An external Xverse wallet can still sign its own segwit/taproot inputs via the browser entry.
  • Hardware wallets out of the box. The WalletProvider interface lets you wire one up. A browser-extension adapter (XverseProvider) is shipped; no USB/HID hardware-wallet adapter is.

Legacy P2PKH migration

0.3.0 switched the built-in HD signer from legacy P2PKH (m/44') to native segwit P2WPKH (m/84'). The same mnemonic now derives different addresses, so any balance still held on the old m/44'/… chain is invisible to sync(), balance, and send(). The coins are safe on-chain, but this library will not discover or spend them until you move them.

Before you upgrade a wallet that may have received legacy funds, scan the old chain and sweep anything you find:

import {
  scanLegacyP2pkh,
  assertNoStrandedLegacyFunds,
  sweepLegacy,
  getNetwork,
  createDefaultRpcClient,
} from "@tachibtc/taurus-wallet-aggregator";

const network = getNetwork("signet");
const rpc = createDefaultRpcClient({ url, username, password });

// 1) Detect — read-only, derives m/44' addresses for a gap-limit window.
const legacy = await scanLegacyP2pkh(mnemonic, network, rpc);
if (legacy.totalSats > 0n) {
  console.warn(`${legacy.totalSats} sats stranded on ${legacy.accountPath}`);
}

// 2) Guard — throws LEGACY_FUNDS_DETECTED if the legacy chain is non-empty.
//    Drop this into your upgrade / first-run path so funds can't silently strand.
await assertNoStrandedLegacyFunds(mnemonic, network, rpc);

// 3) Sweep — move everything to your new p2wpkh address, then upgrade.
const result = await sweepLegacy({
  mnemonic,
  network,
  rpc,
  destination: newP2wpkhAddress, // e.g. wallet.receiveAddress on 0.3.x+
  feeRate: 5,                     // sat/vB
});
console.log(`swept ${result.sweptSats} sats in ${result.txid}`);

Notes:

  • These helpers are an opt-in, temporary bridge. They re-derive P2PKH locally (BIP44 purpose 44) without re-adding p2pkh to the supported AddressType, so the main wallet, PSBT builder, and signer stay p2wpkh-only.
  • Pass { account, passphrase, gapLimit } if the legacy wallet used a non-default account, a BIP39 passphrase, or a wider gap.
  • sweepLegacy() verifies each fetched parent transaction against the scan result before signing (same prevout check as the p2wpkh builder), and wipes derived key material on a best-effort basis. Pass broadcast: false to get the signed hex back without sending it.

Vault user keys and legacy record recovery

Sweeping recovers stranded funds. Stranded identities are a separate problem: a vault whose user key was derived at m/44'/… before the SDK enforced BIP84 cannot be reconstructed from a post-cutover wallet, because m/44'/coin'/0'/0/i and m/84'/coin'/0'/0/i are different keys from the same mnemonic — and the vault record does not say which purpose produced it.

m/84' (p2wpkh) remains the only supported scheme. The daemon does not need to accept m/44'; what is needed is a way to identify the legacy records that already exist, and metadata that stops new ones from being ambiguous.

Creating a vault — persist the derivation, not just the pubkey

import {
  deriveUserKey,
  assertSupportedUserKeyScheme,
} from "@tachibtc/taurus-wallet-aggregator";

const userKey = deriveUserKey(mnemonic, network); // m/84'/coin'/0'/0/0
assertSupportedUserKeyScheme(userKey); // fail closed on anything but BIP84

// Store the WHOLE descriptor with the vault, not just userKey.publicKey:
// { version, scheme: "bip84-p2wpkh", purpose: 84, coinType, network, account,
//   change, index, path, publicKey, masterFingerprint, address, addressType }
await createVault({ userKey });

Recording only the pubkey is what made the legacy vaults unrecoverable. The descriptor pins scheme, path, network, and master fingerprint, so any future scheme change is a lookup rather than a search.

Recovering an existing vault — identify what produced its key

import {
  resolveUserKey,
  isLegacyUserKey,
} from "@tachibtc/taurus-wallet-aggregator";

// `vault.userPubkey` is all an old record has. Searches m/84' first, then the
// legacy m/44' chain, across receive + change.
const found = resolveUserKey(mnemonic, network, vault.userPubkey);

if (!found) {
  // Wrong mnemonic/passphrase/network/account, or an index past the window —
  // retry with { account, passphrase, count }.
} else if (isLegacyUserKey(found)) {
  // Stranded legacy vault: real, reproducible, created outside the supported
  // scheme. found.path is e.g. "m/44'/1'/0'/0/3" — use it to reconstruct the
  // vault, and migrate the user to a freshly created m/84' vault.
} else {
  // Correctly created — found.path is the m/84' path to record going forward.
}

Notes:

  • requireUserKey() is the throwing variant (USER_KEY_NOT_FOUND) for when a miss is a genuine error rather than a branch.
  • Both accept a compressed (66 hex chars) or x-only/BIP340 (64 hex chars) pubkey, with or without a 0x prefix — so a Taproot internal key recorded without its parity byte still resolves.
  • resolveUserKey() returns public material only and wipes the private bytes derived along the way; it grants no signing capability. Recovering the legacy balance is still sweepLegacy().
  • Search defaults: account 0, 20 indices per chain, receive + change, legacy included. Narrow or widen with { account, passphrase, count, includeChange, includeLegacy }.
  • assertSupportedUserKeyScheme() throws UNSUPPORTED_USER_KEY_SCHEME on a legacy descriptor. Call it on creation paths only — recovery paths expect legacy descriptors.
  • userKeyMatches(descriptor, pubkey) checks a recorded pubkey against a stored descriptor without re-deriving.

What this package IS ready for

  • Seed-phrase HD wallets — BIP39 mnemonic in, derived p2wpkh addresses and signed PSBTs out, on every built-in network.
  • Custom networks via registerNetwork — sidechains, custom signets, coin-type forks.
  • External providers via WalletProvider — wire up a hardware or extension wallet against the same Wallet API as the built-in keystore.
  • Browser dApps with Xverse or a seed phrase — the /browser entry connects an Xverse extension (signet) or imports a BIP39 mnemonic locally (signet + regtest), aggregates balance/UTXOs, signs externally-built PSBTs and BIP-322 messages, and ships a ready-made useTestnetWallet React hook with live chain switching.
  • Self-describing vault user keysderiveUserKey() records exactly which derivation produced a key, and resolveUserKey() recovers records that predate that metadata.
  • Custom RPC transports via RpcClient — the RPC layer is an interface. Swap in your own { call(method, params) } to proxy through a browser, hit an Electrum server, or mock in tests.
  • Deterministic test fixtures — every test uses the canonical BIP39 vector ("abandon … about"). Randomness is opt-in.

Testing

# Unit tests — fully offline, no bitcoind
npm run test

# Type-check the source
npm run typecheck

# Lint
npm run lint

Integration tests

Integration tests live in src/tests/integration.test.ts and are skipped by default. To run them, point the suite at a regtest node:

RUN_INTEGRATION_TESTS=1 \
BITCOIND_URL=http://127.0.0.1:18443 \
BITCOIND_USER=foo BITCOIND_PASS=bar \
npm run test

They exercise: funding via generatetoaddress, P2WPKH sends and sweeps, multi-account aggregator balance aggregation, gap-limit discovery with non-sequential funding, lock → send → WalletLockedError, and custom-network registration.

If RUN_INTEGRATION_TESTS=1 is set but bitcoind is unreachable, the suite prints a warning and still skips — it will not fail.


Regtest-first disclaimer

This package is exercised end-to-end against regtest. Derivation, descriptor generation, and PSBT signing also work for signet, but you should not treat it as battle-tested on live networks until you have validated it under your own conditions. Dry-run first. File issues liberally.


Publishing (GitHub Packages)

This package is published to GitHub Packages using a release triggered workflow.

Environment

  • CI runner: GitHub Actions (self-hosted)
  • Node.js: v20
  • Registry: https://npm.pkg.github.com
  • Scope: @tachibtc

Pre-requisites

  • NPM_PUBLISH_TOKEN must be configured in the repo's Actions Secrets (already set up for this repo).

    • Token must have write:packages permission.
  • Package name must be scoped:

    "name": "@tachibtc/taurus-wallet-aggregator"
  • .npmrc must point at the GitHub Packages registry (handled in CI by actions/setup-node via the registry-url input).

  • build and test scripts must pass — the workflow will refuse to publish if either fails.

How to publish

  1. Create a new GitHub Release (e.g. v1.0.0).
  2. The workflow at .github/workflows/publish.yml runs automatically on the release: published event.
  3. The package is built, tested, and published to GitHub Packages under the @tachibtc scope.

Installing a published version

Consumers need an .npmrc pointing the @tachibtc scope at GitHub Packages, plus a token with read:packages:

@tachibtc:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
npm install @tachibtc/taurus-wallet-aggregator

Releases

0.4.4

  • Automated release on version bump. A version bump landing on main now publishes to npm, then tags and creates the GitHub release (.github/workflows/release.yml). The tag follows the publish instead of triggering it, so a tag can no longer point at a commit that was never published.
  • Docs corrected. The env-var section linked to an "Unreleased" heading that no longer existed, and the WalletNetworkConfig samples still described rpc.rest as a configured host rather than a deprecated jsonRpc mirror.
  • No runtime or API changes.

0.4.2

  • TAURUS_SIGNET_REST / TAURUS_REGTEST_REST removed. They only ever duplicated TAURUS_SIGNET_RPC / TAURUS_REGTEST_RPC — the Tachi daemons are JSON-RPC proxies, not Esplora servers — and nothing in the data plane read the resulting rpc.rest. The field stays on RpcEndpoints (no type break) and now mirrors rpc.jsonRpc; setting the old env vars has no effect. Use TAURUS_SIGNET_ESPLORA to point at a real indexer.
    • Shipped as a patch because nothing in the package reads rpc.rest and the type is unchanged. One case needs a look before upgrading: if you set TAURUS_*_REST to a host different from TAURUS_*_RPC and read config.rpc.rest in your own code, it now silently returns the RPC host. Read rpc.esplora instead.

0.4.1

  • Republish of 0.4.0 (the 0.4.0 release never reached the registry — the tag was moved after the release was created, so the publish job aborted at checkout). No API changes; the 0.4.0 notes below describe everything in this release.

0.4.0

  • Breaking: TESTNET4 is gone. NetworkName narrowed to "regtest" | "testnet" | "signet" | "mainnet", and the built-in registry now holds signet (default) and regtest. getNetwork("testnet4") throws InvalidNetworkError; TAURUS_TESTNET4_* env vars are no longer read. Replacements: SIGNET, REGTEST, DEFAULT_WALLET_NETWORK, SUPPORTED_WALLET_NETWORKS, SUPPORTED_WALLET_CHAINS, getWalletNetwork(), and the new WalletChainName ("signet" | "regtest") type.
  • Breaking: no-derivation providers report null, not a sentinel. WalletProvider.accountPath / .accountXpub, DerivedKey.path, Utxo.derivationPath, and SignerInfo.accountPath / .accountXpub are now string | null. XverseProvider previously returned the sentinel "xverse:<network>" and "", which passed every truthiness guard and only failed once a consumer parsed it as a path. The new HdDerivedKey type (exported) narrows path back to string for everything the SDK derives locally — Keystore.deriveAddress(), deriveAddressFromAccountNode(), the seed import, and the migration scan all return it.
  • Breaking: the Xverse connector is signet-only. It defaults to BitcoinNetworkType.Signet and rejects any other chain up front rather than handshaking on the wrong one. New XVERSE_SUPPORTED_CHAINS / isXverseSupportedChain() let you gate a selector instead of catching a throw.
  • Seed-phrase import connector (src/wallets/seed.ts) — connectSeed, deriveSeedAddress, signPsbtSeed, seedAddressDescriptor, isSeedImportAvailable, isSeedSupportedChain, SEED_SUPPORTED_CHAINS. Keys are derived locally with BIP39/BIP84, so it is the only import path that covers both signet and regtest.
  • useTestnetWallet rewritten — adds importSeedPhrase(), switchNetwork(), walletType, chain, derivationPath, supportedChains, and chainsFor() (per-wallet-type chain gating). Chain switching on a seed wallet re-encodes the same account locally without re-prompting for the mnemonic; a generation counter drops stale async writes, and refreshBalance is single-flighted per generation.
  • Esplora-preferred data planeRpcEndpoints gains optional esplora and explorer. getUtxos() / broadcastTx() prefer a configured indexer (~0.4 s and mempool-aware, versus ~14 s for a signet scantxoutset) and fall back to the authoritative node on transport failure only; a malformed indexer response throws rather than degrading silently. New GetUtxosOptions / BroadcastTxOptions (preferEsplora, scanRetries).
  • scantxoutset lock-contention retry in the core RPC layerscanForUtxos() retries Bitcoin Core error -8 and scan timeouts with jittered exponential backoff. Tunable via the new scanRetry option on WalletOptions and AggregatorConfig, and ScanRetryOptions / ScanForUtxosOptions are exported.
  • Vault user keys (src/user-key.ts) — deriveUserKey() returns a self-describing UserKeyDescriptor (version, scheme, purpose, coin type, network, account, chain, index, path, pubkey, master fingerprint, address) to persist at vault creation. resolveUserKey() / requireUserKey() recover a vault recorded before that metadata existed by searching m/84' then legacy m/44'; isLegacyUserKey(), assertSupportedUserKeyScheme(), and userKeyMatches() round out the API. See Vault user keys.
  • deriveLegacyUserKeys() — re-derive the legacy m/44' account pubkeys for consumers that committed one as an identity (e.g. a Taproot internal key) before the 0.3.0 cutover.
  • Default endpoints moved to the hosted Tachi validators (rpc-signet / rpc-regtest.tachibtc.com), with TAURUS_SIGNET_ESPLORA (legacy name TAURUS_SIGNET_ELECTRS still honoured) and TAURUS_SIGNET_EXPLORER added. Every URL is validated by requireSecureUrl.
  • verifyBip322Signature() resolves the address encoding directly rather than through the network registry, so an expectedNetwork of "mainnet" or "testnet" still binds correctly even though neither is a registered network.
  • Test suite grew to 383 passing tests across 19 offline files (plus the opt-in regtest integration suite), covering the seed connector, user keys, the Esplora path, and the hook.

0.3.0

  • Breaking: the built-in HD signer switched from legacy p2pkh to native segwit p2wpkh (BIP84). Derivation now uses m/84'/coin'/account', buildDescriptor() emits wpkh(...), and receive/change addresses are bech32 (bc1q… / tb1q… / bcrt1q…). Funds on old m/44' p2pkh addresses are not visible under the new derivation — sweep them out before upgrading. New scanLegacyP2pkh() / assertNoStrandedLegacyFunds() / sweepLegacy() helpers detect and migrate stranded legacy funds; see Legacy P2PKH migration.
  • Breaking: AddressType is now "p2wpkh"; p2pkh was removed and will be re-introduced (alongside p2sh-p2wpkh and p2tr) in a future release.
  • Breaking: wallet.send() now accepts only p2wpkh destination addresses; non-p2wpkh destinations throw InvalidAddressError.
  • buildPsbt() now attaches segwit witnessUtxo (script + value) instead of nonWitnessUtxo. The parent transaction is still fetched and verified so a tampered scanner result cannot misstate an input before it is signed.
  • Coin-selection vsize tables updated to segwit weights (input 68 vB, output 31 vB, +0.5 vB witness marker/flag overhead).
  • Browser / React entry (@tachibtc/taurus-wallet-aggregator/browser): Xverse connector (connectXverse, signPsbtXverse, signMessageXverse, isXverseInstalled), XverseProvider, and the useTestnetWallet hook. Adds react as an optional peer dependency and sats-connect as a dependency.
  • External wallet signingprepareForWalletSigning() / WalletSigningRequest serialize an unsigned PSBT for browser wallets; WalletProvider.supportsInternalSend gates the internal send() builder.
  • BIP-322 verificationverifyBip322Signature() (P2WPKH / P2TR simple).
  • signet / regtest gateways — built-in SIGNET (default) and REGTEST network configs pointed at the tachibtc validator nodes (rpc-signet / rpc-regtest.tachibtc.com), with TAURUS_SIGNET_RPC / TAURUS_SIGNET_REST / TAURUS_REGTEST_RPC / TAURUS_REGTEST_REST env overrides; plus a fetch-based gateway RPC adapter (rpcCall, getUtxos, broadcastTx) built on the node's JSON-RPC proxy (scantxoutset for balance, sendrawtransaction for broadcast). The import-wallet flow (connect / switchNetwork) is limited to these two chains, both p2wpkh.

0.2.1

  • Republish of 0.2.0 (the 0.2.0 version conflicted on the registry). No API changes.

0.2.0

  • Breaking: address types narrowed to legacy p2pkh only.
  • Breaking: buildPsbt() verifies parent tx and prevout against the scanner-reported UTXO; mismatches throw PsbtError at build time.
  • Breaking: RpcError gains method / rpcCode; RpcTransportError gains httpStatus.
  • Serialized wallet.send(), opt-in RBF, and local UTXO pruning after broadcast.
  • Hardened keystore seed handling and aggregator lifecycle.

Project Structure