import * as bitcoin from 'bitcoinjs-lib'; import { Network, Psbt } from 'bitcoinjs-lib'; import { BIP32Interface } from 'bip32'; type AddressType = "p2wpkh"; type NetworkName = "regtest" | "testnet" | "signet" | "mainnet"; type WalletChainName = "signet" | "regtest"; interface NetworkConfig { network: Network; coinType: number; bech32: string; name: string; } interface RpcEndpoints { jsonRpc: string; rest: string; esplora?: string; fallback?: string; explorer?: string; } interface WalletNetworkConfig extends NetworkConfig { rpc: RpcEndpoints; bitcoinjsNetwork: Network; } interface WalletConfig { mnemonic: string; network?: string; addressType: AddressType; accountIndex?: number; addressIndex?: number; } interface DerivedKey { readonly index: number; readonly change: boolean; readonly path: string | null; readonly address: string; readonly scriptPubKey: string; readonly publicKey: string; } /** * A `DerivedKey` that came from real BIP32 derivation, so `path` is always a * concrete path. Everything the SDK derives locally — the keystore, the seed * import, the legacy-migration scan — returns this. Only an external * single-address provider widens to `DerivedKey`, where `path` may be null. */ interface HdDerivedKey extends DerivedKey { readonly path: string; } interface Utxo { readonly txid: string; readonly vout: number; readonly valueSats: bigint; readonly address: string; readonly scriptPubKey: string; readonly height: number; readonly confirmations: number; readonly coinbase: boolean; readonly derivationPath: string | null; readonly change: boolean; readonly addressIndex: number; } interface WalletBalance { confirmed: bigint; unconfirmed: bigint; total: bigint; } interface WalletInfo { address: string; balance: WalletBalance; network: string; addressType: AddressType; accountPath: string | null; accountXpub: string | null; receiveDescriptor: string; changeDescriptor: string; } interface SendOptions { to: string; amount: bigint; feeRate: number; } interface BroadcastResult { txid: string; } interface SignerInfo { readonly fingerprint: string; readonly accountPath: string | null; readonly accountXpub: string | null; readonly addressType: AddressType; readonly canSign: boolean; } declare function getNetwork(name: string): NetworkConfig; declare function registerNetwork(name: string, config: NetworkConfig): void; declare function unregisterNetwork(name: string): void; declare function listNetworks(): string[]; declare function isAddressOnNetwork(address: string, name: string): boolean; declare function validateAddress(address: string, name: string): void; declare const SIGNET: WalletNetworkConfig; declare const REGTEST: WalletNetworkConfig; declare const SUPPORTED_WALLET_NETWORKS: Record; declare const SUPPORTED_WALLET_CHAINS: WalletChainName[]; declare const DEFAULT_WALLET_NETWORK: WalletNetworkConfig; declare function getWalletNetwork(name: WalletChainName): WalletNetworkConfig; /** * Thin, dependency-free RPC adapter for the hosted Tachi Bitcoin gateways. * * These helpers are intentionally decoupled from the existing * `src/rpc/` Bitcoin Core client: they target the gateway shape used by * {@link WalletNetworkConfig} and work with any network object exposing those * endpoints — SIGNET, REGTEST, or any future network defined the same way. * * TWO BACKENDS, one API * --------------------- * `getUtxos` / `broadcastTx` can talk to either of two very different servers, * and pick per network based on what that network configures: * * 1. **Esplora/Electrs indexer** (`rpc.esplora`) — PREFERRED when set. Signet * points at Xverse's public signet indexer. An indexer answers an address * query from a prebuilt index: ~0.4s vs ~14s, no node-global lock, and it * sees the MEMPOOL, so `status.confirmed` carries real information. * * 2. **Bitcoin Core JSON-RPC 1.0 proxy** (`rpc.jsonRpc`) — the authoritative * node, and the automatic fallback. The hosted Tachi daemons * (rpc-signet / rpc-regtest.tachibtc.com) are NOT Esplora servers; they * expose this proxy at `POST /`. Here `getUtxos` scans the on-chain UTXO * set via `scantxoutset` and `broadcastTx` submits via * `sendrawtransaction`. Regtest uses this path exclusively — its chain is * small enough that a full scan is already fast. * * Both reflect the wallet's real L1 balance (not the daemon's L2/VTXO balance), * and for signet both serve the same public signet chain. * * FALLBACK BOUNDARY: only a TRANSPORT failure from the indexer (connection * error, timeout, HTTP status, non-JSON body) falls through to the node. A * malformed indexer *body* throws instead — falling back on bad data would let * a tampered or broken indexer hide behind the node rather than surface as a * wrong balance. A broadcast rejection (4xx) is likewise reported, not retried. * * All methods on both backends are unauthenticated — no API key is sent. * * Uses the global `fetch` (Node >= 18 and all browsers). */ type NetworkWithRpc = Pick; interface UtxoStatus { confirmed: boolean; block_height?: number; block_hash?: string; block_time?: number; } interface RestUtxo { txid: string; vout: number; value: number; status: UtxoStatus; } declare function rpcCall(network: NetworkWithRpc, method: string, params?: readonly unknown[]): Promise; interface GetUtxosOptions { scanRetries?: number; preferEsplora?: boolean; } /** * Fetch the unspent outputs for an address. * * Two paths, in preference order: * * 1. `network.rpc.esplora`, when one is configured (signet is) and * `options.preferEsplora !== false`. An indexer answers in ~0.4s, takes no * node-global lock, and sees the MEMPOOL — so unconfirmed outputs come back * with `status.confirmed === false`. * 2. The node's `scantxoutset` JSON-RPC (the Tachi daemon is not Esplora), * which is authoritative and the automatic fallback when the indexer is * unreachable. It does NOT see the mempool, so every UTXO it returns is * reported as confirmed. * * Only a TRANSPORT failure from the indexer falls back. A malformed indexer * response throws, so a broken or tampered indexer surfaces rather than hiding * behind the node. * * Because `scantxoutset` holds a node-global lock (see the retry note above), a * concurrent scan on the shared gateway can make path 2 transiently fail; the * call retries such failures with backoff before throwing. Retries are bounded * by `options.scanRetries`. * * TRUST BOUNDARY: the returned values are only validated for *shape and range*, * not *authenticity*. The hosted gateway is trusted to report the correct UTXO * set. This is acceptable for the display-only balance path * ({@link useTestnetWallet}'s `refreshBalance`), but any caller that uses these * UTXOs to BUILD or SIGN a transaction MUST re-verify each input's value and * scriptPubKey against its parent transaction first — see `verifyParentOutput` * in `src/psbt.ts`, which defends the Core-RPC signing path against exactly this * tampering. Do not trust `value` from this function for fee or coin-selection * decisions without that check. */ declare function getUtxos(network: NetworkWithRpc, address: string, options?: GetUtxosOptions): Promise; interface BroadcastTxOptions { preferEsplora?: boolean; } /** * Broadcast a raw transaction hex and return the txid. * * Uses the Esplora indexer's `POST /tx` when one is configured, falling back to * the node's `sendrawtransaction` JSON-RPC if the indexer is unreachable. Both * ultimately hand the transaction to the same bitcoind on the same chain. * * A REJECTION (bad signature, insufficient fee, double spend) is returned as a * hard error from whichever path saw it — it is not retried against the other, * since the second node would reject it identically and a spurious retry could * mask the real reason. * * The hosted Tachi daemon accepts `sendrawtransaction` without authentication, * so no API key is required. */ declare function broadcastTx(network: NetworkWithRpc, txHex: string, options?: BroadcastTxOptions): Promise; /** * BIP-322 "simple" signature verification for P2WPKH and P2TR addresses. * * NOTE: `bitcoinjs-message` only implements the legacy "Bitcoin Signed Message" * (ECDSA) scheme — it does NOT support BIP-322. Xverse returns BIP-322 * signatures, so this module implements the BIP-322 *simple* * verification manually with bitcoinjs-lib, following the toSpend / toSign * virtual-transaction construction from the BIP: * * 1. message_hash = tagged_hash("BIP0322-signed-message", message) * 2. toSpend: a 0-fee tx whose single input commits to message_hash and * whose single output pays the address' scriptPubKey. * 3. toSign: spends toSpend[0] carrying the provided witness, outputs * an OP_RETURN. Its sighash is what the wallet actually signed. * * Verification then checks the witness signature against that sighash and * confirms the key commits to the address. */ interface VerifyResult { valid: boolean; reason?: string; network?: NetworkName; } /** * Verify a BIP-322 simple signature for a P2WPKH or P2TR address. * * @param address the signer's address (P2WPKH `bc1q…` / P2TR `bc1p…`) * @param message the signed message (UTF-8) * @param signatureBase64 base64-encoded witness stack (as returned by wallets) * @param expectedNetwork when set, the address must parse on this network or * verification fails — bind this for proof-of-ownership * use cases so a signature over an address on a * different network is not silently accepted. The * resolved network is echoed back on `VerifyResult.network`. */ declare function verifyBip322Signature(address: string, message: string, signatureBase64: string, expectedNetwork?: NetworkName): VerifyResult; /** * BIP39/32/84 HD key derivation (p2wpkh only). * * Security notes: * - Seed bytes are held in a private field on `Keystore`, hidden from * JSON.stringify, structured clones, and util.inspect. * - `lock()` is best-effort memory hygiene: it zeros the seed and attempts * to zero the private key bytes on the account node before dropping the reference. * - Intermediate seed buffers created during construction are zeroed as * soon as they are no longer needed. * - Public derivations expose only pubkeys and scriptPubKeys; * `signerFor()` is package-internal and used only by `psbt.ts`. */ declare function generateMnemonic(strength?: 128 | 160 | 192 | 224 | 256): string; declare function validateMnemonic(mnemonic: string): boolean; declare function normalizeMnemonic(mnemonic: string): string; declare function buildAccountPath(addressType: AddressType, coinType: number, account: number): string; declare function buildAddressPath(addressType: AddressType, coinType: number, account: number, change: boolean, index: number): string; declare function deriveAddressFromAccountNode(accountNode: BIP32Interface, addressType: AddressType, networkConfig: NetworkConfig, account: number, change: boolean, index: number): HdDerivedKey; declare class Keystore { #private; private constructor(); static fromMnemonic(mnemonic: string, passphrase: string, network: NetworkConfig, addressType: AddressType, account: number): Keystore; static fromSeedBytes(seed: Uint8Array, network: NetworkConfig, addressType: AddressType, account: number): Keystore; private static fromSeedBuffer; get fingerprint(): string; get accountPath(): string; get accountXpub(): string; get addressType(): AddressType; get account(): number; get network(): NetworkConfig; get locked(): boolean; watchOnlyAccountNode(): BIP32Interface; deriveAddress(change: boolean, index: number): HdDerivedKey; signerFor(change: boolean, index: number): BIP32Interface; lock(): void; toJSON(): Record; } declare function buildDescriptor(addressType: AddressType, fingerprint: string, accountPath: string, accountXpub: string, change: boolean): string; interface RpcConfig { url: string; username?: string; password?: string; authHeader?: string; fetchImpl?: typeof fetch; timeoutMs?: number; requestInit?: Omit; } interface RpcClient { call(method: string, params?: readonly unknown[]): Promise; } interface ScantxoutsetResult { success: boolean; txouts: number; height: number; bestblock: string; unspents: ScantxoutsetUnspent[]; total_amount: number; } interface ScantxoutsetUnspent { txid: string; vout: number; scriptPubKey: string; desc?: string; amount: number; height: number; coinbase?: boolean; } interface RawTransaction { txid: string; hash: string; version: number; size: number; vsize: number; weight: number; locktime: number; vin: RawVin[]; vout: RawVout[]; hex?: string; blockhash?: string; confirmations?: number; time?: number; blocktime?: number; } interface RawVin { txid?: string; vout?: number; coinbase?: string; sequence: number; txinwitness?: string[]; scriptSig?: { asm: string; hex: string; }; } interface RawVout { value: number; n: number; scriptPubKey: { asm: string; hex: string; address?: string; addresses?: string[]; type: string; }; } interface MempoolInfo { loaded: boolean; size: number; bytes: number; usage: number; mempoolminfee: number; minrelaytxfee: number; } interface BlockchainInfo { chain: string; blocks: number; headers: number; bestblockhash: string; difficulty: number; mediantime: number; verificationprogress: number; initialblockdownload: boolean; pruned: boolean; } /** * PSBT construction, signing, and finalization (p2wpkh only). * * p2wpkh inputs are witness-based: the prevout (scriptPubKey + value) is * supplied as `witnessUtxo`. The full parent tx is still fetched via RPC and * verified, so a tampered scanner result cannot misstate an input's value or * script before it is signed over. * * Per address type (for the types returning in a future release): * p2pkh - non-witness; requires full parent tx as `nonWitnessUtxo`. * p2sh-p2wpkh - witness; also needs redeem script. * p2wpkh - witness only. * p2tr - taproot key-path; signer tweaked per BIP86 before schnorr signing. */ declare const RBF_SEQUENCE = 4294967293; interface BuildPsbtArgs { network: bitcoin.networks.Network; inputs: readonly Utxo[]; outputs: readonly { address: string; valueSats: bigint; }[]; inputAddressType: AddressType; rpc: RpcClient; } declare function buildPsbt(args: BuildPsbtArgs): Promise; interface SignPsbtArgs { psbt: bitcoin.Psbt; inputs: readonly Utxo[]; keystore: Keystore; } declare function signPsbt(args: SignPsbtArgs): void; declare function extractTransaction(psbt: bitcoin.Psbt): { txid: string; hex: string; vsize: number; }; interface WalletSigningRequest { psbtBase64: string; psbtHex: string; inputsToSign: { address: string; signingIndexes: number[]; }[]; } declare function prepareForWalletSigning(psbt: bitcoin.Psbt, signerAddress: string): WalletSigningRequest; /** * Vault user keys: derivation metadata that makes a recorded pubkey * self-describing, plus recovery for records created before that metadata * existed. * * ## The problem this closes * * `m/84'` (p2wpkh, BIP84) is the only supported scheme — the daemon's contract * is correct as written and does not need to accept `m/44'`. But an earlier * client flow created vaults from `m/44'` (p2pkh, BIP44) derived keys, before * the SDK enforced the scheme. Those vault records are still on file, and the * current SDK cannot reconstruct them: `AddressType` is p2wpkh-only, so * `Keystore.fromMnemonic()` can no longer reach purpose 44, and nothing in the * record says which purpose produced its user key. That is a legacy-data * problem, not a daemon-acceptance one. * * ## The two halves of the fix * * 1. **Recovery** — `resolveUserKey()` takes the pubkey a vault already * recorded and searches the current `m/84'` chain first, then the legacy * `m/44'` chain, and reports which scheme, path, and index produced it. The * legacy key is still derivable locally from the mnemonic, so a stranded * vault can be identified and reconstructed even though the signer is * p2wpkh-only. * * 2. **Prevention** — `deriveUserKey()` returns a `UserKeyDescriptor`: * scheme, purpose, coin type, account, chain, index, full path, master * fingerprint, and the pubkey itself. Vault creation should persist this * whole record alongside the vault, so no future record is ambiguous about * the derivation that produced it. `assertSupportedUserKeyScheme()` is the * fail-closed guard for creation paths: it rejects a legacy-scheme * descriptor outright. * * No signing capability is returned anywhere in this module — descriptors carry * public material only, and private bytes derived along the way are wiped * before returning. Stranded *funds* are a separate concern: see * `scanLegacyP2pkh()` / `sweepLegacy()` in migration.ts. */ /** * Schema version of a persisted `UserKeyDescriptor`. Bump when the record shape * changes so consumers can migrate stored vault metadata deterministically. */ declare const USER_KEY_RECORD_VERSION = 1; /** * Derivation schemes a vault user key can have been created under. * - `bip84-p2wpkh` — the only supported scheme; what new vaults must use. * - `bip44-p2pkh` — legacy, recovery-only. Vaults created before the SDK * enforced BIP84. Never valid for new vaults. */ type UserKeyScheme = "bip84-p2wpkh" | "bip44-p2pkh"; declare const CURRENT_USER_KEY_SCHEME: "bip84-p2wpkh"; declare const LEGACY_USER_KEY_SCHEME: "bip44-p2pkh"; /** * The full derivation record for a vault's user key. Persist ALL of it at vault * creation — the pubkey alone is what made the legacy records unrecoverable. */ interface UserKeyDescriptor { readonly version: number; readonly scheme: UserKeyScheme; readonly purpose: number; readonly coinType: number; readonly network: string; readonly account: number; readonly change: boolean; readonly index: number; readonly path: string; readonly publicKey: string; readonly masterFingerprint: string; readonly address: string; readonly addressType: AddressType | "p2pkh"; } interface DeriveUserKeyOptions { account?: number; passphrase?: string; change?: boolean; index?: number; } interface ResolveUserKeyOptions { account?: number; passphrase?: string; count?: number; includeChange?: boolean; includeLegacy?: boolean; } /** * Derive a vault user key under the supported scheme (`m/84'`, p2wpkh) and * return the full derivation record to persist with the vault. * * This is what vault creation should call. Store the returned descriptor — * every field of it — so the vault is never ambiguous about which key and which * derivation produced it. Public material only; the keystore is locked before * returning. */ declare function deriveUserKey(mnemonic: string, network: NetworkConfig, opts?: DeriveUserKeyOptions): UserKeyDescriptor; /** * Identify which derivation produced a user key a vault already recorded. * * Searches the supported `m/84'` chain first, then — unless `includeLegacy` is * false — the legacy `m/44'` chain, over `count` indices of the receive chain * and (by default) the change chain. Returns the reconstructed descriptor, or * null when no derivation in the searched window matches. * * A non-null result with `scheme === "bip44-p2pkh"` is the diagnosis for a * stranded legacy vault: the record is real and reproducible from this * mnemonic, but was created outside the supported scheme. Use it to reconstruct * the vault; do NOT feed it to a creation path (see * `assertSupportedUserKeyScheme()`). */ declare function resolveUserKey(mnemonic: string, network: NetworkConfig, publicKey: string, opts?: ResolveUserKeyOptions): UserKeyDescriptor | null; /** * `resolveUserKey()` that throws instead of returning null. Use when a vault * record is known to belong to this mnemonic and a miss is a genuine error * (wrong mnemonic, wrong passphrase, wrong network, or an index beyond the * search window). */ declare function requireUserKey(mnemonic: string, network: NetworkConfig, publicKey: string, opts?: ResolveUserKeyOptions): UserKeyDescriptor; /** True when the descriptor came from the legacy, unsupported m/44' scheme. */ declare function isLegacyUserKey(descriptor: UserKeyDescriptor): boolean; /** * Fail-closed guard for vault CREATION paths: throws unless the descriptor was * produced by the supported `m/84'` scheme. Recovery paths should not call this * — a legacy descriptor is exactly what they expect to handle. */ declare function assertSupportedUserKeyScheme(descriptor: UserKeyDescriptor): void; /** * Check a recorded pubkey against a descriptor without re-deriving. Accepts the * same compressed or x-only forms as `resolveUserKey()`. */ declare function userKeyMatches(descriptor: UserKeyDescriptor, publicKey: string): boolean; declare class WalletError extends Error { readonly code: string; constructor(code: string, message: string); } declare class InvalidMnemonicError extends WalletError { constructor(message?: string); } declare class InvalidNetworkError extends WalletError { constructor(network: string); } declare class InvalidAddressError extends WalletError { constructor(address: string, reason?: string); } declare class InvalidAmountError extends WalletError { constructor(message?: string); } declare class InsufficientFundsError extends WalletError { readonly available: bigint; readonly required: bigint; constructor(available: bigint, required: bigint); } declare class WalletLockedError extends WalletError { constructor(); } declare class RpcError extends WalletError { readonly method: string; readonly rpcCode: number | undefined; constructor(method: string, message: string, rpcCode?: number); } declare class RpcTransportError extends WalletError { readonly httpStatus: number | undefined; constructor(message: string, httpStatus?: number); } declare class FeeRateError extends WalletError { constructor(message?: string); } declare class PsbtError extends WalletError { constructor(message: string); } interface WalletProvider { readonly name: string; readonly network: NetworkConfig; readonly addressType: AddressType; readonly account: number; readonly fingerprint: string; readonly accountPath: string | null; readonly accountXpub: string | null; readonly locked: boolean; readonly supportsInternalSend?: boolean; deriveReceiveAddresses(count: number): DerivedKey[]; deriveChangeAddresses(count: number): DerivedKey[]; signPsbt(psbt: Psbt, inputs: readonly Utxo[]): Promise | void; buildDescriptor(change: boolean): string; getSignerInfo(): SignerInfo; lock(): void; } export { type BroadcastResult as $, type AddressType as A, type BroadcastTxOptions as B, requireUserKey as C, type DerivedKey as D, resolveUserKey as E, rpcCall as F, type GetUtxosOptions as G, userKeyMatches as H, InvalidAddressError as I, validateMnemonic as J, Keystore as K, verifyBip322Signature as L, type RpcClient as M, type NetworkConfig as N, type RpcConfig as O, PsbtError as P, type BlockchainInfo as Q, REGTEST as R, type SignerInfo as S, type MempoolInfo as T, type Utxo as U, type VerifyResult as V, type WalletChainName as W, type RawTransaction as X, type WalletBalance as Y, type WalletInfo as Z, type SendOptions as _, type WalletNetworkConfig as a, type BuildPsbtArgs as a0, CURRENT_USER_KEY_SCHEME as a1, type DeriveUserKeyOptions as a2, FeeRateError as a3, type HdDerivedKey as a4, InsufficientFundsError as a5, InvalidAmountError as a6, LEGACY_USER_KEY_SCHEME as a7, type NetworkName as a8, RBF_SEQUENCE as a9, type ResolveUserKeyOptions as aa, RpcError as ab, RpcTransportError as ac, type ScantxoutsetResult as ad, type ScantxoutsetUnspent as ae, type SignPsbtArgs as af, USER_KEY_RECORD_VERSION as ag, type UserKeyScheme as ah, type WalletConfig as ai, assertSupportedUserKeyScheme as aj, buildPsbt as ak, extractTransaction as al, getNetwork as am, isAddressOnNetwork as an, isLegacyUserKey as ao, listNetworks as ap, registerNetwork as aq, signPsbt as ar, unregisterNetwork as as, validateAddress as at, type WalletProvider as b, DEFAULT_WALLET_NETWORK as c, InvalidMnemonicError as d, InvalidNetworkError as e, type RestUtxo as f, type RpcEndpoints as g, SIGNET as h, SUPPORTED_WALLET_CHAINS as i, SUPPORTED_WALLET_NETWORKS as j, type UserKeyDescriptor as k, type UtxoStatus as l, WalletError as m, WalletLockedError as n, type WalletSigningRequest as o, broadcastTx as p, buildAccountPath as q, buildAddressPath as r, buildDescriptor as s, deriveAddressFromAccountNode as t, deriveUserKey as u, generateMnemonic as v, getUtxos as w, getWalletNetwork as x, normalizeMnemonic as y, prepareForWalletSigning as z };