Skip to content

Base32 / Base58 / Base62 / Base85 Encoder — JavaScript source

Encode text to Base32, Base58, Base62, or Ascii85 - or decode it back. UTF-8 safe, runs entirely in your browser, with a shareable link to your exact input.

This is the JavaScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

/**
 * base-encoder - Base32 (RFC 4648), Base58 (Bitcoin), Base62, and Base85
 * (Ascii85) byte-array encoders, operating on the UTF-8 bytes of the input.
 *
 * Language:   JavaScript (ES2020+: BigInt + TextEncoder/TextDecoder; runs
 *             unmodified in Node 16+ and modern browsers)
 * Source:     CosmoDev polyglot showcase port of the Base Encoder tool, ported
 *             from cli/base-encoder/base-encoder.go (the authoritative Go twin).
 * License:    display source - part of CosmoDev's polyglot tool pages.
 *
 * Design goals:
 *   - Pure + deterministic; never throws (decode returns null for invalid or
 *     malformed input, mirroring the TS lib's null and the Go twin's errInvalid).
 *   - Functionally equivalent to the Go twin: same inputs -> same outputs.
 *   - Self-contained: stdlib only (no npm dependencies).
 *
 * Arbitrary-precision note: Base58 and Base62 base-convert the whole byte
 * array. JS has native BigInt, so we get the exact same semantics as the Go
 * twin's math/big for free - no manual bignum code is needed (unlike the
 * dependency-free Rust/PHP ports).
 */

'use strict';

/**
 * One of the four supported byte-array base encodings. Mirrors the Go twin's
 * `Scheme` type and the TS `Scheme` union.
 * @typedef {('base32' | 'base58' | 'base62' | 'base85')} Scheme
 */

const B32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
const B58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
const B62_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';

// Data characters emitted by a final (partial) 5-byte chunk before '=' padding,
// per RFC 4648. Index = byte count (0..4). Matches the TS `outLen` table.
const OUT_LEN_32 = [0, 2, 4, 5, 7];

// ---------------------------------------------------------------------------
// Base32 - RFC 4648 alphabet, padded to a multiple of 8 chars with '='.
// ---------------------------------------------------------------------------

function encode32(bytes) {
  let out = '';
  for (let i = 0; i < bytes.length; i += 5) {
    const chunk = bytes.subarray(i, i + 5);
    const b = [0, 0, 0, 0, 0];
    for (let j = 0; j < chunk.length; j++) b[j] = chunk[j];
    // Pack 5 bytes (40 bits) into 8 base32 digits (5 bits each, big-endian).
    const digits = [
      (b[0] >> 3) & 0x1f,
      ((b[0] << 2) & 0x1f) | ((b[1] >> 6) & 0x1f),
      (b[1] >> 1) & 0x1f,
      ((b[1] << 4) & 0x1f) | ((b[2] >> 4) & 0x1f),
      ((b[2] << 1) & 0x1f) | ((b[3] >> 7) & 0x1f),
      (b[3] >> 2) & 0x1f,
      ((b[3] << 3) & 0x1f) | ((b[4] >> 5) & 0x1f),
      b[4] & 0x1f,
    ];
    let group = '';
    for (const d of digits) group += B32_ALPHABET[d];
    if (chunk.length < 5) {
      // Truncate to the data chars and pad with '=' to 8 total.
      const outLen = OUT_LEN_32[chunk.length];
      group = group.slice(0, outLen) + '='.repeat(8 - outLen);
    }
    out += group;
  }
  return out;
}

function decode32(s) {
  const bytes = [];
  let buffer = 0;
  let bits = 0;
  for (const c of s) {
    if (c === '=') break; // padding marks the end
    const idx = B32_ALPHABET.indexOf(c);
    if (idx === -1) return null;
    buffer = (buffer << 5) | idx;
    bits += 5;
    if (bits >= 8) {
      bits -= 8;
      bytes.push((buffer >>> bits) & 0xff);
      buffer &= (1 << bits) - 1; // keep only the leftover bits
    }
  }
  return new Uint8Array(bytes);
}

// ---------------------------------------------------------------------------
// Base58 - Bitcoin alphabet. Leading 0x00 bytes -> leading '1' (count preserved).
// ---------------------------------------------------------------------------

function encode58(bytes) {
  // Count leading zero bytes - each maps to a leading '1'.
  let zeros = 0;
  while (zeros < bytes.length && bytes[zeros] === 0) zeros++;
  // Big-endian byte array (skipping the leading zeros) -> BigInt.
  let num = 0n;
  for (let i = zeros; i < bytes.length; i++) num = (num << 8n) | BigInt(bytes[i]);
  // Base-convert to 58 digits (collected least-significant first).
  const digits = [];
  let v = num;
  while (v > 0n) {
    const rem = v % 58n;
    digits.push(Number(rem));
    v = v / 58n;
  }
  let out = '1'.repeat(zeros);
  for (let i = digits.length - 1; i >= 0; i--) out += B58_ALPHABET[digits[i]];
  return out;
}

