/* scure-base - MIT License (c) 2022 Paul Miller (paulmillr.com) */ import { stringToUInt8Array, Uint8ArrayToString } from './bin' // Utilities /** * @__NO_SIDE_EFFECTS__ */ export function assertNumber(n: number) { if (!Number.isSafeInteger(n)) throw new Error(`Wrong integer: ${n}`) } export interface Coder { encode(from: F): T decode(to: T): F } export interface BytesCoder extends Coder { encode: (data: Uint8Array) => string decode: (str: string) => Uint8Array } function isBytes(a: unknown): a is Uint8Array { return ( a instanceof Uint8Array || (a != null && typeof a === 'object' && a.constructor.name === 'Uint8Array') ) } // TODO: some recusive type inference so it would check correct order of input/output inside rest? // like , , export type BaseXChain = [Coder, ...Coder[]] // Extract info from Coder type export type BaseXInput = F extends Coder ? T : never export type BaseXOutput = F extends Coder ? T : never // Generic function for arrays export type BaseXFirst = T extends [infer U, ...any[]] ? U : never export type BaseXLast = T extends [...any[], infer U] ? U : never export type BaseXTail = T extends [any, ...infer U] ? U : never export type BaseXAsChain> = { // C[K] = Coder, Input> [K in keyof C]: Coder, BaseXInput> } // Publicly visible aliases prefixed with BaseX for clearer documentation // Remove old alias leftovers (we renamed core types to BaseX-prefixed names). /** * @__NO_SIDE_EFFECTS__ */ function chain>(...args: T): Coder>, BaseXOutput>> { const id = (a: any) => a // Wrap call in closure so JIT can inline calls const wrap = (a: any, b: any) => (c: any) => a(b(c)) // Construct chain of args[-1].encode(args[-2].encode([...])) const encode = args.map((x) => x.encode).reduceRight(wrap, id) // Construct chain of args[0].decode(args[1].decode(...)) const decode = args.map((x) => x.decode).reduce(wrap, id) return { encode, decode } } export type BaseXAlphabet = string[] | string /** * Encodes integer radix representation to array of strings using alphabet and back * @__NO_SIDE_EFFECTS__ */ function alphabet(alphabet: BaseXAlphabet): Coder { return { encode: (digits: number[]) => { if (!Array.isArray(digits) || (digits.length && typeof digits[0] !== 'number')) throw new Error('alphabet.encode input should be an array of numbers') return digits.map((i) => { assertNumber(i) if (i < 0 || i >= alphabet.length) throw new Error(`Digit index outside alphabet: ${i} (alphabet: ${alphabet.length})`) return alphabet[i]! }) }, decode: (input: string[]) => { if (!Array.isArray(input) || (input.length && typeof input[0] !== 'string')) throw new Error('alphabet.decode input should be array of strings') return input.map((letter) => { if (typeof letter !== 'string') throw new Error(`alphabet.decode: not string element=${letter}`) const index = alphabet.indexOf(letter) if (index === -1) throw new Error(`Unknown letter: "${letter}". Allowed: ${alphabet}`) return index }) }, } } // Alias exported after Alphabet is declared (kept only for backwards compat if needed) /** * @__NO_SIDE_EFFECTS__ */ function join(separator = ''): Coder { if (typeof separator !== 'string') throw new Error('join separator should be string') return { encode: (from) => { if (!Array.isArray(from) || (from.length && typeof from[0] !== 'string')) throw new Error('join.encode input should be array of strings') for (let i of from) if (typeof i !== 'string') throw new Error(`join.encode: non-string input=${i}`) return from.join(separator) }, decode: (to) => { if (typeof to !== 'string') throw new Error('join.decode input should be string') return to.split(separator) }, } } /** * Pad strings array so it has integer number of bits * @__NO_SIDE_EFFECTS__ */ function padding(bits: number, chr = '='): Coder { assertNumber(bits) if (typeof chr !== 'string') throw new Error('padding chr should be string') return { encode(data: string[]): string[] { if (!Array.isArray(data) || (data.length && typeof data[0] !== 'string')) throw new Error('padding.encode input should be array of strings') for (let i of data) if (typeof i !== 'string') throw new Error(`padding.encode: non-string input=${i}`) while ((data.length * bits) % 8) data.push(chr) return data }, decode(input: string[]): string[] { if (!Array.isArray(input) || (input.length && typeof input[0] !== 'string')) throw new Error('padding.encode input should be array of strings') for (let i of input) if (typeof i !== 'string') throw new Error(`padding.decode: non-string input=${i}`) let end = input.length if ((end * bits) % 8) throw new Error('Invalid padding: string should have whole number of bytes') for (; end > 0 && input[end - 1] === chr; end--) { if (!(((end - 1) * bits) % 8)) throw new Error('Invalid padding: string has too much padding') } return input.slice(0, end) }, } } /** * @__NO_SIDE_EFFECTS__ */ function normalize(fn: (val: T) => T): Coder { if (typeof fn !== 'function') throw new Error('normalize fn should be function') return { encode: (from: T) => from, decode: (to: T) => fn(to) } } /** * Slow: O(n^2) time complexity * @__NO_SIDE_EFFECTS__ */ function convertRadix(data: number[], from: number, to: number): number[] { // base 1 is impossible if (from < 2) throw new Error(`convertRadix: wrong from=${from}, base cannot be less than 2`) if (to < 2) throw new Error(`convertRadix: wrong to=${to}, base cannot be less than 2`) if (!Array.isArray(data)) throw new Error('convertRadix: data should be array') if (!data.length) return [] let pos = 0 const res = [] const digits = Array.from(data) digits.forEach((d) => { assertNumber(d) if (d < 0 || d >= from) throw new Error(`Wrong integer: ${d}`) }) while (true) { let carry = 0 let done = true for (let i = pos; i < digits.length; i++) { const digit = digits[i]! const digitBase = from * carry + digit if ( !Number.isSafeInteger(digitBase) || (from * carry) / from !== carry || digitBase - digit !== from * carry ) { throw new Error('convertRadix: carry overflow') } carry = digitBase % to const rounded = Math.floor(digitBase / to) digits[i] = rounded if (!Number.isSafeInteger(rounded) || rounded * to + carry !== digitBase) throw new Error('convertRadix: carry overflow') if (!done) continue else if (!rounded) pos = i else done = false } res.push(carry) if (done) break } for (let i = 0; i < data.length - 1 && data[i] === 0; i++) res.push(0) return res.reverse() } const gcd = /* @__NO_SIDE_EFFECTS__ */ (a: number, b: number): number => (!b ? a : gcd(b, a % b)) const radix2carry = /*@__NO_SIDE_EFFECTS__ */ (from: number, to: number) => from + (to - gcd(from, to)) /** * Implemented with numbers, because BigInt is 5x slower * @__NO_SIDE_EFFECTS__ */ function convertRadix2(data: number[], from: number, to: number, padding: boolean): number[] { if (!Array.isArray(data)) throw new Error('convertRadix2: data should be array') if (from <= 0 || from > 32) throw new Error(`convertRadix2: wrong from=${from}`) if (to <= 0 || to > 32) throw new Error(`convertRadix2: wrong to=${to}`) if (radix2carry(from, to) > 32) { throw new Error( `convertRadix2: carry overflow from=${from} to=${to} carryBits=${radix2carry(from, to)}` ) } let carry = 0 let pos = 0 // bitwise position in current element const mask = 2 ** to - 1 const res: number[] = [] for (const n of data) { assertNumber(n) if (n >= 2 ** from) throw new Error(`convertRadix2: invalid data word=${n} from=${from}`) carry = (carry << from) | n if (pos + from > 32) throw new Error(`convertRadix2: carry overflow pos=${pos} from=${from}`) pos += from for (; pos >= to; pos -= to) res.push(((carry >> (pos - to)) & mask) >>> 0) carry &= 2 ** pos - 1 // clean carry, otherwise it will cause overflow } carry = (carry << (to - pos)) & mask if (!padding && pos >= from) throw new Error('Excess padding') if (!padding && carry) throw new Error(`Non-zero padding: ${carry}`) if (padding && pos > 0) res.push(carry >>> 0) return res } /** * @__NO_SIDE_EFFECTS__ */ function radix(num: number): Coder { assertNumber(num) return { encode: (bytes: Uint8Array) => { if (!isBytes(bytes)) throw new Error('radix.encode input should be Uint8Array') return convertRadix(Array.from(bytes), 2 ** 8, num) }, decode: (digits: number[]) => { if (!Array.isArray(digits) || (digits.length && typeof digits[0] !== 'number')) throw new Error('radix.decode input should be array of numbers') return Uint8Array.from(convertRadix(digits, num, 2 ** 8)) }, } } /** * If both bases are power of same number (like `2**8 <-> 2**64`), * there is a linear algorithm. For now we have implementation for power-of-two bases only. * @__NO_SIDE_EFFECTS__ */ function radix2(bits: number, revPadding = false): Coder { assertNumber(bits) if (bits <= 0 || bits > 32) throw new Error('radix2: bits should be in (0..32]') if (radix2carry(8, bits) > 32 || radix2carry(bits, 8) > 32) throw new Error('radix2: carry overflow') return { encode: (bytes: Uint8Array) => { if (!isBytes(bytes)) throw new Error('radix2.encode input should be Uint8Array') return convertRadix2(Array.from(bytes), 8, bits, !revPadding) }, decode: (digits: number[]) => { if (!Array.isArray(digits) || (digits.length && typeof digits[0] !== 'number')) throw new Error('radix2.decode input should be array of numbers') return Uint8Array.from(convertRadix2(digits, bits, 8, revPadding)) }, } } type ArgumentTypes = F extends (...args: infer A) => any ? A : never /** * @__NO_SIDE_EFFECTS__ */ function unsafeWrapper any>(fn: T) { if (typeof fn !== 'function') throw new Error('unsafeWrapper fn should be function') return function (...args: ArgumentTypes): ReturnType | void { try { return fn.apply(null, args) } catch (e) { } } } /** * @__NO_SIDE_EFFECTS__ */ function checksum( len: number, fn: (data: Uint8Array) => Uint8Array ): Coder { assertNumber(len) if (typeof fn !== 'function') throw new Error('checksum fn should be function') return { encode(data: Uint8Array) { if (!isBytes(data)) throw new Error('checksum.encode: input should be Uint8Array') const checksum = fn(data).slice(0, len) const res = new Uint8Array(data.length + len) res.set(data) res.set(checksum, data.length) return res }, decode(data: Uint8Array) { if (!isBytes(data)) throw new Error('checksum.decode: input should be Uint8Array') const payload = data.slice(0, -len) const newChecksum = fn(payload).slice(0, len) const oldChecksum = data.slice(-len) for (let i = 0; i < len; i++) if (newChecksum[i] !== oldChecksum[i]) throw new Error('Invalid checksum') return payload }, } } // prettier-ignore export const utils = { alphabet, chain, checksum, convertRadix, convertRadix2, radix, radix2, join, padding, } // RFC 4648 aka RFC 3548 // --------------------- export const base16: BytesCoder = /* @__PURE__ */ chain( radix2(4), alphabet('0123456789ABCDEF'), join('') ) export const base32: BytesCoder = /* @__PURE__ */ chain( radix2(5), alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'), padding(5), join('') ) export const base32nopad: BytesCoder = /* @__PURE__ */ chain( radix2(5), alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'), join('') ) export const base32hex: BytesCoder = /* @__PURE__ */ chain( radix2(5), alphabet('0123456789ABCDEFGHIJKLMNOPQRSTUV'), padding(5), join('') ) export const base32hexnopad: BytesCoder = /* @__PURE__ */ chain( radix2(5), alphabet('0123456789ABCDEFGHIJKLMNOPQRSTUV'), join('') ) export const base32agnoster: BytesCoder = /* @__PURE__ */ chain( radix2(5), alphabet('0123456789abcdefghjkmnpqrtuvwxyz'), join('') ) export const base32crockford: BytesCoder = /* @__PURE__ */ chain( radix2(5), alphabet('0123456789ABCDEFGHJKMNPQRSTVWXYZ'), join(''), normalize((s: string) => s.toUpperCase().replace(/O/g, '0').replace(/[IL]/g, '1')) ) export const base64: BytesCoder = /* @__PURE__ */ chain( radix2(6), alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'), padding(6), join('') ) export const base64nopad: BytesCoder = /* @__PURE__ */ chain( radix2(6), alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'), join('') ) export const base64url: BytesCoder = /* @__PURE__ */ chain( radix2(6), alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'), padding(6), join('') ) export const base64urlnopad: BytesCoder = /* @__PURE__ */ chain( radix2(6), alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'), join('') ) // base58 code // ----------- const genBase58 = (abc: string) => chain(radix(58), alphabet(abc), join('')) export const base58: BytesCoder = /* @__PURE__ */ genBase58( '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' ) export const base58flickr: BytesCoder = /* @__PURE__ */ genBase58( '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ' ) export const base58xrp: BytesCoder = /* @__PURE__ */ genBase58( 'rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz' ) // xmr ver is done in 8-byte blocks (which equals 11 chars in decoding). Last (non-full) block padded with '1' to size in XMR_BLOCK_LEN. // Block encoding significantly reduces quadratic complexity of base58. // Data len (index) -> encoded block len const XMR_BLOCK_LEN = [0, 2, 3, 5, 6, 7, 9, 10, 11] export const base58xmr: BytesCoder = { encode(data: Uint8Array) { let res = '' for (let i = 0; i < data.length; i += 8) { const block = data.subarray(i, i + 8) res += base58.encode(block).padStart(XMR_BLOCK_LEN[block.length]!, '1') } return res }, decode(str: string) { let res: number[] = [] for (let i = 0; i < str.length; i += 11) { const slice = str.slice(i, i + 11) const blockLen = XMR_BLOCK_LEN.indexOf(slice.length) const block = base58.decode(slice) for (let j = 0; j < block.length - blockLen; j++) { if (block[j] !== 0) throw new Error('base58xmr: wrong padding') } res = res.concat(Array.from(block.slice(block.length - blockLen))) } return Uint8Array.from(res) }, } export const createBase58check = (sha256: (data: Uint8Array) => Uint8Array): BytesCoder => chain( checksum(4, (data) => sha256(sha256(data))), base58 ) // legacy export, bad name export const base58check = createBase58check // Bech32 code // ----------- export interface Bech32Decoded { prefix: Prefix words: number[] } export interface Bech32DecodedWithArray { prefix: Prefix words: number[] bytes: Uint8Array } const BECH_ALPHABET: Coder = /* @__PURE__ */ chain( alphabet('qpzry9x8gf2tvdw0s3jn54khce6mua7l'), join('') ) const POLYMOD_GENERATORS = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] /** * @__NO_SIDE_EFFECTS__ */ function bech32Polymod(pre: number): number { const b = pre >> 25 let chk = (pre & 0x1ffffff) << 5 for (let i = 0; i < POLYMOD_GENERATORS.length; i++) { if (((b >> i) & 1) === 1) chk ^= POLYMOD_GENERATORS[i]! } return chk } /** * @__NO_SIDE_EFFECTS__ */ function bechChecksum(prefix: string, words: number[], encodingConst = 1): string { const len = prefix.length let chk = 1 for (let i = 0; i < len; i++) { const c = prefix.charCodeAt(i) if (c < 33 || c > 126) throw new Error(`Invalid prefix (${prefix})`) chk = bech32Polymod(chk) ^ (c >> 5) } chk = bech32Polymod(chk) for (let i = 0; i < len; i++) chk = bech32Polymod(chk) ^ (prefix.charCodeAt(i) & 0x1f) for (let v of words) chk = bech32Polymod(chk) ^ v for (let i = 0; i < 6; i++) chk = bech32Polymod(chk) chk ^= encodingConst return BECH_ALPHABET.encode(convertRadix2([chk % 2 ** 30], 30, 5, false)) } export interface Bech32 { encode( prefix: Prefix, words: number[] | Uint8Array, limit?: number | false ): `${Lowercase}1${string}` decode( str: `${Prefix}1${string}`, limit?: number | false ): Bech32Decoded encodeFromBytes(prefix: string, bytes: Uint8Array): string decodeToBytes(str: string): Bech32DecodedWithArray decodeUnsafe(str: string, limit?: number | false): void | Bech32Decoded fromWords(to: number[]): Uint8Array fromWordsUnsafe(to: number[]): void | Uint8Array toWords(from: Uint8Array): number[] } /** * @__NO_SIDE_EFFECTS__ */ function genBech32(encoding: 'bech32' | 'bech32m'): Bech32 { const ENCODING_CONST = encoding === 'bech32' ? 1 : 0x2bc830a3 const _words = radix2(5) const fromWords = _words.decode const toWords = _words.encode const fromWordsUnsafe = unsafeWrapper(fromWords) function encode( prefix: Prefix, words: number[] | Uint8Array, limit: number | false = 90 ): `${Lowercase}1${string}` { if (typeof prefix !== 'string') throw new Error(`bech32.encode prefix should be string, not ${typeof prefix}`) if (words instanceof Uint8Array) words = Array.from(words) if (!Array.isArray(words) || (words.length && typeof words[0] !== 'number')) throw new Error(`bech32.encode words should be array of numbers, not ${typeof words}`) if (prefix.length === 0) throw new TypeError(`Invalid prefix length ${prefix.length}`) const actualLength = prefix.length + 7 + words.length if (limit !== false && actualLength > limit) throw new TypeError(`Length ${actualLength} exceeds limit ${limit}`) const lowered = prefix.toLowerCase() const sum = bechChecksum(lowered, words, ENCODING_CONST) return `${lowered}1${BECH_ALPHABET.encode(words)}${sum}` as `${Lowercase}1${string}` } function decode( str: `${Prefix}1${string}`, limit?: number | false ): Bech32Decoded function decode(str: string, limit?: number | false): Bech32Decoded function decode(str: string, limit: number | false = 90): Bech32Decoded { if (typeof str !== 'string') throw new Error(`bech32.decode input should be string, not ${typeof str}`) if (str.length < 8 || (limit !== false && str.length > limit)) throw new TypeError(`Wrong string length: ${str.length} (${str}). Expected (8..${limit})`) // don't allow mixed case const lowered = str.toLowerCase() if (str !== lowered && str !== str.toUpperCase()) throw new Error(`String must be lowercase or uppercase`) const sepIndex = lowered.lastIndexOf('1') if (sepIndex === 0 || sepIndex === -1) throw new Error(`Letter "1" must be present between prefix and data only`) const prefix = lowered.slice(0, sepIndex) const data = lowered.slice(sepIndex + 1) if (data.length < 6) throw new Error('Data must be at least 6 characters long') const words = BECH_ALPHABET.decode(data).slice(0, -6) const sum = bechChecksum(prefix, words, ENCODING_CONST) if (!data.endsWith(sum)) throw new Error(`Invalid checksum in ${str}: expected "${sum}"`) return { prefix, words } } const decodeUnsafe = unsafeWrapper(decode) function decodeToBytes(str: string): Bech32DecodedWithArray { const { prefix, words } = decode(str, false) return { prefix, words, bytes: fromWords(words) } } function encodeFromBytes(prefix: string, bytes: Uint8Array) { return encode(prefix, toWords(bytes)) } return { encode, decode, encodeFromBytes, decodeToBytes, decodeUnsafe, fromWords, fromWordsUnsafe, toWords, } } export const bech32: Bech32 = /* @__PURE__ */ genBech32('bech32') export const bech32m: Bech32 = /* @__PURE__ */ genBech32('bech32m') export const utf8: BytesCoder = { encode: (data) => Uint8ArrayToString(data), decode: (str) => stringToUInt8Array(str), } export const hex: BytesCoder = /* @__PURE__ */ chain( radix2(4), alphabet('0123456789abcdef'), join(''), normalize((s: string) => { if (typeof s !== 'string' || s.length % 2) throw new TypeError(`hex.decode: expected string, got ${typeof s} with length ${s.length}`) return s.toLowerCase() }) ) // prettier-ignore const CODERS = { utf8, hex, base16, base32, base64, base64url, base58, base58xmr } type CoderType = keyof typeof CODERS const coderTypeError = 'Invalid encoding type. Available types: utf8, hex, base16, base32, base64, base64url, base58, base58xmr' export const bytesToString = (type: CoderType, bytes: Uint8Array): string => { if (typeof type !== 'string' || !CODERS.hasOwnProperty(type)) throw new TypeError(coderTypeError) if (!isBytes(bytes)) throw new TypeError('bytesToString() expects Uint8Array') return CODERS[type].encode(bytes) } export const str = bytesToString // as in python, but for bytes only export const stringToBytes = (type: CoderType, str: string): Uint8Array => { if (!CODERS.hasOwnProperty(type)) throw new TypeError(coderTypeError) if (typeof str !== 'string') throw new TypeError('stringToBytes() expects string') return CODERS[type].decode(str) } export const bytes = stringToBytes;