Universal Bitcoin signer library with branded types and a pluggable CryptoBackend. Written in TypeScript with zero CommonJS. Ships with a pure-JS backend (@noble/curves) and a legacy adapter for tiny-secp256k1.
What is ecpair?
@btc-vision/ecpairprovides secp256k1 key management, ECDSA signing, BIP-340 Schnorr signing, Taproot-style key tweaking, and WIF import/export. It is designed for use with@btc-vision/bitcoinand the broader OPNet ecosystem, but works with any Bitcoin library that consumes standard key types.
Why branded types?
PrivateKey,PublicKey,XOnlyPublicKey,Signature, and other key types are nominal (branded)Uint8Arraysubtypes. This prevents accidentally passing a raw hash where a private key is expected, or mixing up compressed and x-only public keys. Mistakes are caught at compile time, not at runtime in production.
Why pluggable backends?
CryptoBackendis an interface. The library ships two implementations:
NobleBackend: pure JavaScript via@noble/curves/secp256k1, zero native dependenciesLegacyBackend: adapter for existingtiny-secp256k1installations (WASM or ASM.js)Swap backends without changing application code.
No hardcoded networks
The library does not ship Bitcoin, testnet, or regtest constants. Consumers must provide a
Networkobject to every factory method. This keeps the library network-agnostic and avoids accidental mainnet usage in test environments.
npm install @btc-vision/ecpair
Requires Node.js >= 24.0.0. The package is ESM-only ("type": "module").
import {
ECPairSigner,
createNobleBackend,
createPrivateKey,
createMessageHash,
verifyCryptoBackend,
} from '@btc-vision/ecpair';
import type { Network } from '@btc-vision/ecpair';
// Define your network
const bitcoin: Network = {
messagePrefix: '\x18Bitcoin Signed Message:\n',
bech32: 'bc',
bech32Opnet: 'op',
bip32: { public: 0x0488b21e, private: 0x0488ade4 },
pubKeyHash: 0x00,
scriptHash: 0x05,
wif: 0x80,
};
// Create backend and verify integrity
const backend = createNobleBackend();
verifyCryptoBackend(backend);
// Generate a random signer (FIPS 186-5 B.4.2 key generation)
const signer = ECPairSigner.makeRandom(backend, bitcoin);
console.log(signer.toWIF());
// Sign and verify
const hash = createMessageHash(new Uint8Array(32));
const sig = signer.sign(hash);
console.log(signer.verify(hash, sig)); // true
// Schnorr (BIP-340)
const schnorrSig = signer.signSchnorr(hash);
console.log(signer.verifySchnorr(hash, schnorrSig)); // true
| Old API (v3) | New API (v4) |
|---|---|
ECPairFactory(tinysecp) |
createNobleBackend() or createLegacyBackend(tinysecp) |
ECPair.makeRandom() |
ECPairSigner.makeRandom(backend, network) |
ECPair.fromPrivateKey(buf, opts) |
ECPairSigner.fromPrivateKey(backend, privateKey, network) |
ECPair.fromPublicKey(buf, opts) |
ECPairSigner.fromPublicKey(backend, publicKey, network) |
ECPair.fromWIF(str, network) |
ECPairSigner.fromWIF(backend, str, network) |
keyPair.network (optional) |
signer.network (always set, required parameter) |
{ network } in options |
Separate network parameter on every factory method |
Set<SignerCapability> |
number bitmask of SignerCapability flags |
const signer = ECPairSigner.fromWIF(backend, 'KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn', bitcoin);
console.log(signer.compressed); // true
console.log(signer.toWIF());
const signer = ECPairSigner.fromPrivateKey(
backend,
createPrivateKey(new Uint8Array(32).fill(1)),
bitcoin,
);
import { createPublicKey } from '@btc-vision/ecpair';
const pubOnly = ECPairSigner.fromPublicKey(backend, createPublicKey(pubKeyBytes), bitcoin);
console.log(pubOnly.privateKey); // undefined
console.log(pubOnly.verify(hash, sig)); // true
import type { Bytes32 } from '@btc-vision/ecpair';
const tweakScalar = new Uint8Array(32).fill(2) as Bytes32;
const tweaked = signer.tweak(tweakScalar);
console.log(tweaked.toWIF());
import { createLegacyBackend } from '@btc-vision/ecpair';
import type { TinySecp256k1Interface } from '@btc-vision/ecpair';
import * as tinysecp from 'tiny-secp256k1';
const legacy = createLegacyBackend(tinysecp as unknown as TinySecp256k1Interface);
const kp = ECPairSigner.makeRandom(legacy, bitcoin);
import { randomBytes } from 'node:crypto';
const kp = ECPairSigner.makeRandom(backend, bitcoin, {
rng: (size: number) => new Uint8Array(randomBytes(size).buffer),
});
The rng function receives 48 bytes (FIPS 186-5 seed length) and must return exactly size bytes.
const testnet: Network = {
messagePrefix: '\x18Bitcoin Signed Message:\n',
bech32: 'tb',
bech32Opnet: 'opt',
bip32: { public: 0x043587cf, private: 0x04358394 },
pubKeyHash: 0x6f,
scriptHash: 0xc4,
wif: 0xef,
};
// fromWIF accepts an array of candidate networks
const kp = ECPairSigner.fromWIF(backend, wifString, [bitcoin, testnet]);
console.log(kp.network === bitcoin); // true if mainnet WIF
import { SignerCapability } from '@btc-vision/ecpair';
const kp = ECPairSigner.makeRandom(backend, bitcoin);
if (kp.capabilities & SignerCapability.SchnorrSign) {
console.log('Schnorr signing available');
}
// Or use the convenience method
kp.hasCapability(SignerCapability.EcdsaSign); // true
kp.hasCapability(SignerCapability.PrivateKeyExport); // true
import { encodeWIF, decodeWIF, createPrivateKey } from '@btc-vision/ecpair';
const wif = encodeWIF(createPrivateKey(keyBytes), true, bitcoin);
const decoded = decodeWIF(wif, bitcoin);
// decoded.privateKey, decoded.compressed, decoded.network
Visit our API documentation generated by TypeDoc.
npm test
npm run lint
npm run lint:tests
npm run format:ci
npm testSee CONTRIBUTING.md for details.