function decode58(s) {
  // Count leading '1's - each maps to a 0x00 byte.
  let zeros = 0;
  while (zeros < s.length && s[zeros] === '1') zeros++;
  let num = 0n;
  for (let i = zeros; i < s.length; i++) {
    const idx = B58_ALPHABET.indexOf(s[i]);
    if (idx === -1) return null;
    num = num * 58n + BigInt(idx);
  }
  // BigInt -> big-endian bytes.
  const bytes = [];
  let v = num;
  while (v > 0n) {
    bytes.unshift(Number(v & 0xffn));
    v = v >> 8n;
  }
  const out = new Uint8Array(zeros + bytes.length);
  for (let i = 0; i < bytes.length; i++) out[zeros + i] = bytes[i];
  return out;
}

// ---------------------------------------------------------------------------
// Base62 - standard BigInt base-conversion of the byte array (no leading-zero
// special-casing beyond the standard big-int).
// ---------------------------------------------------------------------------

function encode62(bytes) {
  if (bytes.length === 0) return '';
  let num = 0n;
  for (const b of bytes) num = (num << 8n) | BigInt(b);
  if (num === 0n) return '0';
  const digits = [];
  let v = num;
  while (v > 0n) {
    const rem = v % 62n;
    digits.push(Number(rem));
    v = v / 62n;
  }
  let out = '';
  for (let i = digits.length - 1; i >= 0; i--) out += B62_ALPHABET[digits[i]];
  return out;
}

function decode62(s) {
  if (s.length === 0) return new Uint8Array(0);
  let num = 0n;
  for (const c of s) {
    const idx = B62_ALPHABET.indexOf(c);
    if (idx === -1) return null;
    num = num * 62n + BigInt(idx);
  }
  const bytes = [];
  let v = num;
  while (v > 0n) {
    bytes.unshift(Number(v & 0xffn));
    v = v >> 8n;
  }
  return new Uint8Array(bytes);
}

// ---------------------------------------------------------------------------
// Base85 - Ascii85. 4 bytes -> 5 chars in '!'(33)..'u'(117); a full 4-zero
// group is shortened to 'z'. No <~ ~> delimiters. Partial final groups emit
// one fewer char than (bytes+1) would suggest; decode reverses, padding with
// 'u' (value 84).
// ---------------------------------------------------------------------------

function encode85(bytes) {
  let out = '';
  for (let i = 0; i < bytes.length; i += 4) {
    const chunk = bytes.subarray(i, i + 4);
    const isFull = chunk.length === 4;
    const b = [0, 0, 0, 0];
    for (let j = 0; j < chunk.length; j++) b[j] = chunk[j];
    const u = b[0] * 16777216 + b[1] * 65536 + b[2] * 256 + b[3];
    if (isFull && u === 0) {
      out += 'z'; // zero-group shorthand
      continue;
    }
    const digits = [0, 0, 0, 0, 0];
    let v = u;
    for (let k = 4; k >= 0; k--) {
      digits[k] = v % 85;
      v = Math.floor(v / 85);
    }
    let chars = '';
    for (const d of digits) chars += String.fromCharCode(d + 33);
    if (!isFull) chars = chars.slice(0, chunk.length + 1); // n bytes -> n+1 chars
    out += chars;
  }
  return out;
}

function decode85(s) {
  const bytes = [];
  let group = [];
  for (let i = 0; i < s.length; i++) {
    const c = s[i];
    if (c === 'z') {
      // 'z' is only valid at a group boundary (an empty accumulator).
      if (group.length !== 0) return null;
      bytes.push(0, 0, 0, 0);
      continue;
    }
    const code = c.charCodeAt(0);
    if (code < 33 || code > 117) return null;
    group.push(code - 33);
    if (group.length === 5) {
      let v = 0;
      for (const d of group) v = v * 85 + d;
      if (v > 0xffffffff) return null; // a 5-char group must fit in 32 bits
      bytes.push((v >>> 24) & 0xff, (v >>> 16) & 0xff, (v >>> 8) & 0xff, v & 0xff);
      group = [];
    }
  }
  // Handle a partial final group (2-4 chars -> 1-3 bytes).
  if (group.length > 0) {
    const m = group.length;
    if (m < 2) return null; // a lone trailing char is malformed
    while (group.length < 5) group.push(84); // pad with 'u'
    let v = 0;
    for (const d of group) v = v * 85 + d;
    if (v > 0xffffffff) return null;
    const all = [(v >>> 24) & 0xff, (v >>> 16) & 0xff, (v >>> 8) & 0xff, v & 0xff];
    for (let k = 0; k < m - 1; k++) bytes.push(all[k]);
  }
  return new Uint8Array(bytes);
}

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

