A simple JavaScript library for deriving BIP-352 Silent Payment addresses, scan keys, and spend keys. It also implements the spscan/spspend key expressions from BIP-392. It is designed to work well as a library module alongside other bitcoinjs suite of libraries.
Silent Payments is a privacy-enhancing protocol that allows users to receive Bitcoin payments to a static, reusable address without revealing it on-chain. Each payment generates a unique output address, improving receiver privacy.
npm install bip352-jsconst bip39 = require('bip39');
const bip32 = require('bip32');
const ecc = require('tiny-secp256k1');
const bip352 = require('bip352-js');
const BIP32 = bip32.BIP32Factory(ecc);
// Generate or import a mnemonic
const mnemonic = bip39.generateMnemonic();
const seed = bip39.mnemonicToSeedSync(mnemonic);
const root = BIP32.fromSeed(seed);
// Derive Silent Payment address for account 0
const { address, scanKey, spendKey } = bip352.deriveAddress(
root,
0,
bip352.COIN_TYPE_BTC_MAINNET
);
console.log('Silent Payment Address:', address);
// => sp1qq...const { scanKey, spendKey } = bip352.deriveKeys(
root,
0,
bip352.COIN_TYPE_BTC_MAINNET
);
console.log('Scan key path:', bip352.getPath('scan', 0, bip352.COIN_TYPE_BTC_MAINNET));
// => m/352'/0'/0'/1'/0
console.log('Spend key path:', bip352.getPath('spend', 0, bip352.COIN_TYPE_BTC_MAINNET));
// => m/352'/0'/0'/0'/0const address = bip352.toAddress(
scanKey.publicKey,
spendKey.publicKey,
bip352.COIN_TYPE_BTC_MAINNET
);
console.log('Address:', address);const decoded = bip352.fromAddress(address);
console.log('Version:', decoded.version);
console.log('Scan pubkey:', Buffer.from(decoded.scanPub).toString('hex'));
console.log('Spend pubkey:', Buffer.from(decoded.spendPub).toString('hex'));
console.log('Testnet:', decoded.testnet);const { address } = bip352.deriveAddress(
root,
0,
bip352.COIN_TYPE_BTC_TESTNET
);
console.log('Testnet Address:', address);
// => tsp1qq...BIP-392 defines two key expressions for use in the sp() output script descriptor. An spscan expression carries the scan private key with the spend public key, which lets a watch-only wallet detect incoming payments without being able to spend them. An spspend expression carries both private keys, for a wallet that can do both.
const { scanKey, spendKey } = bip352.deriveKeys(root, 0, bip352.COIN_TYPE_BTC_MAINNET);
// Watch-only: can scan, cannot spend
const scanExpr = bip352.toScanKeyExpression(
scanKey.privateKey,
spendKey.publicKey,
bip352.COIN_TYPE_BTC_MAINNET
);
// => spscan1q0rnl6lft0gkpg4nsn528qgdpytfdej40atdqgrxpqqsg8c5r8vys97...
// Full wallet: can scan and spend
const spendExpr = bip352.toSpendKeyExpression(
scanKey.privateKey,
spendKey.privateKey,
bip352.COIN_TYPE_BTC_MAINNET
);
// => spspend1q0rnl6lft0gkpg4nsn528qgdpytfdej40atdqgrxpqqsg8c5r8vyu3p...
const { scanPriv, spendPub, testnet } = bip352.fromScanKeyExpression(scanExpr);Note: Both expressions encode private key material. Treat them like a seed — an
spscanexpression leaks full transaction history to anyone holding it, and anspspendexpression allows spending.
Derives scan and spend keys according to BIP-352.
Parameters:
rootKey(BIP32Interface): Master BIP32 keyaccount(number, optional): Account index (default: 0)coinType(number, optional): BIP44 coin type - 0 for mainnet, 1 for testnet (default: 0)
Returns: { scanKey, spendKey } - Both are BIP32Interface objects
Derivation paths:
- Scan:
m/352'/coin_type'/account'/1'/0 - Spend:
m/352'/coin_type'/account'/0'/0
Encodes a Silent Payment address from public keys.
Parameters:
scanPub(Uint8Array): 33-byte compressed scan public keyspendPub(Uint8Array): 33-byte compressed spend public keycoinType(number, optional): 0 for mainnet, 1 for testnet (default: 0)
Returns: Silent Payment address string (bech32m encoded)
Decodes a Silent Payment address.
Parameters:
address(string): Silent Payment address
Returns: { version, scanPub, spendPub, testnet }
version(number): Protocol versionscanPub(Uint8Array): 33-byte scan public keyspendPub(Uint8Array): 33-byte spend public keytestnet(boolean): True if testnet address
Convenience function that combines key derivation and address encoding.
Parameters:
rootKey(BIP32Interface): Master BIP32 keyaccount(number, optional): Account index (default: 0)coinType(number, optional): 0 for mainnet, 1 for testnet (default: 0)
Returns: { address, scanKey, spendKey }
Returns the BIP-32 derivation path string.
Parameters:
type(string): 'scan' or 'spend'account(number, optional): Account index (default: 0)coinType(number, optional): 0 for mainnet, 1 for testnet (default: 0)
Returns: Derivation path string (e.g., m/352'/0'/0'/1'/0)
Encodes a BIP-392 spscan key expression, for watch-only wallets that can scan but not spend.
Parameters:
scanPriv(Uint8Array): 32-byte scan private keyspendPub(Uint8Array): 33-byte compressed spend public keycoinType(number, optional): 0 for mainnet, 1 for testnet (default: 0)
Returns: spscan key expression string (bech32m encoded)
Decodes an spscan key expression.
Parameters:
expression(string):spscankey expression
Returns: { version, scanPriv, spendPub, testnet }
Encodes a BIP-392 spspend key expression, for full wallets that can both scan and spend.
Parameters:
scanPriv(Uint8Array): 32-byte scan private keyspendPriv(Uint8Array): 32-byte spend private keycoinType(number, optional): 0 for mainnet, 1 for testnet (default: 0)
Returns: spspend key expression string (bech32m encoded)
Decodes an spspend key expression.
Parameters:
expression(string):spspendkey expression
Returns: { version, scanPriv, spendPriv, testnet }
PURPOSE_BIP352: 352COIN_TYPE_BTC_MAINNET: 0COIN_TYPE_BTC_TESTNET: 1VERSION_SP: 0HRP_SP_MAINNET: 'sp'HRP_SP_TESTNET: 'tsp'HRP_SPSCAN_MAINNET: 'spscan'HRP_SPSCAN_TESTNET: 'tspscan'HRP_SPSPEND_MAINNET: 'spspend'HRP_SPSPEND_TESTNET: 'tspspend'
npm testThe test suite uses deterministic test vectors to verify:
- Correct key derivation paths
- Expected xpub/xprv values
- Address encoding/decoding
- Network type detection
- Invalid address rejection
- Key expression round-trips, HRP selection, and malformed input rejection
Silent Payment addresses use bech32m encoding (not bech32):
- HRP:
sp(mainnet) ortsp(testnet) - Version: 0
- Payload: 66 bytes (33-byte scan pubkey + 33-byte spend pubkey)
- Example:
sp1qqw3hksrgxfk0p7lf2h6hatk0n206a59l4wc354rxurvrjnzudfjx6q7dda0gtxsj4n0rp3u7782w8334tp5yt8y4uarpulqf05ca8gecj5p7ke3y
Key expressions also use bech32m, with the same version byte as addresses:
| HRP (mainnet / testnet) | Payload | Contents | |
|---|---|---|---|
spscan |
spscan / tspscan |
65 bytes | 32-byte scan privkey + 33-byte spend pubkey |
spspend |
spspend / tspspend |
64 bytes | 32-byte scan privkey + 32-byte spend privkey |
- bech32: Bech32m encoding/decoding — the only module
src/imports - bip32: HD key derivation
- tiny-secp256k1: Elliptic curve cryptography
- bip39: Mnemonic generation (for examples/testing)
- mocha: Test framework (dev dependency)
This library takes an already-derived BIP32 node as input rather than importing bip32 itself, so you supply your own bip32/tiny-secp256k1 instances as shown in the usage examples.
GPL-3.0