UNPKG

@scure/base

Version:

Secure, audited & 0-dep implementation of base64, bech32, base58, base32 & base16

1,352 lines (1,291 loc) 51.5 kB
/*! scure-base - MIT License (c) 2022 Paul Miller (paulmillr.com) */ /** Transforms values between two representations. */ export interface Coder<F, T> { /** * Converts a value from the input representation to the output representation. * @param from - Value in the source representation. * @returns Converted value. */ encode(from: F): T; /** * Converts a value from the output representation back to the input representation. * @param to - Value in the target representation. * @returns Converted value. */ decode(to: T): F; } /** Coder that works with byte arrays and strings. */ export interface BytesCoder extends Coder<Uint8Array, string> { /** * Encodes bytes into a string representation. * @param data - Bytes to encode. * @returns Encoded string. */ encode: (data: Uint8Array) => string; /** * Decodes a string representation into raw bytes. * @param str - Encoded string. * @returns Decoded bytes. */ decode: (str: string) => Uint8Array; } /** * Bytes API type helpers for old + new TypeScript. * * TS 5.6 has `Uint8Array`, while TS 5.9+ made it generic `Uint8Array<ArrayBuffer>`. * We can't use specific return type, because TS 5.6 will error. * We can't use generic return type, because most TS 5.9 software will expect specific type. * * Maps typed-array input leaves to broad forms. * These are compatibility adapters, not ownership guarantees. * * - `TArg` keeps byte inputs broad. * - `TRet` marks byte outputs for TS 5.6 and TS 5.9+ compatibility. */ export type TypedArg<T> = T extends BigInt64Array ? BigInt64Array : T extends BigUint64Array ? BigUint64Array : T extends Float32Array ? Float32Array : T extends Float64Array ? Float64Array : T extends Int16Array ? Int16Array : T extends Int32Array ? Int32Array : T extends Int8Array ? Int8Array : T extends Uint16Array ? Uint16Array : T extends Uint32Array ? Uint32Array : T extends Uint8ClampedArray ? Uint8ClampedArray : T extends Uint8Array ? Uint8Array : never; /** Maps typed-array output leaves to narrow TS-compatible forms. */ export type TypedRet<T> = T extends BigInt64Array ? ReturnType<typeof BigInt64Array.of> : T extends BigUint64Array ? ReturnType<typeof BigUint64Array.of> : T extends Float32Array ? ReturnType<typeof Float32Array.of> : T extends Float64Array ? ReturnType<typeof Float64Array.of> : T extends Int16Array ? ReturnType<typeof Int16Array.of> : T extends Int32Array ? ReturnType<typeof Int32Array.of> : T extends Int8Array ? ReturnType<typeof Int8Array.of> : T extends Uint16Array ? ReturnType<typeof Uint16Array.of> : T extends Uint32Array ? ReturnType<typeof Uint32Array.of> : T extends Uint8ClampedArray ? ReturnType<typeof Uint8ClampedArray.of> : T extends Uint8Array ? ReturnType<typeof Uint8Array.of> : never; /** Recursively adapts byte-carrying API input types. See {@link TypedArg}. */ export type TArg<T> = | T | ([TypedArg<T>] extends [never] ? T extends (...args: infer A) => infer R ? ((...args: { [K in keyof A]: TRet<A[K]> }) => TArg<R>) & { [K in keyof T]: T[K] extends (...args: any) => any ? T[K] : TArg<T[K]>; } : T extends [infer A, ...infer R] ? [TArg<A>, ...{ [K in keyof R]: TArg<R[K]> }] : T extends readonly [infer A, ...infer R] ? readonly [TArg<A>, ...{ [K in keyof R]: TArg<R[K]> }] : T extends (infer A)[] ? TArg<A>[] : T extends readonly (infer A)[] ? readonly TArg<A>[] : T extends Promise<infer A> ? Promise<TArg<A>> : T extends object ? { [K in keyof T]: TArg<T[K]> } : T : TypedArg<T>); /** Recursively adapts byte-carrying API output types. See {@link TypedArg}. */ export type TRet<T> = T extends unknown ? T & ([TypedRet<T>] extends [never] ? T extends (...args: infer A) => infer R ? ((...args: { [K in keyof A]: TArg<A[K]> }) => TRet<R>) & { [K in keyof T]: T[K] extends (...args: any) => any ? T[K] : TRet<T[K]>; } : T extends [infer A, ...infer R] ? [TRet<A>, ...{ [K in keyof R]: TRet<R[K]> }] : T extends readonly [infer A, ...infer R] ? readonly [TRet<A>, ...{ [K in keyof R]: TRet<R[K]> }] : T extends (infer A)[] ? TRet<A>[] : T extends readonly (infer A)[] ? readonly TRet<A>[] : T extends Promise<infer A> ? Promise<TRet<A>> : T extends object ? { [K in keyof T]: TRet<T[K]> } : T : TypedRet<T>) : never; function isBytes(a: unknown): a is Uint8Array { // Plain `instanceof Uint8Array` is too strict for some Buffer / proxy / cross-realm cases. The // fallback still requires a real ArrayBuffer view, so plain JSON-deserialized // `{ constructor: ... }` spoofing is rejected. `BYTES_PER_ELEMENT === 1` keeps the // fallback on byte-oriented views. return ( a instanceof Uint8Array || (ArrayBuffer.isView(a) && a.constructor.name === 'Uint8Array' && 'BYTES_PER_ELEMENT' in a && a.BYTES_PER_ELEMENT === 1) ); } /** Asserts something is Uint8Array. */ function abytes(b: TArg<Uint8Array | undefined>): void { if (!isBytes(b)) throw new TypeError('Uint8Array expected'); } function isArrayOf(isString: boolean, arr: any[]) { if (!Array.isArray(arr)) return false; if (arr.length === 0) return true; if (isString) { return arr.every((item) => typeof item === 'string'); } else { return arr.every((item) => Number.isSafeInteger(item)); } } function afn(input: Function): input is Function { if (typeof input !== 'function') throw new TypeError('function expected'); return true; } function astr(label: string, input: unknown): input is string { if (typeof input !== 'string') throw new TypeError(`${label}: string expected`); return true; } function anumber(n: number): void { if (typeof n !== 'number') throw new TypeError(`number expected, got ${typeof n}`); if (!Number.isSafeInteger(n)) throw new RangeError(`invalid integer: ${n}`); } function aArr(input: any[]) { if (!Array.isArray(input)) throw new TypeError('array expected'); } function astrArr(label: string, input: string[]) { if (!isArrayOf(true, input)) throw new TypeError(`${label}: array of strings expected`); } function anumArr(label: string, input: number[]) { if (!isArrayOf(false, input)) throw new TypeError(`${label}: array of numbers expected`); } // TODO: some recusive type inference so it would check correct order of input/output inside rest? // like <string, number>, <number, bytes>, <bytes, float> type Chain = [Coder<any, any>, ...Coder<any, any>[]]; // Extract info from Coder type type Input<F> = F extends Coder<infer T, any> ? T : never; type Output<F> = F extends Coder<any, infer T> ? T : never; // Generic function for arrays type First<T> = T extends [infer U, ...any[]] ? U : never; type Last<T> = T extends [...any[], infer U] ? U : never; type Tail<T> = T extends [any, ...infer U] ? U : never; type AsChain<C extends Chain, Rest = Tail<C>> = { // C[K] = Coder<Input<C[K]>, Input<Rest[k]>> [K in keyof C]: Coder<Input<C[K]>, Input<K extends keyof Rest ? Rest[K] : any>>; }; /** * @__NO_SIDE_EFFECTS__ */ function chain<T extends Chain & AsChain<T>>(...args: T): Coder<Input<First<T>>, Output<Last<T>>> { 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 }; } /** * Encodes integer radix representation to array of strings using alphabet and back. * Could also be array of strings. * @__NO_SIDE_EFFECTS__ */ function alphabet(letters: string | string[]): Coder<number[], string[]> { // mapping 1 to "b" const lettersA = typeof letters === 'string' ? letters.split('') : letters; const len = lettersA.length; astrArr('alphabet', lettersA); // mapping "b" to 1 const indexes = new Map(lettersA.map((l, i) => [l, i])); return { encode: (digits: number[]) => { aArr(digits); return digits.map((i) => { if (!Number.isSafeInteger(i) || i < 0 || i >= len) throw new Error( `alphabet.encode: digit index outside alphabet "${i}". Allowed: ${letters}` ); return lettersA[i]!; }); }, decode: (input: string[]): number[] => { aArr(input); return input.map((letter) => { astr('alphabet.decode', letter); const i = indexes.get(letter); if (i === undefined) throw new Error(`Unknown letter: "${letter}". Allowed: ${letters}`); return i; }); }, }; } /** * @__NO_SIDE_EFFECTS__ */ function join(separator = ''): Coder<string[], string> { astr('join', separator); // join('') is only lossless when each chunk is already unambiguous, such as single-symbol alphabets. // Multi-character tokens need a separator that cannot appear inside the chunks. return { encode: (from) => { astrArr('join.decode', from); return from.join(separator); }, decode: (to) => { astr('join.decode', to); return to.split(separator); }, }; } /** * Pad strings array so it has integer number of bits * @__NO_SIDE_EFFECTS__ */ function padding(bits: number, chr = '='): Coder<string[], string[]> { anumber(bits); astr('padding', chr); return { encode(data: string[]): string[] { astrArr('padding.encode', data); // Mutates the intermediate token array in place while appending pad chars. // utils.padding callers that need to preserve their input should pass a copy. while ((data.length * bits) % 8) data.push(chr); return data; }, decode(input: string[]): string[] { astrArr('padding.decode', input); let end = input.length; if ((end * bits) % 8) throw new Error('padding: invalid, string should have whole number of bytes'); for (; end > 0 && input[end - 1] === chr; end--) { const last = end - 1; const byte = last * bits; if (byte % 8 === 0) throw new Error('padding: invalid, string has too much padding'); } return input.slice(0, end); }, }; } /** * @__NO_SIDE_EFFECTS__ */ function normalize<T>(fn: (val: T) => T): Coder<T, T> { afn(fn); return { encode: (from: T) => from, decode: (to: T) => fn(to) }; } /** * Slow: O(n^2) time complexity */ function convertRadix(data: number[], from: number, to: number): number[] { // base 1 is impossible if (from < 2) throw new RangeError(`convertRadix: invalid from=${from}, base cannot be less than 2`); if (to < 2) throw new RangeError(`convertRadix: invalid to=${to}, base cannot be less than 2`); aArr(data); if (!data.length) return []; let pos = 0; const res = []; const digits = Array.from(data, (d) => { anumber(d); if (d < 0 || d >= from) throw new Error(`invalid integer: ${d}`); return d; }); const dlen = digits.length; while (true) { let carry = 0; let done = true; for (let i = pos; i < dlen; i++) { const digit = digits[i]!; const fromCarry = from * carry; const digitBase = fromCarry + digit; if ( !Number.isSafeInteger(digitBase) || fromCarry / from !== carry || digitBase - digit !== fromCarry ) { throw new Error('convertRadix: carry overflow'); } const div = digitBase / to; carry = digitBase % to; const rounded = Math.floor(div); 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; } // Preserve explicit leading zero digits so callers like base58 keep zero-prefix semantics. for (let i = 0; i < data.length - 1 && data[i] === 0; i++) res.push(0); return res.reverse(); } const gcd = (a: number, b: number): number => (b === 0 ? a : gcd(b, a % b)); // Maximum carry width before the `pos` cycle repeats. // Residues advance in gcd(from, to) steps, so the largest pre-drain width is from + (to - gcd). const radix2carry = /* @__NO_SIDE_EFFECTS__ */ (from: number, to: number) => from + (to - gcd(from, to)); const powers: number[] = /* @__PURE__ */ (() => { let res = []; for (let i = 0; i < 40; i++) res.push(2 ** i); return res; })(); /** * Implemented with numbers, because BigInt is 5x slower */ function convertRadix2(data: number[], from: number, to: number, padding: boolean): number[] { aArr(data); if (from <= 0 || from > 32) throw new RangeError(`convertRadix2: wrong from=${from}`); if (to <= 0 || to > 32) throw new RangeError(`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 max = powers[from]!; const mask = powers[to]! - 1; const res: number[] = []; for (const n of data) { anumber(n); if (n >= max) 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); const pow = powers[pos]; if (pow === undefined) throw new Error('invalid carry'); carry &= pow - 1; // clean carry, otherwise it will cause overflow } carry = (carry << (to - pos)) & mask; // Canonical decode paths reject leftover whole input words and non-zero pad bits. // For Bech32 5->8 regrouping, this is the "4 bits or less, all zeroes" tail rule. if (!padding && pos >= from) throw new Error('Excess padding'); if (!padding && carry > 0) throw new Error(`Non-zero padding: ${carry}`); if (padding && pos > 0) res.push(carry >>> 0); return res; } /** * @__NO_SIDE_EFFECTS__ */ function radix(num: number): TRet<Coder<Uint8Array, number[]>> { anumber(num); const _256 = 2 ** 8; // Base-range and carry-overflow checks live in convertRadix so encode/decode reject unsupported bases symmetrically. return { encode: (bytes: TArg<Uint8Array>) => { if (!isBytes(bytes)) throw new TypeError('radix.encode input should be Uint8Array'); return convertRadix(Array.from(bytes), _256, num); }, decode: (digits: number[]) => { anumArr('radix.decode', digits); return Uint8Array.from(convertRadix(digits, num, _256)); }, }; } /** * 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): TRet<Coder<Uint8Array, number[]>> { anumber(bits); if (bits <= 0 || bits > 32) throw new RangeError('radix2: bits should be in (0..32]'); if (radix2carry(8, bits) > 32 || radix2carry(bits, 8) > 32) throw new RangeError('radix2: carry overflow'); // revPadding flips which direction allows a partial zero tail. // Default pads 8->bits and rejects extra bits on bits->8; `true` does the opposite. return { encode: (bytes: TArg<Uint8Array>) => { if (!isBytes(bytes)) throw new TypeError('radix2.encode input should be Uint8Array'); return convertRadix2(Array.from(bytes), 8, bits, !revPadding); }, decode: (digits: number[]) => { anumArr('radix2.decode', digits); return Uint8Array.from(convertRadix2(digits, bits, 8, revPadding)); }, }; } type ArgumentTypes<F extends Function> = F extends (...args: infer A) => any ? A : never; type BytesFn = (data: TArg<Uint8Array>) => TRet<Uint8Array>; function unsafeWrapper<T extends (...args: any) => any>(fn: T) { afn(fn); return function (...args: ArgumentTypes<T>): ReturnType<T> | void { // Only for *Unsafe APIs that intentionally collapse validation failures to `undefined`. // Do not wrap code that needs to preserve exception details. try { return fn.apply(null, args); } catch (e) {} }; } function checksum(len: number, fn: TArg<BytesFn>): TRet<Coder<Uint8Array, Uint8Array>> { anumber(len); // Reject degenerate zero-byte checksums up front so callers don't accidentally // build a no-op checksum stage. if (len <= 0) throw new RangeError(`checksum length must be positive: ${len}`); afn(fn); const _fn = fn as BytesFn; // Uses the first `len` bytes of fn(data) in both directions. // Current call sites rely on `len > 0` and checksum functions that return at least that many bytes. return { encode(data: TArg<Uint8Array>) { if (!isBytes(data)) throw new TypeError('checksum.encode: input should be Uint8Array'); const sum = _fn(data).slice(0, len); const res = new Uint8Array(data.length + len); res.set(data); res.set(sum, data.length); return res; }, decode(data: TArg<Uint8Array>) { if (!isBytes(data)) throw new TypeError('checksum.decode: input should be Uint8Array'); const payload = data.slice(0, -len); const oldChecksum = data.slice(-len); const newChecksum = _fn(payload).slice(0, len); for (let i = 0; i < len; i++) if (newChecksum[i] !== oldChecksum[i]) throw new Error('Invalid checksum'); return payload; }, }; } // prettier-ignore /** * Low-level building blocks used by the exported codecs. * @example * Build a radix-32 coder from the low-level helpers. * ```ts * import { utils } from '@scure/base'; * utils.radix2(5).encode(Uint8Array.from([1, 2, 3])); * ``` */ export const utils: { alphabet: typeof alphabet; chain: typeof chain; checksum: typeof checksum; convertRadix: typeof convertRadix; convertRadix2: typeof convertRadix2; radix: typeof radix; radix2: typeof radix2; join: typeof join; padding: typeof padding; } = /* @__PURE__ */ Object.freeze({ alphabet, chain, checksum, convertRadix, convertRadix2, radix, radix2, join, padding, }); // RFC 4648 aka RFC 3548 // --------------------- /** * base16 encoding from RFC 4648. * This codec uses RFC 4648 Table 5's uppercase alphabet directly. * RFC 4648 §8 calls base16 "case-insensitive hex encoding", but we intentionally do not case-fold decode input here. * Use `hex` for case-insensitive hex decoding. * @example * ```js * base16.encode(Uint8Array.from([0x12, 0xab])); * // => '12AB' * ``` */ export const base16: BytesCoder = /* @__PURE__ */ Object.freeze( chain(radix2(4), alphabet('0123456789ABCDEF'), join('')) ); /** * base32 encoding from RFC 4648. Has padding. * RFC 4648 §6 Table 3 uses uppercase letters, and RFC 4648 §3.4 allows applications to choose * upper- or lowercase alphabets. We keep the published uppercase table and do not case-fold decode input. * Use `base32nopad` for unpadded version. * Also check out `base32hex`, `base32hexnopad`, `base32crockford`. * @example * ```js * base32.encode(Uint8Array.from([0x12, 0xab])); * // => 'CKVQ====' * base32.decode('CKVQ===='); * // => Uint8Array.from([0x12, 0xab]) * ``` */ export const base32: BytesCoder = /* @__PURE__ */ Object.freeze( chain(radix2(5), alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'), padding(5), join('')) ); /** * base32 encoding from RFC 4648. No padding. * This variant inherits RFC 4648 base32's uppercase table and intentionally does not case-fold decode input. * Use `base32` for padded version. * Also check out `base32hex`, `base32hexnopad`, `base32crockford`. * @example * ```js * base32nopad.encode(Uint8Array.from([0x12, 0xab])); * // => 'CKVQ' * base32nopad.decode('CKVQ'); * // => Uint8Array.from([0x12, 0xab]) * ``` */ export const base32nopad: BytesCoder = /* @__PURE__ */ Object.freeze( chain(radix2(5), alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'), join('')) ); /** * base32 encoding from RFC 4648. Padded. Compared to ordinary `base32`, slightly different alphabet. * RFC 4648 §7 Table 4 uses uppercase letters, and we intentionally keep that table without case-folding decode input. * Use `base32hexnopad` for unpadded version. * @example * ```js * base32hex.encode(Uint8Array.from([0x12, 0xab])); * // => '2ALG====' * base32hex.decode('2ALG===='); * // => Uint8Array.from([0x12, 0xab]) * ``` */ export const base32hex: BytesCoder = /* @__PURE__ */ Object.freeze( chain(radix2(5), alphabet('0123456789ABCDEFGHIJKLMNOPQRSTUV'), padding(5), join('')) ); /** * base32 encoding from RFC 4648. No padding. Compared to ordinary `base32`, slightly different alphabet. * This variant inherits RFC 4648 base32hex's uppercase table and intentionally does not case-fold decode input. * Use `base32hex` for padded version. * @example * ```js * base32hexnopad.encode(Uint8Array.from([0x12, 0xab])); * // => '2ALG' * base32hexnopad.decode('2ALG'); * // => Uint8Array.from([0x12, 0xab]) * ``` */ export const base32hexnopad: BytesCoder = /* @__PURE__ */ Object.freeze( chain(radix2(5), alphabet('0123456789ABCDEFGHIJKLMNOPQRSTUV'), join('')) ); /** * base32 encoding from RFC 4648. Doug Crockford's version. * See {@link https://www.crockford.com/base32.html | Douglas Crockford's Base32}. * @example * ```js * base32crockford.encode(Uint8Array.from([0x12, 0xab])); * // => '2ANG' * base32crockford.decode('2ANG'); * // => Uint8Array.from([0x12, 0xab]) * ``` */ export const base32crockford: BytesCoder = /* @__PURE__ */ Object.freeze( chain( radix2(5), alphabet('0123456789ABCDEFGHJKMNPQRSTVWXYZ'), join(''), normalize((s: string) => s.toUpperCase().replace(/O/g, '0').replace(/[IL]/g, '1')) ) ); // Built-in base64 conversion https://caniuse.com/mdn-javascript_builtins_uint8array_frombase64 // Require both directions before taking the native fast path, so base64/base64url don't mix native and JS behavior. // prettier-ignore const hasBase64Builtin: boolean = /* @__PURE__ */ (() => typeof (Uint8Array as any).from([]).toBase64 === 'function' && typeof (Uint8Array as any).fromBase64 === 'function')(); // Native `Uint8Array.fromBase64()` accepts these ASCII whitespace chars. // Reject them first so the native base64 path still follows RFC 4648 §3.3. // ASCII whitespace is U+0009 TAB, U+000A LF, U+000C FF, U+000D CR, or U+0020 SPACE const ASCII_WHITESPACE = /[\t\n\f\r ]/; const decodeBase64Builtin = (s: string, isUrl: boolean) => { astr('base64', s); const alphabet = isUrl ? 'base64url' : 'base64'; // Per spec, .fromBase64 already throws on any other non-alphabet symbols except ASCII whitespace // And checking just for whitespace makes decoding about 3x faster than a full range check. // lastChunkHandling: 'strict' rejects loose tails and non-zero pad bits so native decoding stays canonical. if (s.length > 0 && ASCII_WHITESPACE.test(s)) throw new Error('invalid base64'); return (Uint8Array as any).fromBase64(s, { alphabet, lastChunkHandling: 'strict' }); }; /** * base64 from RFC 4648. Padded. * Use `base64nopad` for unpadded version. * Also check out `base64url`, `base64urlnopad`. * Falls back to built-in function, when available. * @example * ```js * base64.encode(Uint8Array.from([0x12, 0xab])); * // => 'Eqs=' * base64.decode('Eqs='); * // => Uint8Array.from([0x12, 0xab]) * ``` */ // prettier-ignore export const base64: BytesCoder = /* @__PURE__ */ Object.freeze(hasBase64Builtin ? { encode(b) { abytes(b); return (b as any).toBase64(); }, decode(s) { return decodeBase64Builtin(s, false); }, } : chain( radix2(6), alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'), padding(6), join('') )); /** * base64 from RFC 4648. No padding. * Use `base64` for padded version. * @example * ```js * base64nopad.encode(Uint8Array.from([0x12, 0xab])); * // => 'Eqs' * base64nopad.decode('Eqs'); * // => Uint8Array.from([0x12, 0xab]) * ``` */ export const base64nopad: BytesCoder = /* @__PURE__ */ Object.freeze( chain( radix2(6), alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'), join('') ) ); /** * base64 from RFC 4648, using URL-safe alphabet. Padded. * Use `base64urlnopad` for unpadded version. * Falls back to built-in function, when available. * @example * ```js * base64url.encode(Uint8Array.from([0x12, 0xab])); * // => 'Eqs=' * base64url.decode('Eqs='); * // => Uint8Array.from([0x12, 0xab]) * ``` */ // prettier-ignore export const base64url: BytesCoder = /* @__PURE__ */ Object.freeze(hasBase64Builtin ? { encode(b) { abytes(b); return (b as any).toBase64({ alphabet: 'base64url' }); }, decode(s) { return decodeBase64Builtin(s, true); }, } : chain( radix2(6), alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'), padding(6), join('') )); /** * base64 from RFC 4648, using URL-safe alphabet. No padding. * Use `base64url` for padded version. * @example * ```js * base64urlnopad.encode(Uint8Array.from([0x12, 0xab])); * // => 'Eqs' * base64urlnopad.decode('Eqs'); * // => Uint8Array.from([0x12, 0xab]) * ``` */ export const base64urlnopad: BytesCoder = /* @__PURE__ */ Object.freeze( chain( radix2(6), alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'), join('') ) ); // base58 code // ----------- const genBase58 = /* @__NO_SIDE_EFFECTS__ */ (abc: string) => chain(radix(58), alphabet(abc), join('')); /** * base58: base64 without ambigous characters +, /, 0, O, I, l. * Quadratic (O(n^2)) - so, can't be used on large inputs. * @example * ```js * const text = base58.encode(Uint8Array.from([0, 1, 2])); * base58.decode(text); * // => Uint8Array.from([0, 1, 2]) * ``` */ export const base58: BytesCoder = /* @__PURE__ */ Object.freeze( genBase58('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz') ); /** * base58: flickr version. Check out `base58`. * @example * Round-trip bytes with the Flickr alphabet. * ```ts * const text = base58flickr.encode(Uint8Array.from([0, 1, 2])); * base58flickr.decode(text); * ``` */ export const base58flickr: BytesCoder = /* @__PURE__ */ Object.freeze( genBase58('123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ') ); /** * base58: XRP version. Check out `base58`. * @example * Round-trip bytes with the XRP alphabet. * ```ts * const text = base58xrp.encode(Uint8Array.from([0, 1, 2])); * base58xrp.decode(text); * ``` */ export const base58xrp: BytesCoder = /* @__PURE__ */ Object.freeze( genBase58('rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz') ); // Data len (index) -> encoded block len. // Monero pads each 1..8-byte block to this fixed base58 width so decode can recover the tail length. const XMR_BLOCK_LEN = [0, 2, 3, 5, 6, 7, 9, 10, 11]; /** * base58: XMR version. Check out `base58`. * 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. * @example * Round-trip bytes with the Monero block codec. * ```ts * const text = base58xmr.encode(Uint8Array.from([0, 1, 2])); * base58xmr.decode(text); * ``` */ export const base58xmr: BytesCoder = /* @__PURE__ */ Object.freeze({ encode(data: TArg<Uint8Array>) { abytes(data); 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) { astr('base58xmr.decode', str); 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); }, }); /** * Method, which creates base58check encoder. * Requires function, calculating sha256. * Callers must include any version bytes in `data`; this helper only applies the * 4-byte double-SHA256 checksum used by Bitcoin Base58Check. * @param sha256 - Function used to calculate the checksum hash. * @returns base58check codec using 4 checksum bytes. * @throws On wrong argument types. {@link TypeError} * @example * Create a base58check codec from a SHA-256 implementation. * ```ts * import { createBase58check } from '@scure/base'; * import { sha256 } from '@noble/hashes/sha2.js'; * const coder = createBase58check(sha256); * coder.encode(Uint8Array.from([1, 2, 3])); * ``` */ export const createBase58check = (sha256: TArg<BytesFn>): BytesCoder => { // Validate the hash function at construction time so wrong inputs fail before returning a coder. afn(sha256); const _sha256 = sha256 as BytesFn; return chain( checksum(4, (data: TArg<Uint8Array>) => _sha256(_sha256(data))), base58 ); }; /** * Use `createBase58check` instead. * @deprecated Use {@link createBase58check} instead. * Callers must include any version bytes in `data`; this alias keeps the same * 4-byte double-SHA256 checksum behavior as `createBase58check`. * @param sha256 - Function used to calculate the checksum hash. * @returns base58check codec using 4 checksum bytes. * @example * Create a base58check codec with the deprecated alias. * ```ts * import { base58check } from '@scure/base'; * import { sha256 } from '@noble/hashes/sha2.js'; * const coder = base58check(sha256); * coder.encode(Uint8Array.from([1, 2, 3])); * ``` */ export const base58check: (sha256: TArg<BytesFn>) => BytesCoder = createBase58check; // Bech32 code // ----------- /** Result of bech32 decoding. */ export interface Bech32Decoded<Prefix extends string = string> { /** Human-readable bech32 prefix. */ prefix: Prefix; /** Decoded 5-bit word payload. */ words: number[]; } /** Result of bech32 decoding with original bytes attached. */ export interface Bech32DecodedWithArray<Prefix extends string = string> { /** Human-readable bech32 prefix. */ prefix: Prefix; /** Decoded 5-bit word payload. */ words: number[]; /** Decoded payload converted back into raw bytes. */ bytes: Uint8Array; } // BIP 173 character table: data values 0..31 map to `qpzry9x8gf2tvdw0s3jn54khce6mua7l`. const BECH_ALPHABET: Coder<number[], string> = chain( alphabet('qpzry9x8gf2tvdw0s3jn54khce6mua7l'), join('') ); // BIP 173 `bech32_polymod` GEN coefficients. const POLYMOD_GENERATORS = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; // BIP 173 step split: this applies the polymod state transition before callers xor in the next 5-bit value. 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; } 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); // BIP 173/BIP 350: xor the final checksum constant, then emit the 30-bit state as six 5-bit symbols. chk ^= encodingConst; return BECH_ALPHABET.encode(convertRadix2([chk % powers[30]!], 30, 5, false)); } /** bech32 codec surface. */ export interface Bech32 { /** * Encodes a human-readable prefix and 5-bit words into a bech32 string. * @param prefix - Human-readable prefix. * @param words - 5-bit words or raw bytes. * @param limit - Maximum accepted output length, or `false` to disable the limit. * @returns Encoded bech32 string. */ encode<Prefix extends string>( prefix: Prefix, words: number[] | Uint8Array, limit?: number | false ): `${Lowercase<Prefix>}1${string}`; /** * Decodes a bech32 string into prefix and words. * @param str - Encoded bech32 string. * @param limit - Maximum accepted input length, or `false` to disable the limit. * @returns Decoded prefix and 5-bit words. */ decode<Prefix extends string>( str: `${Prefix}1${string}`, limit?: number | false ): Bech32Decoded<Prefix>; decode(str: string, limit?: number | false): Bech32Decoded; /** * Encodes raw bytes by first converting them to 5-bit words. * @param prefix - Human-readable prefix. * @param bytes - Raw bytes to encode. * @returns Encoded bech32 string. */ encodeFromBytes(prefix: string, bytes: Uint8Array): string; /** * Decodes a bech32 string and converts the payload back into bytes. * @param str - Encoded bech32 string. * @returns Decoded prefix, words, and bytes. */ decodeToBytes(str: string): Bech32DecodedWithArray; /** * Decodes a bech32 string, returning `undefined` instead of throwing on invalid input. * @param str - Encoded bech32 string. * @param limit - Maximum accepted input length, or `false` to disable the limit. * @returns Decoded prefix and words, or `undefined` for invalid input. */ decodeUnsafe(str: string, limit?: number | false): void | Bech32Decoded<string>; /** * Converts 5-bit words back into raw bytes. * @param to - 5-bit words to decode. * @returns Decoded bytes. */ fromWords(to: number[]): Uint8Array; /** * Converts 5-bit words back into raw bytes, returning `undefined` instead of throwing. * @param to - 5-bit words to decode. * @returns Decoded bytes, or `undefined` for invalid input. */ fromWordsUnsafe(to: number[]): void | Uint8Array; /** * Converts raw bytes into 5-bit words for bech32 encoding. * @param from - Raw bytes to convert. * @returns 5-bit words. */ toWords(from: Uint8Array): number[]; } /** * @__NO_SIDE_EFFECTS__ */ function genBech32(encoding: 'bech32' | 'bech32m'): TRet<Bech32> { // BIP 173 uses final xor constant 1; BIP 350 swaps in 0x2bc830a3 for Bech32m. 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 extends string>( prefix: Prefix, words: TArg<number[] | Uint8Array>, limit: number | false = 90 ): `${Lowercase<Prefix>}1${string}` { astr('bech32.encode prefix', prefix); if (isBytes(words)) words = Array.from(words); anumArr('bech32.encode', words); const plen = prefix.length; if (plen === 0) throw new TypeError(`Invalid prefix length ${plen}`); // Total output is hrp + `1` separator + payload words + 6 checksum chars. const actualLength = plen + 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<Prefix>}1${string}`; } function decode<Prefix extends string>( str: `${Prefix}1${string}`, limit?: number | false ): Bech32Decoded<Prefix>; function decode(str: string, limit?: number | false): Bech32Decoded; function decode(str: string, limit: number | false = 90): Bech32Decoded { astr('bech32.decode input', str); const slen = str.length; // Minimum length is 1-char hrp + `1` separator + 6-char checksum. if (slen < 8 || (limit !== false && slen > limit)) throw new TypeError(`invalid string length: ${slen} (${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): TRet<Bech32DecodedWithArray> { // Keep the byte helper unbounded; callers that need the default BIP 173 length cap should use decode(str). const { prefix, words } = decode(str, false); return { prefix, words, bytes: fromWords(words) as TRet<Uint8Array>, } as TRet<Bech32DecodedWithArray>; } function encodeFromBytes(prefix: string, bytes: TArg<Uint8Array>) { // Keep the convenience wrapper on encode()'s default 90-char cap; custom limits should call encode(prefix, toWords(bytes), limit). return encode(prefix, toWords(bytes)); } return { encode, decode, encodeFromBytes, decodeToBytes, decodeUnsafe, fromWords, fromWordsUnsafe, toWords, }; } /** * bech32 from BIP 173. Operates on words. * For high-level helpers, check out {@link https://github.com/paulmillr/scure-btc-signer | scure-btc-signer}. * @example * Convert bytes to words, encode them, then decode back. * ```ts * const words = bech32.toWords(Uint8Array.from([1, 2, 3])); * const text = bech32.encode('bc', words); * bech32.decode(text); * ``` */ export const bech32: TRet<Bech32> = /* @__PURE__ */ Object.freeze(genBech32('bech32')); /** * bech32m from BIP 350. Operates on words. * It was to mitigate `bech32` weaknesses. * For high-level helpers, check out {@link https://github.com/paulmillr/scure-btc-signer | scure-btc-signer}. * @example * Convert bytes to words, encode them with bech32m, then decode back. * ```ts * const words = bech32m.toWords(Uint8Array.from([1, 2, 3])); * const text = bech32m.encode('bc', words); * bech32m.decode(text); * ``` */ export const bech32m: TRet<Bech32> = /* @__PURE__ */ Object.freeze(genBech32('bech32m')); declare const TextEncoder: any; declare const TextDecoder: any; /** * ASCII-to-byte decoder. Rejects non-ASCII text and bytes instead of doing UTF-8 replacement. * Method names follow `BytesCoder`, so `encode(bytes)` returns a string and `decode(string)` returns bytes. * @example * ```js * const b = ascii.decode("ABC"); // => new Uint8Array([ 65, 66, 67 ]) * const str = ascii.encode(b); // "ABC" * ``` */ export const ascii: TRet<BytesCoder> = /* @__PURE__ */ Object.freeze({ encode(data: TArg<Uint8Array>) { abytes(data); let res = ''; for (let i = 0; i < data.length; i++) { const byte = data[i]!; // ASCII is 7-bit; reject bytes outside 0x00..0x7f instead of silently widening to // Latin-1/UTF-8. if (byte > 127) throw new RangeError(`bytes contain non-ASCII byte ${byte} at position ${i}`); res += String.fromCharCode(byte); } return res; }, decode(str: string) { if (typeof str !== 'string') throw new TypeError('ascii string expected, got ' + typeof str); const res = new Uint8Array(str.length); for (let i = 0; i < str.length; i++) { // Indexed access is much faster than Uint8Array.from(str, mapFn) here and keeps // exact error positions. const charCode = str.charCodeAt(i); if (charCode > 127) { throw new RangeError( `string contains non-ASCII character "${str[i]}" with code ${charCode} at position ${i}` ); } res[i] = charCode; } return res; }, }); const _isWellFormedShim = (str: string): boolean => { // encodeURI rejects malformed UTF-16, giving a compact fallback that matches native // isWellFormed on our tests/fuzz corpus. try { return encodeURI(str) !== null; } catch { return false; } }; const _isWellFormed: (str: string) => boolean = /* @__PURE__ */ (() => // Pick the native check once so utf8.decode doesn't re-probe String.prototype on every call. typeof ('' as any).isWellFormed === 'function' ? (str) => (str as any).isWellFormed() : _isWellFormedShim)(); // This fallback stays small because strict UTF-8 only needs fatal decoding plus well-formed // UTF-16 checks, not the replacement, streaming, or legacy-encoding behavior of full platform // text codecs. const utf8Fallback: BytesCoder = /* @__PURE__ */ Object.freeze({ encode(data: TArg<Uint8Array>) { abytes(data); let res = ''; for (let i = 0; i < data.length; ) { const a = data[i++]!; if (a < 0b1000_0000) { res += String.fromCharCode(a); continue; } if (a < 0b1100_0010 || i >= data.length) throw new TypeError(`invalid utf8 at byte ${i - 1}`); const b = data[i++]!; if ((b & 0b1100_0000) !== 0b1000_0000) throw new TypeError(`invalid utf8 at byte ${i - 1}`); let cp = ((a & 0b0001_1111) << 6) | (b & 0b0011_1111); if (a >= 0b1110_0000) { if (i >= data.length) throw new TypeError(`invalid utf8 at byte ${i - 1}`); const c = data[i++]!; if ( (c & 0b1100_0000) !== 0b1000_0000 || (a === 0b1110_0000 && b < 0b1010_0000) || (a === 0xed && b >= 0b1010_0000) ) throw new TypeError(`invalid utf8 at byte ${i - 1}`); cp = ((a & 0b0000_1111) << 12) | ((b & 0b0011_1111) << 6) | (c & 0b0011_1111); if (a >= 0b1111_0000) { if (i >= data.length) throw new TypeError(`invalid utf8 at byte ${i - 1}`); const d = data[i++]!; if ( a > 0b1111_0100 || (d & 0b1100_0000) !== 0b1000_0000 || (a === 0b1111_0000 && b < 0b1001_0000) || (a === 0b1111_0100 && b >= 0b1001_0000) ) throw new TypeError(`invalid utf8 at byte ${i - 1}`); cp = ((a & 7) << 18) | ((b & 0b0011_1111) << 12) | ((c & 0b0011_1111) << 6) | (d & 0b0011_1111); } } if (cp < 0x10000) res += String.fromCharCode(cp); else { cp -= 0x10000; res += String.fromCharCode((cp >> 10) + 0xd800, (cp & 0x3ff) + 0xdc00); } } return res; }, decode(str: string) { astr('utf8', str); if (!_isWellFormed(str)) throw new TypeError('utf8 expected well-formed string'); // Direct Uint8Array writes are much faster than number[] + Uint8Array.from on Hermes and // large Node inputs. const res = new Uint8Array(str.length * 3); let pos = 0; for (let i = 0; i < str.length; i++) { let c = str.charCodeAt(i); if (c < 0b1000_0000) { res[pos++] = c; continue; } if (c >= 0xd800 && c <= 0xdfff) { const d = str.charCodeAt(++i); c = 0x10000 + ((c - 0xd800) << 10) + d - 0xdc00; } if (c >= 0x10000) { res[pos++] = (c >> 18) | 0b1111_0000; res[pos++] = ((c >> 12) & 0b0011_1111) | 0b1000_0000; } else if (c >= 0x800) res[pos++] = (c >> 12) | 0b1110_0000; else res[pos++] = (c >> 6) | 0b1100_0000; if (c >= 0x800) res[pos++] = ((c >> 6) & 0b0011_1111) | 0b1000_0000; res[pos++] = (c & 0b0011_1111) | 0b1000_0000; } return res.subarray(0, pos); }, }); /** * Strict UTF-8-to-byte decoder. Uses built-in TextDecoder / TextEncoder when available. * Method names follow `BytesCoder`, so `encode(bytes)` returns a string and * `decode(string)` returns bytes. * `encode(bytes)` requires Uint8Array input, preserves an explicit leading BOM, and * throws on invalid UTF-8 bytes. * `decode(string)` requires a primitive string and throws on malformed UTF-16 strings with * lone surrogates. * @example * ```js * const b = utf8.decode("hey"); // => new Uint8Array([ 104, 101, 121 ]) * const str = utf8.encode(b); // "hey" * ``` */ export const utf8: BytesCoder = /* @__PURE__ */ (() => { let _utf8Encoder: any; let _utf8Decoder: any; const utf8Builtin: BytesCoder = { // ignoreBOM preserves an explicit leading U+FEFF; // fatal rejects invalid UTF-8 bytes instead of replacing them. encode(data) { abytes(data); return ( _utf8Decoder || (_utf8Decoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true })) ).decode(data); }, decode(str) { astr('utf8', str); if (!_isWellFormed(str)) throw new TypeError('utf8 expected well-formed string'); return (_utf8Encoder || (_utf8Encoder = new TextEncoder())).encode(str); }, }; return Object.freeze({ // Select each direction once at module init, since // TextEncoder and TextDecoder can exist independently. encode: typeof TextDecoder === 'function' ? utf8Builtin.encode : utf8Fallback.encode, decode: typeof TextEncoder === 'function' ? utf8Builtin.decode : utf8Fallback.decode, }); })(); // Keep fallback parity probes behind a test-only export until runtime fallback behavior is decided. export const __TESTS: { utf8Fallback: BytesCoder; _isWellFormedShim: (str: string) => boolean; } = /* @__PURE__ */ Object.freeze({ utf8Fallback: utf8Fallback, _isWellFormedShim: _isWellFormedShim, }); // Built-in hex conversion https://caniuse.com/mdn-javascript_builtins_uint8array_fromhex // prettier-ignore const hasHexBuiltin: boolean = /* @__PURE__ */ (() => // Require both directions before enabling the native hex path so encode/decode stay symmetric. typeof (Uint8Array as any).from([]).toHex === 'function' && typeof (Uint8Array as any).fromHex === 'function')(); // prettier-ignore const hexBuiltin: BytesCoder = { // Keep local type guards so the native path preserves library-level input errors. // Native toHex emits lowercase hex, matching the fallback alphabet and Node's hex strings. encode(data) { abytes(data); return (data as any).toHex(); }, // Native fromHex accepts either hex case and rejects odd-length / non-hex syntax. decode(s) { astr('hex', s); return (Uint8Array as any).fromHex(s); }, }; /** * hex string decoder. Uses built-in function, when available. * Lowercase codec; unlike `base16`, this variant accepts either hex case and emits lowercase. * @example * ```js * const b = hex.decode("0102ff"); // => new Uint8Array([ 1, 2, 255 ]) * const str = hex.encode(b); // "0102ff" * ``` */ export const hex: BytesCoder = /* @__PURE__ */ Object.freeze( hasHexBuiltin ? hexBuiltin : chain( radix2(4), alphabet('0123456789abcdef'), join(''), normalize((s: string) => { if (typeof s !== 'string' || s.length % 2 !== 0) throw new TypeError( `hex.decode: expected string, got ${typeof s} with length ${s.length}` ); return s.toLowerCase(); }) ) ); /** Built-in codecs exposed through the deprecated string conversion helpers. */ export type SomeCoders = { /** UTF-8 string codec. */ utf8: BytesCoder; /** Hex codec. */ hex: BytesCoder; /** Uppercase RFC 4648 base16 codec. */ base16: BytesCoder; /** RFC 4648 base32 codec with padding. */ base32: BytesCoder; /** RFC 4648 base64 codec with padding. */ base64: BytesCoder; /** URL-safe base64 codec without `+` or `/`. */ base64url: BytesCoder; /** Bitcoin-style base58 codec. */ base58: BytesCoder; /** Monero-style base58 codec. */ base58xmr: BytesCoder; }; // prettier-ignore // Keep this registry aligned with CoderType/coderTypeError; only byte<->string codecs belong here. const CODERS: SomeCoders = { utf8, hex, base16, base32, base64, base64url, base58, base58xmr }; type CoderType = keyof SomeCoders; const coderTypeError = 'Invalid encoding type. Available types: utf8, hex, base16, base32, base64, base64url, base58, base58xmr'; /** * Encodes bytes with one of the built-in codecs. * @deprecated Use the codec directly, for example `hex.encode(bytes)`. * @param type - Codec name. * @param bytes - Bytes to encode. * @returns Encoded string. * @throws On wrong argument types. {@link TypeError} * @example * ```ts * bytesToString('hex', Uint8Array.from([1, 2, 255])); * ``` */ export const bytesToString = (type: CoderType, bytes: TArg<Uint8Array>): string => { if (typeof type !== 'string' || !CODERS.hasOwnProperty(type)) throw new TypeError(coderTypeError); if (!isBytes(bytes)) throw new TypeError('bytesToString() expects Uint8Array'); return CODERS[