safex-base58
Version:
CryptoNote-style chunked Base-58 encoder/decoder for Safex addresses
93 lines (85 loc) • 3.06 kB
JavaScript
/*!
* safex-base58
* CryptoNote-style Base-58 encoder/decoder & var-int helpers
* SPDX-License-Identifier: MIT
* Copyright © 2025 Daniel Dabek <github.com/safex>
*/
const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
const MAP = Object.fromEntries([...ALPHABET].map((c, i) => [c, i]));
const FULL_BLOCK_SIZE = 8; // bytes
const FULL_ENC_SIZE = 11; // chars
const ENC_SIZES = [0, 2, 3, 5, 6, 7, 9, 10]; // tail-block encode sizes
/** Little-endian 64-bit → base-58 string of fixed length */
function encodeBlock(data, size) {
let num = 0n;
for (let i = size - 1; i >= 0; i--) num = (num << 8n) | BigInt(data[i]);
const outSize = size === FULL_BLOCK_SIZE ? FULL_ENC_SIZE : ENC_SIZES[size];
const out = Array(outSize).fill('1');
for (let k = outSize - 1; k >= 0; k--) {
const div = num / 58n;
const rem = Number(num % 58n);
out[k] = ALPHABET[rem];
num = div;
}
return out.join('');
}
/** Base-58 chunked (CryptoNote) encoder. Accepts Buffer or hex string. */
export function base58Encode(input) {
const buf = Buffer.isBuffer(input) ? input : Buffer.from(input, 'hex');
let res = '';
for (let i = 0; i < buf.length; i += FULL_BLOCK_SIZE) {
const blk = buf.subarray(i, i + FULL_BLOCK_SIZE);
res += encodeBlock(blk, blk.length);
}
return res;
}
/** Decode a CryptoNote base-58 string → Buffer. Throws on malformed input. */
export function base58Decode(str) {
const chunks = [];
let i = 0;
while (i < str.length) {
const rem = str.length - i;
const encSize = rem >= FULL_ENC_SIZE ? FULL_ENC_SIZE : rem;
const blkSize = encSize === FULL_ENC_SIZE
? FULL_BLOCK_SIZE
: ENC_SIZES.indexOf(encSize);
if (blkSize <= 0) throw new Error('Invalid block length in CN Base58');
let num = 0n;
for (let k = 0; k < encSize; k++) {
const char = str[i + k];
const val = MAP[char];
if (val === undefined) throw new Error('Invalid Base58 char');
num = num * 58n + BigInt(val);
}
const blk = Buffer.alloc(blkSize);
for (let j = 0; j < blkSize; j++) {
blk[j] = Number(num & 0xffn);
num >>= 8n;
}
chunks.push(blk);
i += encSize;
}
return Buffer.concat(chunks);
}
/** CryptoNote var-int encoder (little-endian 7-bit groups) → Buffer */
export function encodeVarint(n) {
const out = [];
let num = n >>> 0;
while (num >= 0x80) { out.push((num & 0x7f) | 0x80); num >>= 7; }
out.push(num);
return Buffer.from(out);
}
/** Decoder counterpart. Returns {value, bytesRead}. */
export function decodeVarint(buf, offset = 0) {
let res = 0;
let shift = 0;
let i = offset;
while (i < buf.length) {
const byte = buf[i];
res |= (byte & 0x7f) << shift;
if (!(byte & 0x80)) break;
shift += 7;
i++;
}
return { value: res >>> 0, bytesRead: i - offset + 1 };
}