Skip to content

Base32 / Base58 / Base62 / Base85 Encoder — TypeScript 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 TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Byte-array base encoders: Base32 (RFC 4648), Base58 (Bitcoin), Base62,
// and Base85 (Ascii85). All four operate on the UTF-8 bytes of the input text.
// Pure + deterministic, zero npm deps - the unit-test surface for the
// Base Encoder tool.

export type Scheme = 'base32' | 'base58' | 'base62' | 'base85';

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

const B32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';

function encode32(bytes: Uint8Array): string {
  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 = [0, 2, 4, 5, 7][chunk.length];
      group = group.slice(0, outLen) + '='.repeat(8 - outLen);
    }
    out += group;
  }
  return out;
}

function decode32(s: string): Uint8Array | null {
  const bytes: number[] = [];
  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).
// ---------------------------------------------------------------------------

const B58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';

function encode58(bytes: Uint8Array): string {
  // 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 → BigInt (skipping the leading zeros).
  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: number[] = [];
  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: string): Uint8Array | null {
  // 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: number[] = [];
  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 standard big-int).
// ---------------------------------------------------------------------------

const B62_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';

function encode62(bytes: Uint8Array): string {
  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: number[] = [];
  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: string): Uint8Array | null {
  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: number[] = [];
  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: Uint8Array): string {
  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: string): Uint8Array | null {
  const bytes: number[] = [];
  let group: number[] = [];
  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: Uint8Array, scheme: Scheme): string {
  switch (scheme) {
    case 'base32':
      return encode32(bytes);
    case 'base58':
      return encode58(bytes);
    case 'base62':
      return encode62(bytes);
    case 'base85':
      return encode85(bytes);
  }
}

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

/** Encode the UTF-8 bytes of `text` per `scheme`. Empty text → ''. */
export function encode(text: string, scheme: Scheme): string {
  const bytes = new TextEncoder().encode(text);
  return encodeBytes(bytes, scheme);
}

/** Decode `encoded` back to UTF-8 text. Invalid chars / malformed → null. */
export function decode(encoded: string, scheme: Scheme): string | null {
  const bytes = decodeBytes(encoded, scheme);
  if (bytes === null) return null;
  return new TextDecoder().decode(bytes);
}

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 →