UNPKG

ox

Version:

Ethereum Standard Library

709 lines (636 loc) 21.3 kB
import * as Bytes from './Bytes.js' import * as Errors from './Errors.js' import * as Hex from './Hex.js' import * as internal_bytes from './internal/bytes.js' import * as Cursor from './internal/cursor.js' import type { ExactPartial, RecursiveArray } from './internal/types.js' /** Maximum nesting depth permitted when decoding an RLP value. */ const depthLimit = 1_024 /** * Decodes a Recursive-Length Prefix (RLP) value into a {@link ox#Bytes.Bytes} value. * * @example * ```ts twoslash * import { Rlp } from 'ox' * Rlp.toBytes('0x8b68656c6c6f20776f726c64') * // Uint8Array([139, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]) * ``` * * @param value - The value to decode. * @returns The decoded {@link ox#Bytes.Bytes} value. */ export function toBytes( value: Bytes.Bytes | Hex.Hex, ): RecursiveArray<Bytes.Bytes> { return to(value, 'Bytes') } export declare namespace toBytes { type ErrorType = to.ErrorType } /** * Decodes a Recursive-Length Prefix (RLP) value into a {@link ox#Hex.Hex} value. * * @example * ```ts twoslash * import { Rlp } from 'ox' * Rlp.toHex('0x8b68656c6c6f20776f726c64') * // 0x68656c6c6f20776f726c64 * ``` * * @param value - The value to decode. * @returns The decoded {@link ox#Hex.Hex} value. */ export function toHex(value: Bytes.Bytes | Hex.Hex): RecursiveArray<Hex.Hex> { return to(value, 'Hex') } export declare namespace toHex { type ErrorType = to.ErrorType } ///////////////////////////////////////////////////////////////////////////////// // Internal ///////////////////////////////////////////////////////////////////////////////// /** @internal */ export function to< value extends Bytes.Bytes | Hex.Hex, to extends 'Hex' | 'Bytes', >(value: value, to: to | 'Hex' | 'Bytes'): to.ReturnType<to> { const to_ = to ?? (typeof value === 'string' ? 'Hex' : 'Bytes') const bytes = typeof value === 'string' ? Bytes.fromHex(value) : (value as Bytes.Bytes) const cursor = Cursor.create(bytes, { recursiveReadLimit: Number.POSITIVE_INFINITY, }) const result = decodeRlpCursor(cursor, to_) // RLP payloads encode exactly one item (Yellow Paper, Appendix B). if (cursor.position < cursor.bytes.length) throw new TrailingBytesError({ count: cursor.bytes.length - cursor.position, }) return result as to.ReturnType<to> } /** @internal */ export declare namespace to { type ReturnType<to extends 'Hex' | 'Bytes' = 'Hex' | 'Bytes'> = | (to extends 'Bytes' ? RecursiveArray<Bytes.Bytes> : never) | (to extends 'Hex' ? RecursiveArray<Hex.Hex> : never) type ErrorType = | Bytes.fromHex.ErrorType | decodeRlpCursor.ErrorType | Cursor.create.ErrorType | Hex.InvalidLengthError | TrailingBytesError | Errors.GlobalErrorType } /** @internal */ /** @internal */ export function decodeRlpCursor<to extends 'Hex' | 'Bytes' = 'Hex'>( cursor: Cursor.Cursor, to: to | 'Hex' | 'Bytes' | undefined = 'Hex', depth = 0, ): decodeRlpCursor.ReturnType<to> { if (depth >= depthLimit) throw new DepthLimitExceededError({ limit: depthLimit }) if (cursor.bytes.length === 0) return ( to === 'Hex' ? Hex.fromBytes(cursor.bytes) : cursor.bytes ) as decodeRlpCursor.ReturnType<to> const prefix = cursor.readByte() if (prefix < 0x80) cursor.decrementPosition(1) // bytes if (prefix < 0xc0) { const length = readLength(cursor, prefix, 0x80) const bytes = cursor.readBytes(length) return ( to === 'Hex' ? Hex.fromBytes(bytes) : bytes ) as decodeRlpCursor.ReturnType<to> } // list const length = readLength(cursor, prefix, 0xc0) return readList( cursor, length, to, depth + 1, ) as {} as decodeRlpCursor.ReturnType<to> } /** @internal */ export declare namespace decodeRlpCursor { type ReturnType<to extends 'Hex' | 'Bytes' = 'Hex'> = to.ReturnType<to> type ErrorType = | Hex.fromBytes.ErrorType | readLength.ErrorType | readList.ErrorType | DepthLimitExceededError | Errors.GlobalErrorType } /** @internal */ export function readLength( cursor: Cursor.Cursor, prefix: number, offset: number, ) { if (offset === 0x80 && prefix < 0x80) return 1 if (prefix <= offset + 55) return prefix - offset if (prefix === offset + 55 + 1) return cursor.readUint8() if (prefix === offset + 55 + 2) return cursor.readUint16() if (prefix === offset + 55 + 3) return cursor.readUint24() if (prefix === offset + 55 + 4) return cursor.readUint32() throw new Errors.BaseError('Invalid RLP prefix') } /** @internal */ export declare namespace readLength { type ErrorType = Errors.BaseError | Errors.GlobalErrorType } /** @internal */ export function readList<to extends 'Hex' | 'Bytes'>( cursor: Cursor.Cursor, length: number, to: to | 'Hex' | 'Bytes', depth = 0, ) { const position = cursor.position const value: decodeRlpCursor.ReturnType<to>[] = [] while (cursor.position - position < length) value.push(decodeRlpCursor(cursor, to, depth)) // Items must consume exactly the declared list length. if (cursor.position - position !== length) throw new ListBoundaryExceededError({ consumed: cursor.position - position, declared: length, }) return value } /** @internal */ export declare namespace readList { type ErrorType = | DepthLimitExceededError | ListBoundaryExceededError | Errors.GlobalErrorType } /** * Encodes a {@link ox#Bytes.Bytes} or {@link ox#Hex.Hex} value into a Recursive-Length Prefix (RLP) value. * * @example * ```ts twoslash * import { Bytes, Rlp } from 'ox' * * Rlp.from('0x68656c6c6f20776f726c64', { as: 'Hex' }) * // @log: 0x8b68656c6c6f20776f726c64 * * Rlp.from( * Bytes.from([ * 139, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, * 100 * ]), * { as: 'Bytes' } * ) * // @log: Uint8Array([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]) * ``` * * @param value - The {@link ox#Bytes.Bytes} or {@link ox#Hex.Hex} value to encode. * @param options - Options. * @returns The RLP value. */ export function from<as extends 'Hex' | 'Bytes'>( value: RecursiveArray<Bytes.Bytes> | RecursiveArray<Hex.Hex>, options: from.Options<as>, ): from.ReturnType<as> { const { as } = options // Two-walk encode without the per-node `Encodable` closure tree: // 1. `measure` walks the input once and caches each list's `bodyLength` // in a side array indexed by visit order. This makes the second walk // O(N) instead of O(N²) for nested inputs. // 2. `writeEncoded` walks again, reads cached body lengths, and writes // bytes straight into the pre-sized buffer. Hex leaves are // nibble-decoded directly into the destination, skipping the per-leaf // `Bytes.fromHex` allocation. const ctx: EncodeCtx = { lengths: [], cursor: 0 } const totalLength = measure(value, ctx) // Hex-output fast path: when the caller asked for hex AND every leaf is // already hex, emit a hex string directly instead of allocating an // intermediate `Uint8Array` and round-tripping through `Hex.fromBytes`. // This is the dominant shape for transaction envelope serialize. if (as === 'Hex' && isAllHex(value)) { const parts: string[] = [] writeEncodedHex(parts, value as RecursiveArray<Hex.Hex>, { lengths: ctx.lengths, cursor: 0, }) return `0x${parts.join('')}` as from.ReturnType<as> } const bytes = new Uint8Array(totalLength) writeEncoded(bytes, 0, value, { lengths: ctx.lengths, cursor: 0 }) if (as === 'Hex') return Hex.fromBytes(bytes) as from.ReturnType<as> return bytes as from.ReturnType<as> } export declare namespace from { type Options<as extends 'Hex' | 'Bytes'> = { /** The type to convert the RLP value to. */ as: as | 'Hex' | 'Bytes' } type ReturnType<as extends 'Hex' | 'Bytes'> = | (as extends 'Bytes' ? Bytes.Bytes : never) | (as extends 'Hex' ? Hex.Hex : never) type ErrorType = | Cursor.create.ErrorType | Hex.fromBytes.ErrorType | Bytes.fromHex.ErrorType | Errors.GlobalErrorType } /** * Encodes a {@link ox#Bytes.Bytes} value into a Recursive-Length Prefix (RLP) value. * * @example * ```ts twoslash * import { Bytes, Rlp } from 'ox' * * Rlp.fromBytes( * Bytes.from([ * 139, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, * 100 * ]) * ) * // @log: Uint8Array([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]) * ``` * * @param bytes - The {@link ox#Bytes.Bytes} value to encode. * @param options - Options. * @returns The RLP value. */ export function fromBytes<as extends 'Hex' | 'Bytes' = 'Bytes'>( bytes: RecursiveArray<Bytes.Bytes>, options: fromBytes.Options<as> = {}, ): fromBytes.ReturnType<as> { const { as = 'Bytes' } = options return from(bytes, { as }) as never } export declare namespace fromBytes { type Options<as extends 'Hex' | 'Bytes' = 'Bytes'> = ExactPartial< from.Options<as> > type ReturnType<as extends 'Hex' | 'Bytes' = 'Bytes'> = from.ReturnType<as> type ErrorType = from.ErrorType | Errors.GlobalErrorType } /** * Encodes a {@link ox#Hex.Hex} value into a Recursive-Length Prefix (RLP) value. * * @example * ```ts twoslash * import { Rlp } from 'ox' * * Rlp.fromHex('0x68656c6c6f20776f726c64') * // @log: 0x8b68656c6c6f20776f726c64 * ``` * * @param hex - The {@link ox#Hex.Hex} value to encode. * @param options - Options. * @returns The RLP value. */ export function fromHex<as extends 'Hex' | 'Bytes' = 'Hex'>( hex: RecursiveArray<Hex.Hex>, options: fromHex.Options<as> = {}, ): fromHex.ReturnType<as> { const { as = 'Hex' } = options return from(hex, { as }) as never } export declare namespace fromHex { type Options<as extends 'Hex' | 'Bytes' = 'Hex'> = ExactPartial< from.Options<as> > type ReturnType<as extends 'Hex' | 'Bytes' = 'Hex'> = from.ReturnType<as> type ErrorType = from.ErrorType | Errors.GlobalErrorType } ///////////////////////////////////////////////////////////////////////////////// // Internal ///////////////////////////////////////////////////////////////////////////////// /** * Returns the byte length needed to encode `length` itself (1-4 bytes per * RLP), or throws when `length` exceeds the protocol cap. * * @internal */ function getSizeOfLength(length: number) { if (length <= 0xff) return 1 if (length <= 0xff_ff) return 2 if (length <= 0xff_ff_ff) return 3 if (length <= 0xff_ff_ff_ff) return 4 throw new Errors.BaseError('Length is too large.') } /** * Side-channel used by `measure` and `writeEncoded` to share precomputed * list body lengths. `lengths` is filled in pre-order (DFS) by `measure` and * read back in the same order by `writeEncoded` via `cursor`. This avoids * re-walking subtrees from `writeEncoded` (which would be O(N²) on nested * inputs) without allocating a per-node `Encodable` closure tree. * * @internal */ type EncodeCtx = { lengths: number[]; cursor: number } /** * Walks `value` once, caches each list's `bodyLength` into `ctx.lengths`, * and returns the total encoded byte length. Allocates nothing per node * beyond the shared `lengths` array entries. * * @internal */ function measure( value: RecursiveArray<Bytes.Bytes> | RecursiveArray<Hex.Hex>, ctx: EncodeCtx, ): number { if (Array.isArray(value)) { // Reserve this list's slot before descending so children's slots come // after ours; `writeEncoded` walks in the same order and reads slot N // when it visits the Nth list. const slot = ctx.lengths.length ctx.lengths.push(0) let bodyLength = 0 for (let i = 0; i < value.length; i++) bodyLength += measure(value[i]!, ctx) ctx.lengths[slot] = bodyLength if (bodyLength <= 55) return 1 + bodyLength return 1 + getSizeOfLength(bodyLength) + bodyLength } // Hex leaf: byte length = ceil((hex.length - 2) / 2). The `>> 1` of // `length - 1` yields ceil for both odd- and even-nibble inputs. if (typeof value === 'string') { const byteLen = (value.length - 1) >> 1 if (byteLen === 0) return 1 if (byteLen === 1) { // Single-byte values < 0x80 encode as themselves (no prefix). const odd = (value.length & 1) === 1 const firstChar = value.charCodeAt(odd ? 2 : 3) const high = odd ? 0 : (internal_bytes.charCodeToBase16(value.charCodeAt(2)) ?? 0) << 4 const low = internal_bytes.charCodeToBase16(firstChar) ?? 0 const byte = high | low if (byte < 0x80) return 1 return 2 } if (byteLen <= 55) return 1 + byteLen return 1 + getSizeOfLength(byteLen) + byteLen } // Bytes leaf const len = (value as Bytes.Bytes).length if (len === 1 && (value as Bytes.Bytes)[0]! < 0x80) return 1 if (len <= 55) return 1 + len return 1 + getSizeOfLength(len) + len } /** * Writes `value`'s RLP encoding into `bytes` starting at `offset` and returns * the next free offset. Reads list body lengths from `ctx.lengths` in the * same DFS order that `measure` filled them. Hex leaves are nibble-decoded * directly into the destination, skipping the per-leaf `Bytes.fromHex` * allocation. * * @internal */ function writeEncoded( bytes: Uint8Array, offset: number, value: RecursiveArray<Bytes.Bytes> | RecursiveArray<Hex.Hex>, ctx: EncodeCtx, ): number { if (Array.isArray(value)) { const bodyLength = ctx.lengths[ctx.cursor++]! let cursor = offset if (bodyLength <= 55) { bytes[cursor++] = 0xc0 + bodyLength } else { const sizeOfBodyLength = getSizeOfLength(bodyLength) bytes[cursor++] = 0xc0 + 55 + sizeOfBodyLength cursor = writeBigEndian(bytes, cursor, bodyLength, sizeOfBodyLength) } for (let i = 0; i < value.length; i++) cursor = writeEncoded(bytes, cursor, value[i]!, ctx) return cursor } if (typeof value === 'string') return writeHexLeaf(bytes, offset, value) return writeBytesLeaf(bytes, offset, value as Bytes.Bytes) } /** * Hex-leaf fast path: writes the RLP encoding of a hex string directly into * `bytes` by nibble-decoding the source hex chars into the destination * buffer. Even-pads odd-nibble hex (e.g. `'0x1'`) on the fly. * * @internal */ function writeHexLeaf(bytes: Uint8Array, offset: number, hex: Hex.Hex): number { const dataStart = 2 let byteLen = (hex.length - 2) >> 1 let highNibbleFromOddPad = false if ((hex.length & 1) === 1) { // Odd-nibble: first emitted byte's high nibble is `0` (left-pad). byteLen += 1 highNibbleFromOddPad = true } // Empty leaf -> single 0x80 prefix byte (zero-length string in RLP). if (byteLen === 0) { bytes[offset] = 0x80 return offset + 1 } // Single-byte fast path: if the byte < 0x80, write it as-is (no prefix). if (byteLen === 1) { let byte: number if (highNibbleFromOddPad) { byte = internal_bytes.charCodeToBase16(hex.charCodeAt(dataStart)) ?? 0 } else { const high = internal_bytes.charCodeToBase16(hex.charCodeAt(dataStart)) ?? 0 const low = internal_bytes.charCodeToBase16(hex.charCodeAt(dataStart + 1)) ?? 0 byte = (high << 4) | low } if (byte < 0x80) { bytes[offset] = byte return offset + 1 } bytes[offset] = 0x80 + 1 bytes[offset + 1] = byte return offset + 2 } let dest = offset if (byteLen <= 55) { bytes[dest++] = 0x80 + byteLen } else { const sizeOfBytesLength = getSizeOfLength(byteLen) bytes[dest++] = 0x80 + 55 + sizeOfBytesLength dest = writeBigEndian(bytes, dest, byteLen, sizeOfBytesLength) } // Decode hex nibbles directly into the destination buffer. let src = dataStart if (highNibbleFromOddPad) { const low = internal_bytes.charCodeToBase16(hex.charCodeAt(src++)) if (low === undefined) throw invalidNibble(hex) bytes[dest++] = low } while (src < hex.length) { const high = internal_bytes.charCodeToBase16(hex.charCodeAt(src++)) const low = internal_bytes.charCodeToBase16(hex.charCodeAt(src++)) if (high === undefined || low === undefined) throw invalidNibble(hex) bytes[dest++] = (high << 4) | low } return dest } function writeBytesLeaf( bytes: Uint8Array, offset: number, leaf: Bytes.Bytes, ): number { const len = leaf.length if (len === 1 && leaf[0]! < 0x80) { bytes[offset] = leaf[0]! return offset + 1 } let dest = offset if (len <= 55) { bytes[dest++] = 0x80 + len } else { const sizeOfBytesLength = getSizeOfLength(len) bytes[dest++] = 0x80 + 55 + sizeOfBytesLength dest = writeBigEndian(bytes, dest, len, sizeOfBytesLength) } bytes.set(leaf, dest) return dest + len } /** * Returns true if every leaf in the (possibly nested) input is a hex string. * Used to gate the hex-output fast path in `from`. * * @internal */ function isAllHex( value: RecursiveArray<Bytes.Bytes> | RecursiveArray<Hex.Hex>, ): boolean { if (Array.isArray(value)) { for (let i = 0; i < value.length; i++) if (!isAllHex(value[i]!)) return false return true } return typeof value === 'string' } /** * Hex-output fast path: writes the RLP encoding of `value` directly as hex * substrings into `parts`. Avoids the intermediate `Uint8Array` allocation * (and the trailing `Hex.fromBytes` round-trip) used by the bytes path. * * Length-prefix bytes are formatted via the cached `hexes[]` table so we * never call `toString(16)` per node. * * @internal */ function writeEncodedHex( parts: string[], value: RecursiveArray<Hex.Hex>, ctx: EncodeCtx, ): void { if (Array.isArray(value)) { const bodyLength = ctx.lengths[ctx.cursor++]! if (bodyLength <= 55) { parts.push(hexes[0xc0 + bodyLength]!) } else { const sizeOfBodyLength = getSizeOfLength(bodyLength) parts.push(hexes[0xc0 + 55 + sizeOfBodyLength]!) parts.push(bigEndianHex(bodyLength, sizeOfBodyLength)) } for (let i = 0; i < value.length; i++) writeEncodedHex(parts, value[i]!, ctx) return } // Hex leaf: even-pad odd-nibble inputs and skip the `0x` prefix. const hex = value as string const odd = (hex.length & 1) === 1 const body = odd ? `0${hex.slice(2)}` : hex.slice(2) const byteLen = body.length >> 1 if (byteLen === 0) { parts.push(hexes[0x80]!) return } if (byteLen === 1) { const byte = parseInt(body, 16) if (byte < 0x80) { parts.push(body) } else { parts.push(hexes[0x81]!) parts.push(body) } return } if (byteLen <= 55) { parts.push(hexes[0x80 + byteLen]!) parts.push(body) return } const sizeOfBytesLength = getSizeOfLength(byteLen) parts.push(hexes[0x80 + 55 + sizeOfBytesLength]!) parts.push(bigEndianHex(byteLen, sizeOfBytesLength)) parts.push(body) } const hexes = /*#__PURE__*/ Array.from({ length: 256 }, (_v, i) => i.toString(16).padStart(2, '0'), ) /** * Returns the big-endian hex encoding of `value` in `size` bytes. * * @internal */ function bigEndianHex(value: number, size: number): string { if (size === 1) return hexes[value & 0xff]! if (size === 2) return `${hexes[(value >>> 8) & 0xff]!}${hexes[value & 0xff]!}` if (size === 3) return `${hexes[(value >>> 16) & 0xff]!}${hexes[(value >>> 8) & 0xff]!}${hexes[value & 0xff]!}` return `${hexes[(value >>> 24) & 0xff]!}${hexes[(value >>> 16) & 0xff]!}${hexes[(value >>> 8) & 0xff]!}${hexes[value & 0xff]!}` } function writeBigEndian( bytes: Uint8Array, offset: number, value: number, size: number, ): number { if (size === 1) { bytes[offset] = value & 0xff } else if (size === 2) { bytes[offset] = (value >>> 8) & 0xff bytes[offset + 1] = value & 0xff } else if (size === 3) { bytes[offset] = (value >>> 16) & 0xff bytes[offset + 1] = (value >>> 8) & 0xff bytes[offset + 2] = value & 0xff } else { bytes[offset] = (value >>> 24) & 0xff bytes[offset + 1] = (value >>> 16) & 0xff bytes[offset + 2] = (value >>> 8) & 0xff bytes[offset + 3] = value & 0xff } return offset + size } function invalidNibble(hex: Hex.Hex): Errors.BaseError { return new Errors.BaseError(`Invalid hex string \`${hex}\`.`) } /** Thrown when an RLP value nests deeper than the decode depth limit. */ export class DepthLimitExceededError extends Errors.BaseError { override readonly name = 'Rlp.DepthLimitExceededError' constructor({ limit }: { limit: number }) { super(`RLP depth limit of \`${limit}\` exceeded.`) } } /** Thrown when RLP list items overrun the list's declared length. */ export class ListBoundaryExceededError extends Errors.BaseError { override readonly name = 'Rlp.ListBoundaryExceededError' constructor({ consumed, declared }: { consumed: number; declared: number }) { super( `RLP list items consumed \`${consumed}\` bytes but the list declared a length of \`${declared}\`.`, ) } } /** Thrown when an RLP payload contains bytes after the decoded item. */ export class TrailingBytesError extends Errors.BaseError { override readonly name = 'Rlp.TrailingBytesError' constructor({ count }: { count: number }) { super( `RLP payload encodes a single item, but \`${count}\` trailing ${ count === 1 ? 'byte remains' : 'bytes remain' }.`, ) } }