j-bitcoin
Version:
Comprehensive JavaScript/TypeScript Bitcoin (BTC) wallet library with custodial and non-custodial support, hierarchical deterministic keys, threshold signatures, and advanced cryptographic features
259 lines (222 loc) • 6.77 kB
JavaScript
/**
* @fileoverview Bitcoin address and key decoding utilities
* @version 1.0.0
* @author yfbsei
* @license ISC
*/
import { b58decode } from '../base58.js';
import { decodeSegwit } from '../base32.js';
import { NETWORK_VERSIONS, CRYPTO_CONSTANTS } from '../../core/constants.js';
/**
* Custom error class for decoding operations
* @class DecodingError
* @extends Error
*/
class DecodingError extends Error {
constructor(message, code, details = {}) {
super(message);
this.name = 'DecodingError';
this.code = code;
this.details = details;
}
}
/**
* Decode a WIF-encoded private key
* @param {string} wif - WIF string
* @returns {Object} Decoded {privateKey, network, compressed}
* @throws {DecodingError} If WIF format invalid
*/
function decodeWIFPrivateKey(wif) {
if (typeof wif !== 'string') {
throw new DecodingError('WIF must be a string', 'INVALID_TYPE');
}
const decoded = b58decode(wif);
if (decoded.length !== 33 && decoded.length !== 34) {
throw new DecodingError(
`Invalid WIF length: ${decoded.length}`,
'INVALID_LENGTH'
);
}
const versionByte = decoded[0];
let network, compressed;
if (versionByte === NETWORK_VERSIONS.main.versions.WIF_PRIVATE_KEY) {
network = 'main';
} else if (versionByte === NETWORK_VERSIONS.test.versions.WIF_PRIVATE_KEY) {
network = 'test';
} else {
throw new DecodingError('Invalid WIF version byte', 'INVALID_VERSION');
}
if (decoded.length === 34) {
if (decoded[33] !== 0x01) {
throw new DecodingError('Invalid compression flag', 'INVALID_COMPRESSION');
}
compressed = true;
} else {
compressed = false;
}
const privateKey = decoded.slice(1, 33);
return {
privateKey,
network,
compressed
};
}
/**
* Decode a legacy (Base58Check) address
* @param {string} address - Bitcoin address
* @returns {Object} Decoded {hash160, network, type, version}
* @throws {DecodingError} If address invalid
*/
function decodeLegacyAddress(address) {
if (typeof address !== 'string') {
throw new DecodingError('Address must be a string', 'INVALID_TYPE');
}
const decoded = b58decode(address);
if (decoded.length !== 21) {
throw new DecodingError(
`Invalid address length: ${decoded.length}`,
'INVALID_LENGTH'
);
}
const versionByte = decoded[0];
const hash160 = decoded.slice(1, 21);
let network, type;
if (versionByte === NETWORK_VERSIONS.main.versions.P2PKH_ADDRESS) {
network = 'main';
type = 'p2pkh';
} else if (versionByte === NETWORK_VERSIONS.main.versions.P2SH_ADDRESS) {
network = 'main';
type = 'p2sh';
} else if (versionByte === NETWORK_VERSIONS.test.versions.P2PKH_ADDRESS) {
network = 'test';
type = 'p2pkh';
} else if (versionByte === NETWORK_VERSIONS.test.versions.P2SH_ADDRESS) {
network = 'test';
type = 'p2sh';
} else {
throw new DecodingError('Unknown address version', 'UNKNOWN_VERSION');
}
return {
hash160,
network,
type,
version: versionByte
};
}
/**
* Decode a SegWit (Bech32/Bech32m) address
* @param {string} address - Bech32 address
* @returns {Object} Decoded {program, version, network, type}
* @throws {DecodingError} If address invalid
*/
function decodeSegwitAddress(address) {
const lowerAddr = address.toLowerCase();
let hrp;
if (lowerAddr.startsWith('bc1')) {
hrp = 'bc';
} else if (lowerAddr.startsWith('tb1')) {
hrp = 'tb';
} else {
throw new DecodingError('Invalid segwit address prefix', 'INVALID_PREFIX');
}
const result = decodeSegwit(hrp, lowerAddr);
const network = hrp === 'bc' ? 'main' : 'test';
let type;
if (result.version === 0) {
type = result.program.length === 20 ? 'p2wpkh' : 'p2wsh';
} else if (result.version === 1) {
type = 'p2tr';
} else {
type = `witness_v${result.version}`;
}
return {
program: result.program,
version: result.version,
network,
type
};
}
/**
* Decode any Bitcoin address (auto-detects type)
* @param {string} address - Bitcoin address
* @returns {Object} Decoded address information
* @throws {DecodingError} If address invalid
*/
function decodeAddress(address) {
if (typeof address !== 'string' || address.length === 0) {
throw new DecodingError('Address must be a non-empty string', 'INVALID_ADDRESS');
}
const lowerAddr = address.toLowerCase();
if (lowerAddr.startsWith('bc1') || lowerAddr.startsWith('tb1')) {
return decodeSegwitAddress(address);
}
return decodeLegacyAddress(address);
}
/**
* Decode an extended key (xprv/xpub/tprv/tpub)
* @param {string} extendedKey - Extended key string
* @returns {Object} Decoded key information
* @throws {DecodingError} If key format invalid
*/
function decodeExtendedKey(extendedKey) {
if (typeof extendedKey !== 'string') {
throw new DecodingError('Extended key must be a string', 'INVALID_TYPE');
}
const decoded = b58decode(extendedKey);
if (decoded.length !== 78) {
throw new DecodingError(
`Invalid extended key length: ${decoded.length}`,
'INVALID_LENGTH'
);
}
const version = decoded.slice(0, 4);
const depth = decoded.readUInt8(4);
const parentFingerprint = decoded.slice(5, 9);
const childIndex = decoded.readUInt32BE(9);
const chainCode = decoded.slice(13, 45);
const keyData = decoded.slice(45, 78);
let isPrivate = false;
let network = 'main';
if (version.equals(NETWORK_VERSIONS.main.versions.EXTENDED_PRIVATE_KEY)) {
isPrivate = true;
network = 'main';
} else if (version.equals(NETWORK_VERSIONS.main.versions.EXTENDED_PUBLIC_KEY)) {
isPrivate = false;
network = 'main';
} else if (version.equals(NETWORK_VERSIONS.test.versions.EXTENDED_PRIVATE_KEY)) {
isPrivate = true;
network = 'test';
} else if (version.equals(NETWORK_VERSIONS.test.versions.EXTENDED_PUBLIC_KEY)) {
isPrivate = false;
network = 'test';
} else {
throw new DecodingError('Unknown extended key version', 'UNKNOWN_VERSION');
}
let key;
if (isPrivate) {
if (keyData[0] !== 0x00) {
throw new DecodingError('Invalid private key padding', 'INVALID_PADDING');
}
key = keyData.slice(1);
} else {
key = keyData;
}
return {
version,
depth,
parentFingerprint,
childIndex,
chainCode,
key,
isPrivate,
network
};
}
export {
DecodingError,
decodeWIFPrivateKey,
decodeLegacyAddress,
decodeSegwitAddress,
decodeAddress,
decodeExtendedKey
};