function encodeBytes(bytes, scheme) {
  switch (scheme) {
    case 'base32':
      return encode32(bytes);
    case 'base58':
      return encode58(bytes);
    case 'base62':
      return encode62(bytes);
    case 'base85':
      return encode85(bytes);
    default:
      return '';
  }
}

function decodeBytes(encoded, scheme) {
  switch (scheme) {
    case 'base32':
      return decode32(encoded);
    case 'base58':
      return decode58(encoded);
    case 'base62':
      return decode62(encoded);
    case 'base85':
      return decode85(encoded);
    default:
      return null;
  }
}

/**
 * Encode the UTF-8 bytes of `text` per `scheme`. Empty text -> ''.
 * Mirrors `Encode` in cli/base-encoder/base-encoder.go.
 *
 * @param {string} text
 * @param {Scheme} scheme
 * @returns {string}
 */
function encode(text, scheme) {
  const bytes = new TextEncoder().encode(text);
  return encodeBytes(bytes, scheme);
}

/**
 * Decode `encoded` back to UTF-8 text. Invalid chars / malformed -> null
 * (mirrors the Go twin's errInvalid and the TS lib's null).
 * Mirrors `Decode` in cli/base-encoder/base-encoder.go.
 *
 * @param {string} encoded
 * @param {Scheme} scheme
 * @returns {string | null}
 */
function decode(encoded, scheme) {
  const bytes = decodeBytes(encoded, scheme);
  if (bytes === null) return null;
  // TextDecoder is non-fatal by default, so a structurally-valid-but-non-UTF-8
  // payload never throws a second error (mirrors Go's string(data)).
  return new TextDecoder().decode(bytes);
}

// CommonJS export so the file is consumable from Node without a build step,
// while staying dependency-free and framework-agnostic.
module.exports = {
  encode,
  decode,
  encodeBytes,
  decodeBytes,
  B32_ALPHABET,
  B58_ALPHABET,
  B62_ALPHABET,
};

// ---------------------------------------------------------------------------
// Showcase self-test - mirrors cli/base-encoder/base-encoder_test.go vectors.
// Run directly: `node js.js`
// ---------------------------------------------------------------------------
if (require.main === module) {
  const assert = require('assert');

  // Base32 - known values + RFC 4648 padding + case sensitivity.
  assert.strictEqual(encode('hello', 'base32'), 'NBSWY3DP');
  assert.strictEqual(encode('foo', 'base32'), 'MZXW6==='); // 3 bytes -> 5 chars + 3 '='
  assert.strictEqual(decode('NBSWY3DP', 'base32'), 'hello');
  assert.strictEqual(decode('nbswy3dp', 'base32'), null); // lowercase not in RFC 4648

  // Base58 - each leading 0x00 byte -> a leading '1'.
  assert.strictEqual(encode('\x00', 'base58'), '1');
  assert.ok(encode('\x00\x00A', 'base58').startsWith('11'));
  assert.strictEqual(decode('1', 'base58'), '\x00');
  assert.strictEqual(decode(encode('\x00\x00A', 'base58'), 'base58'), '\x00\x00A');

  // Base62 - plain big-int base conversion (no leading-zero preservation).
  assert.strictEqual(encode('A', 'base62'), '13'); // 1*62 + 3
  assert.strictEqual(decode('13', 'base62'), 'A');
  assert.strictEqual(encode('\x00', 'base62'), '0');
  assert.strictEqual(decode('0', 'base62'), ''); // minimal rep of 0 is empty

  // Base85 - Ascii85 'z' shorthand + 32-bit overflow rejection.
  assert.strictEqual(encode('hello', 'base85'), 'BOu!rDZ');
  assert.strictEqual(encode('\x00\x00\x00\x00', 'base85'), 'z');
  assert.strictEqual(encode('\x00'.repeat(8), 'base85'), 'zz');
  assert.strictEqual(decode('uuuuu', 'base85'), null); // 5-char group overflows 32 bits
  assert.strictEqual(decode('B', 'base85'), null); // lone trailing char is malformed

  // Cross-scheme - empty, multibyte round-trip, and invalid rejection.
  for (const scheme of ['base32', 'base58', 'base62', 'base85']) {
    assert.strictEqual(encode('', scheme), '');
    assert.strictEqual(decode('', scheme), '');
    assert.strictEqual(decode(encode('CosmoDev 🚀', scheme), scheme), 'CosmoDev 🚀');
    assert.strictEqual(decode('~!not-valid!~', scheme), null); // '~' outside every alphabet
  }

  console.log('ok');
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →