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
178 lines (147 loc) • 4.72 kB
JavaScript
/**
* @fileoverview Base58Check encoding implementation for Bitcoin
* @version 1.0.0
* @author yfbsei
* @license ISC
*/
import { createHash } from 'node:crypto';
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
const CHECKSUM_LENGTH = 4;
/**
* Double SHA256 hash for checksums
* @param {Buffer} data - Data to hash
* @returns {Buffer} 32-byte hash
*/
function doubleHash256(data) {
return createHash('sha256').update(createHash('sha256').update(data).digest()).digest();
}
/**
* Encode buffer to Base58 (without checksum)
* @param {Buffer} buffer - Data to encode
* @returns {string} Base58-encoded string
* @throws {Error} If input is not a Buffer
*/
function encode(buffer) {
if (!Buffer.isBuffer(buffer)) {
throw new Error('Input must be a Buffer');
}
if (buffer.length === 0) {
return '';
}
let leadingZeros = 0;
for (let i = 0; i < buffer.length && buffer[i] === 0; i++) {
leadingZeros++;
}
let num = BigInt(0);
for (let i = leadingZeros; i < buffer.length; i++) {
num = num * BigInt(256) + BigInt(buffer[i]);
}
const result = [];
if (num === BigInt(0)) {
result.push(BASE58_ALPHABET[0]);
} else {
while (num > 0) {
const remainder = num % BigInt(58);
num = num / BigInt(58);
result.unshift(BASE58_ALPHABET[Number(remainder)]);
}
}
return '1'.repeat(leadingZeros) + result.join('');
}
/**
* Decode Base58 string (without checksum)
* @param {string} str - Base58 string
* @returns {Buffer} Decoded data
* @throws {Error} If invalid characters found
*/
function decode(str) {
if (typeof str !== 'string') {
throw new Error('Input must be a string');
}
if (str.length === 0) {
return Buffer.alloc(0);
}
let leadingOnes = 0;
for (let i = 0; i < str.length && str[i] === '1'; i++) {
leadingOnes++;
}
let num = BigInt(0);
for (let i = leadingOnes; i < str.length; i++) {
const char = str[i];
const index = BASE58_ALPHABET.indexOf(char);
if (index === -1) {
throw new Error(`Invalid Base58 character: ${char}`);
}
num = num * BigInt(58) + BigInt(index);
}
const bytes = [];
if (num === BigInt(0)) {
bytes.push(0);
} else {
while (num > 0) {
const remainder = num % BigInt(256);
num = num / BigInt(256);
bytes.unshift(Number(remainder));
}
}
const leadingZeros = new Array(leadingOnes).fill(0);
return Buffer.from([...leadingZeros, ...bytes]);
}
/**
* Encode buffer with Base58Check (with checksum)
* @param {Buffer} buffer - Data to encode
* @returns {string} Base58Check-encoded string
* @throws {Error} If input invalid or checksum fails
*/
function b58encode(buffer) {
if (!Buffer.isBuffer(buffer)) {
throw new Error('Input must be a Buffer');
}
const checksum = doubleHash256(buffer).slice(0, CHECKSUM_LENGTH);
const checkedBuf = Buffer.concat([buffer, checksum]);
const encoded = encode(checkedBuf);
let leadingZeros = 0;
for (let i = 0; i < buffer.length && buffer[i] === 0; i++) {
leadingZeros++;
}
let decodedLeadingOnes = 0;
for (let i = 0; i < encoded.length && encoded[i] === '1'; i++) {
decodedLeadingOnes++;
}
if (decodedLeadingOnes !== leadingZeros) {
throw new Error(
`Leading zero preservation failed: expected ${leadingZeros} '1' chars, got ${decodedLeadingOnes}`
);
}
return encoded;
}
/**
* Decode Base58Check string (with checksum verification)
* @param {string} encoded - Base58Check string
* @returns {Buffer} Decoded data (without checksum)
* @throws {Error} If checksum verification fails
*/
function b58decode(encoded) {
if (typeof encoded !== 'string') {
throw new Error('Input must be a string');
}
const decoded = decode(encoded);
if (decoded.length < CHECKSUM_LENGTH) {
throw new Error('Decoded data too short for checksum verification');
}
const data = decoded.slice(0, -CHECKSUM_LENGTH);
const providedChecksum = decoded.slice(-CHECKSUM_LENGTH);
const expectedChecksum = doubleHash256(data).slice(0, CHECKSUM_LENGTH);
if (!providedChecksum.equals(expectedChecksum)) {
throw new Error('Checksum verification failed');
}
return data;
}
function encodeBase58Check(data) {
return b58encode(data);
}
function decodeBase58Check(encoded) {
return b58decode(encoded);
}
export { b58encode, b58decode, encode, decode, encodeBase58Check, decodeBase58Check };
export default { b58encode, b58decode, encode, decode, encodeBase58Check, decodeBase58Check };