ox
Version:
Ethereum Standard Library
709 lines (636 loc) • 21.3 kB
text/typescript
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'
}.`,
)
}
}