ox
Version:
264 lines (227 loc) • 7.25 kB
text/typescript
import type * as Errors from './Errors.js'
import { BaseError } from './Errors.js'
/**
* Encodes data bytes with a human-readable part (HRP) into a bech32m string (BIP-350).
*
* @example
* ```ts twoslash
* import { Bech32m } from 'ox'
*
* const encoded = Bech32m.encode('tempo', new Uint8Array(20))
* // @log: 'tempo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqwa7xtm'
* ```
*
* @param hrp - The human-readable part (e.g. `"tempo"`, `"tempoz"`).
* @param data - The data bytes to encode.
* @returns The bech32m-encoded string.
*/
export function encode(
hrp: string,
data: Uint8Array,
options: encode.Options = {},
): string {
const { limit = 90 } = options
hrp = hrp.toLowerCase()
if (hrp.length === 0) throw new InvalidHrpError()
for (let i = 0; i < hrp.length; i++) {
const c = hrp.charCodeAt(i)
if (c < 33 || c > 126) throw new InvalidHrpError()
}
const data5 = convertBits(data, 8, 5, true)
if (hrp.length + 1 + data5.length + 6 > limit)
throw new ExceedsLengthError({ limit })
const checksum = createChecksum(hrp, data5)
let result = hrp + '1'
for (const d of data5.concat(checksum)) result += alphabet[d]
return result
}
export declare namespace encode {
type Options = {
/** Maximum length of the encoded string. @default 90 */
limit?: number | undefined
}
type ErrorType = InvalidHrpError | ExceedsLengthError | Errors.GlobalErrorType
}
/**
* Decodes a bech32m string (BIP-350) into a human-readable part and data bytes.
*
* @example
* ```ts twoslash
* import { Bech32m } from 'ox'
*
* const { hrp, data } = Bech32m.decode('tempo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqwa7xtm')
* // @log: { hrp: 'tempo', data: Uint8Array(20) }
* ```
*
* @param str - The bech32m-encoded string to decode.
* @returns The decoded HRP and data bytes.
*/
export function decode(
str: string,
options: decode.Options = {},
): decode.ReturnType {
const { limit = 90 } = options
if (str.length > limit) throw new ExceedsLengthError({ limit })
if (str !== str.toLowerCase() && str !== str.toUpperCase())
throw new MixedCaseError()
const lower = str.toLowerCase()
const pos = lower.lastIndexOf('1')
if (pos === -1) throw new NoSeparatorError()
if (pos === 0) throw new InvalidHrpError()
if (pos + 7 > lower.length) throw new InvalidChecksumError()
const hrp = lower.slice(0, pos)
for (let i = 0; i < hrp.length; i++) {
const c = hrp.charCodeAt(i)
if (c < 33 || c > 126) throw new InvalidHrpError()
}
const dataChars = lower.slice(pos + 1)
const data5: number[] = []
for (const c of dataChars) {
const v = alphabetMap[c]
if (v === undefined) throw new InvalidCharacterError({ character: c })
data5.push(v)
}
if (!verifyChecksum(hrp, data5)) throw new InvalidChecksumError()
const data8 = convertBits(data5.slice(0, -6), 5, 8, false)
return { hrp, data: new Uint8Array(data8) }
}
export declare namespace decode {
type Options = {
/** Maximum length of the encoded string. @default 90 */
limit?: number | undefined
}
type ReturnType = {
/** The human-readable part. */
hrp: string
/** The decoded data bytes. */
data: Uint8Array
}
type ErrorType =
| NoSeparatorError
| InvalidChecksumError
| InvalidCharacterError
| InvalidPaddingError
| MixedCaseError
| InvalidHrpError
| ExceedsLengthError
| Errors.GlobalErrorType
}
/** Thrown when a bech32m string has no separator. */
export class NoSeparatorError extends BaseError {
override readonly name = 'Bech32m.NoSeparatorError'
constructor() {
super('Bech32m string has no separator.')
}
}
/** Thrown when a bech32m string has an invalid checksum. */
export class InvalidChecksumError extends BaseError {
override readonly name = 'Bech32m.InvalidChecksumError'
constructor() {
super('Invalid bech32m checksum.')
}
}
/** Thrown when a bech32m string contains an invalid character. */
export class InvalidCharacterError extends BaseError {
override readonly name = 'Bech32m.InvalidCharacterError'
constructor({ character }: { character: string }) {
super(`Invalid bech32m character: "${character}".`)
}
}
/** Thrown when the padding bits are invalid during base32 conversion. */
export class InvalidPaddingError extends BaseError {
override readonly name = 'Bech32m.InvalidPaddingError'
constructor() {
super('Invalid padding in bech32m data.')
}
}
/** Thrown when a bech32m string contains mixed case. */
export class MixedCaseError extends BaseError {
override readonly name = 'Bech32m.MixedCaseError'
constructor() {
super('Bech32m string must not contain mixed case.')
}
}
/** Thrown when the HRP is invalid (empty or contains non-ASCII characters). */
export class InvalidHrpError extends BaseError {
override readonly name = 'Bech32m.InvalidHrpError'
constructor() {
super(
'Invalid bech32m human-readable part (HRP). Must be 1+ characters in ASCII range 33-126.',
)
}
}
/** Thrown when the encoded string exceeds the length limit. */
export class ExceedsLengthError extends BaseError {
override readonly name = 'Bech32m.ExceedsLengthError'
constructor({ limit }: { limit: number }) {
super(`Bech32m string exceeds length limit of ${limit}.`)
}
}
/** @internal */
const alphabet = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
/** @internal */
const alphabetMap = /*#__PURE__*/ (() => {
const map: Record<string, number> = {}
for (let i = 0; i < alphabet.length; i++) map[alphabet[i]!] = i
return map
})()
/** @internal */
const BECH32M_CONST = 0x2bc830a3
/** @internal */
const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
/** @internal */
function polymod(values: number[]): number {
let chk = 1
for (const v of values) {
const b = chk >> 25
chk = ((chk & 0x1ffffff) << 5) ^ v
for (let i = 0; i < 5; i++) if ((b >> i) & 1) chk ^= GEN[i]!
}
return chk
}
/** @internal */
function hrpExpand(hrp: string): number[] {
const ret: number[] = []
for (let i = 0; i < hrp.length; i++) ret.push(hrp.charCodeAt(i) >> 5)
ret.push(0)
for (let i = 0; i < hrp.length; i++) ret.push(hrp.charCodeAt(i) & 31)
return ret
}
/** @internal */
function createChecksum(hrp: string, data: number[]): number[] {
const values = hrpExpand(hrp).concat(data).concat([0, 0, 0, 0, 0, 0])
const mod = polymod(values) ^ BECH32M_CONST
const ret: number[] = []
for (let i = 0; i < 6; i++) ret.push((mod >> (5 * (5 - i))) & 31)
return ret
}
/** @internal */
function verifyChecksum(hrp: string, data: number[]): boolean {
return polymod(hrpExpand(hrp).concat(data)) === BECH32M_CONST
}
/** @internal */
function convertBits(
data: Iterable<number>,
fromBits: number,
toBits: number,
pad: boolean,
): number[] {
let acc = 0
let bits = 0
const maxv = (1 << toBits) - 1
const ret: number[] = []
for (const value of data) {
acc = (acc << fromBits) | value
bits += fromBits
while (bits >= toBits) {
bits -= toBits
ret.push((acc >> bits) & maxv)
}
}
if (pad) {
if (bits > 0) ret.push((acc << (toBits - bits)) & maxv)
} else if (bits >= fromBits || (acc << (toBits - bits)) & maxv) {
throw new InvalidPaddingError()
}
return ret
}