ox
Version:
812 lines (725 loc) • 23.7 kB
text/typescript
import type {
AbiParameter,
AbiParameterKind,
AbiParametersToPrimitiveTypes,
AbiParameterToPrimitiveType,
} from 'abitype'
import * as AbiParameters from '../AbiParameters.js'
import * as Address from '../Address.js'
import * as Bytes from '../Bytes.js'
import * as Errors from '../Errors.js'
import * as Hex from '../Hex.js'
import { integerRegex } from '../Solidity.js'
import type * as Cursor from './cursor.js'
import type { Compute, IsNarrowable, UnionToIntersection } from './types.js'
/** @internal */
export type ParameterToPrimitiveType<
abiParameter extends AbiParameter | { name: string; type: unknown },
abiParameterKind extends AbiParameterKind = AbiParameterKind,
> = AbiParameterToPrimitiveType<abiParameter, abiParameterKind>
/** @internal */
export type PreparedParameter = { dynamic: boolean; encoded: Hex.Hex }
/** @internal */
export type ToObject<
parameters extends readonly AbiParameter[],
kind extends AbiParameterKind = AbiParameterKind,
> = IsNarrowable<parameters, AbiParameters.AbiParameters> extends true
? Compute<
UnionToIntersection<
{
[index in keyof parameters]: parameters[index] extends {
name: infer name extends string
}
? {
[key in name]: AbiParameterToPrimitiveType<
parameters[index],
kind
>
}
: {
[key in index]: AbiParameterToPrimitiveType<
parameters[index],
kind
>
}
}[number]
>
>
: unknown
/** @internal */
export type ToPrimitiveTypes<
abiParameters extends readonly AbiParameter[],
abiParameterKind extends AbiParameterKind = AbiParameterKind,
> = AbiParametersToPrimitiveTypes<abiParameters, abiParameterKind>
/** @internal */
export type Tuple = ParameterToPrimitiveType<TupleAbiParameter>
/** @internal */
export function decodeParameter(
cursor: Cursor.Cursor,
param: AbiParameters.Parameter,
options: { checksumAddress?: boolean | undefined; staticPosition: number },
) {
const { checksumAddress, staticPosition } = options
const arrayComponents = getArrayComponents(param.type)
if (arrayComponents) {
const [length, type] = arrayComponents
return decodeArray(
cursor,
{ ...param, type },
{ checksumAddress, length, staticPosition },
)
}
if (param.type === 'tuple')
return decodeTuple(cursor, param as TupleAbiParameter, {
checksumAddress,
staticPosition,
})
if (param.type === 'address')
return decodeAddress(cursor, { checksum: checksumAddress })
if (param.type === 'bool') return decodeBool(cursor)
if (param.type.startsWith('bytes'))
return decodeBytes(cursor, param, { staticPosition })
if (param.type.startsWith('uint') || param.type.startsWith('int'))
return decodeNumber(cursor, param)
if (param.type === 'string') return decodeString(cursor, { staticPosition })
throw new AbiParameters.InvalidTypeError(param.type)
}
export declare namespace decodeParameter {
type ErrorType =
| decodeArray.ErrorType
| decodeTuple.ErrorType
| decodeAddress.ErrorType
| decodeBool.ErrorType
| decodeBytes.ErrorType
| decodeNumber.ErrorType
| decodeString.ErrorType
| AbiParameters.InvalidTypeError
| Errors.GlobalErrorType
}
const sizeOfLength = 32
const sizeOfOffset = 32
/** @internal */
export function decodeAddress(
cursor: Cursor.Cursor,
options: { checksum?: boolean | undefined } = {},
) {
const { checksum = false } = options
const value = cursor.readBytes(32)
const wrap = (address: Hex.Hex) =>
checksum ? Address.checksum(address) : address
return [wrap(Hex.fromBytes(Bytes.slice(value, -20))), 32]
}
export declare namespace decodeAddress {
type ErrorType =
| Hex.fromBytes.ErrorType
| Bytes.slice.ErrorType
| Errors.GlobalErrorType
}
/** @internal */
export function decodeArray(
cursor: Cursor.Cursor,
param: AbiParameters.Parameter,
options: {
checksumAddress?: boolean | undefined
length: number | null
staticPosition: number
},
) {
const { checksumAddress, length, staticPosition } = options
// If the length of the array is not known in advance (dynamic array),
// this means we will need to wonder off to the pointer and decode.
if (!length) {
// Dealing with a dynamic type, so get the offset of the array data.
const offset = Bytes.toNumber(cursor.readBytes(sizeOfOffset))
// Start is the static position of current slot + offset.
const start = staticPosition + offset
const startOfData = start + sizeOfLength
// Get the length of the array from the offset.
cursor.setPosition(start)
const length = Bytes.toNumber(cursor.readBytes(sizeOfLength))
// Check if the array has any dynamic children.
const dynamicChild = hasDynamicChild(param)
let consumed = 0
const value: unknown[] = []
for (let i = 0; i < length; ++i) {
// If any of the children is dynamic, then all elements will be offset pointer, thus size of one slot (32 bytes).
// Otherwise, elements will be the size of their encoding (consumed bytes).
cursor.setPosition(startOfData + (dynamicChild ? i * 32 : consumed))
const [data, consumed_] = decodeParameter(cursor, param, {
checksumAddress,
staticPosition: startOfData,
})
consumed += consumed_
value.push(data)
}
// As we have gone wondering, restore to the original position + next slot.
cursor.setPosition(staticPosition + 32)
return [value, 32]
}
// If the length of the array is known in advance,
// and the length of an element deeply nested in the array is not known,
// we need to decode the offset of the array data.
if (hasDynamicChild(param)) {
// Dealing with dynamic types, so get the offset of the array data.
const offset = Bytes.toNumber(cursor.readBytes(sizeOfOffset))
// Start is the static position of current slot + offset.
const start = staticPosition + offset
const value: unknown[] = []
for (let i = 0; i < length; ++i) {
// Move cursor along to the next slot (next offset pointer).
cursor.setPosition(start + i * 32)
const [data] = decodeParameter(cursor, param, {
checksumAddress,
staticPosition: start,
})
value.push(data)
}
// As we have gone wondering, restore to the original position + next slot.
cursor.setPosition(staticPosition + 32)
return [value, 32]
}
// If the length of the array is known in advance and the array is deeply static,
// then we can just decode each element in sequence.
let consumed = 0
const value: unknown[] = []
for (let i = 0; i < length; ++i) {
const [data, consumed_] = decodeParameter(cursor, param, {
checksumAddress,
staticPosition: staticPosition + consumed,
})
consumed += consumed_
value.push(data)
}
return [value, consumed]
}
export declare namespace decodeArray {
type ErrorType = Bytes.toNumber.ErrorType | Errors.GlobalErrorType
}
/** @internal */
export function decodeBool(cursor: Cursor.Cursor) {
return [Bytes.toBoolean(cursor.readBytes(32), { size: 32 }), 32]
}
export declare namespace decodeBool {
type ErrorType = Bytes.toBoolean.ErrorType | Errors.GlobalErrorType
}
/** @internal */
export function decodeBytes(
cursor: Cursor.Cursor,
param: AbiParameters.Parameter,
{ staticPosition }: { staticPosition: number },
) {
const [_, size] = param.type.split('bytes')
if (!size) {
// Dealing with dynamic types, so get the offset of the bytes data.
const offset = Bytes.toNumber(cursor.readBytes(32))
// Set position of the cursor to start of bytes data.
cursor.setPosition(staticPosition + offset)
const length = Bytes.toNumber(cursor.readBytes(32))
// If there is no length, we have zero data.
if (length === 0) {
// As we have gone wondering, restore to the original position + next slot.
cursor.setPosition(staticPosition + 32)
return ['0x', 32]
}
const data = cursor.readBytes(length)
// As we have gone wondering, restore to the original position + next slot.
cursor.setPosition(staticPosition + 32)
return [Hex.fromBytes(data), 32]
}
const value = Hex.fromBytes(cursor.readBytes(Number.parseInt(size, 10), 32))
return [value, 32]
}
export declare namespace decodeBytes {
type ErrorType =
| Hex.fromBytes.ErrorType
| Bytes.toNumber.ErrorType
| Errors.GlobalErrorType
}
/** @internal */
export function decodeNumber(
cursor: Cursor.Cursor,
param: AbiParameters.Parameter,
) {
const signed = param.type.startsWith('int')
const size = Number.parseInt(param.type.split('int')[1] || '256', 10)
const value = cursor.readBytes(32)
return [
size > 48
? Bytes.toBigInt(value, { signed })
: Bytes.toNumber(value, { signed }),
32,
]
}
export declare namespace decodeNumber {
type ErrorType =
| Bytes.toNumber.ErrorType
| Bytes.toBigInt.ErrorType
| Errors.GlobalErrorType
}
/** @internal */
export type TupleAbiParameter = AbiParameters.Parameter & {
components: readonly AbiParameters.Parameter[]
}
/** @internal */
export function decodeTuple(
cursor: Cursor.Cursor,
param: TupleAbiParameter,
options: { checksumAddress?: boolean | undefined; staticPosition: number },
) {
const { checksumAddress, staticPosition } = options
// Tuples can have unnamed components (i.e. they are arrays), so we must
// determine whether the tuple is named or unnamed. In the case of a named
// tuple, the value will be an object where each property is the name of the
// component. In the case of an unnamed tuple, the value will be an array.
const hasUnnamedChild =
param.components.length === 0 || param.components.some(({ name }) => !name)
// Initialize the value to an object or an array, depending on whether the
// tuple is named or unnamed.
const value: any = hasUnnamedChild ? [] : {}
let consumed = 0
// If the tuple has a dynamic child, we must first decode the offset to the
// tuple data.
if (hasDynamicChild(param)) {
// Dealing with dynamic types, so get the offset of the tuple data.
const offset = Bytes.toNumber(cursor.readBytes(sizeOfOffset))
// Start is the static position of referencing slot + offset.
const start = staticPosition + offset
for (let i = 0; i < param.components.length; ++i) {
const component = param.components[i]!
cursor.setPosition(start + consumed)
const [data, consumed_] = decodeParameter(cursor, component, {
checksumAddress,
staticPosition: start,
})
consumed += consumed_
value[hasUnnamedChild ? i : component?.name!] = data
}
// As we have gone wondering, restore to the original position + next slot.
cursor.setPosition(staticPosition + 32)
return [value, 32]
}
// If the tuple has static children, we can just decode each component
// in sequence.
for (let i = 0; i < param.components.length; ++i) {
const component = param.components[i]!
const [data, consumed_] = decodeParameter(cursor, component, {
checksumAddress,
staticPosition,
})
value[hasUnnamedChild ? i : component?.name!] = data
consumed += consumed_
}
return [value, consumed]
}
export declare namespace decodeTuple {
type ErrorType = Bytes.toNumber.ErrorType | Errors.GlobalErrorType
}
/** @internal */
export function decodeString(
cursor: Cursor.Cursor,
{ staticPosition }: { staticPosition: number },
) {
// Get offset to start of string data.
const offset = Bytes.toNumber(cursor.readBytes(32))
// Start is the static position of current slot + offset.
const start = staticPosition + offset
cursor.setPosition(start)
const length = Bytes.toNumber(cursor.readBytes(32))
// If there is no length, we have zero data (empty string).
if (length === 0) {
cursor.setPosition(staticPosition + 32)
return ['', 32]
}
const data = cursor.readBytes(length, 32)
const value = Bytes.toString(Bytes.trimLeft(data))
// As we have gone wondering, restore to the original position + next slot.
cursor.setPosition(staticPosition + 32)
return [value, 32]
}
export declare namespace decodeString {
type ErrorType =
| Bytes.toNumber.ErrorType
| Bytes.toString.ErrorType
| Bytes.trimLeft.ErrorType
| Errors.GlobalErrorType
}
/** @internal */
export function prepareParameters<
const parameters extends AbiParameters.AbiParameters,
>({
checksumAddress,
parameters,
values,
}: {
checksumAddress?: boolean | undefined
parameters: parameters
values: parameters extends AbiParameters.AbiParameters
? ToPrimitiveTypes<parameters>
: never
}) {
const preparedParameters: PreparedParameter[] = []
for (let i = 0; i < parameters.length; i++) {
preparedParameters.push(
prepareParameter({
checksumAddress,
parameter: parameters[i]!,
value: values[i],
}),
)
}
return preparedParameters
}
/** @internal */
export declare namespace prepareParameters {
type ErrorType = prepareParameter.ErrorType | Errors.GlobalErrorType
}
/** @internal */
export function prepareParameter<
const parameter extends AbiParameters.Parameter,
>({
checksumAddress = false,
parameter: parameter_,
value,
}: {
parameter: parameter
value: parameter extends AbiParameters.Parameter
? ParameterToPrimitiveType<parameter>
: never
checksumAddress?: boolean | undefined
}): PreparedParameter {
const parameter = parameter_ as AbiParameters.Parameter
const arrayComponents = getArrayComponents(parameter.type)
if (arrayComponents) {
const [length, type] = arrayComponents
return encodeArray(value, {
checksumAddress,
length,
parameter: {
...parameter,
type,
},
})
}
if (parameter.type === 'tuple') {
return encodeTuple(value as unknown as Tuple, {
checksumAddress,
parameter: parameter as TupleAbiParameter,
})
}
if (parameter.type === 'address') {
return encodeAddress(value as unknown as Hex.Hex, {
checksum: checksumAddress,
})
}
if (parameter.type === 'bool') {
return encodeBoolean(value as unknown as boolean)
}
if (parameter.type.startsWith('uint') || parameter.type.startsWith('int')) {
const signed = parameter.type.startsWith('int')
const [, , size = '256'] = integerRegex.exec(parameter.type) ?? []
return encodeNumber(value as unknown as number, {
signed,
size: Number(size),
})
}
if (parameter.type.startsWith('bytes')) {
return encodeBytes(value as unknown as Hex.Hex, { type: parameter.type })
}
if (parameter.type === 'string') {
return encodeString(value as unknown as string)
}
throw new AbiParameters.InvalidTypeError(parameter.type)
}
/** @internal */
export declare namespace prepareParameter {
type ErrorType =
| encodeArray.ErrorType
| encodeTuple.ErrorType
| encodeAddress.ErrorType
| encodeBoolean.ErrorType
| encodeBytes.ErrorType
| encodeString.ErrorType
| AbiParameters.InvalidTypeError
| Errors.GlobalErrorType
}
/** @internal */
export function encode(preparedParameters: PreparedParameter[]): Hex.Hex {
// 1. Compute the size of the static part of the parameters.
let staticSize = 0
for (let i = 0; i < preparedParameters.length; i++) {
const { dynamic, encoded } = preparedParameters[i]!
if (dynamic) staticSize += 32
else staticSize += Hex.size(encoded)
}
// 2. Split the parameters into static and dynamic parts.
const staticParameters: Hex.Hex[] = []
const dynamicParameters: Hex.Hex[] = []
let dynamicSize = 0
for (let i = 0; i < preparedParameters.length; i++) {
const { dynamic, encoded } = preparedParameters[i]!
if (dynamic) {
staticParameters.push(
Hex.fromNumber(staticSize + dynamicSize, { size: 32 }),
)
dynamicParameters.push(encoded)
dynamicSize += Hex.size(encoded)
} else {
staticParameters.push(encoded)
}
}
// 3. Concatenate static and dynamic parts.
return Hex.concat(...staticParameters, ...dynamicParameters)
}
/** @internal */
export declare namespace encode {
type ErrorType =
| Hex.concat.ErrorType
| Hex.fromNumber.ErrorType
| Hex.size.ErrorType
| Errors.GlobalErrorType
}
/** @internal */
export function encodeAddress(
value: Hex.Hex,
options: { checksum: boolean },
): PreparedParameter {
const { checksum = false } = options
Address.assert(value, { strict: checksum })
return {
dynamic: false,
encoded: Hex.padLeft(value.toLowerCase() as Hex.Hex),
}
}
/** @internal */
export declare namespace encodeAddress {
type ErrorType =
| Address.assert.ErrorType
| Hex.padLeft.ErrorType
| Errors.GlobalErrorType
}
/** @internal */
export function encodeArray<const parameter extends AbiParameters.Parameter>(
value: ParameterToPrimitiveType<parameter>,
options: {
checksumAddress?: boolean | undefined
length: number | null
parameter: parameter
},
): PreparedParameter {
const { checksumAddress, length, parameter } = options
const dynamic = length === null
if (!Array.isArray(value)) throw new AbiParameters.InvalidArrayError(value)
if (!dynamic && value.length !== length)
throw new AbiParameters.ArrayLengthMismatchError({
expectedLength: length!,
givenLength: value.length,
type: `${parameter.type}[${length}]`,
})
let dynamicChild = false
const preparedParameters: PreparedParameter[] = []
for (let i = 0; i < value.length; i++) {
const preparedParam = prepareParameter({
checksumAddress,
parameter,
value: value[i],
})
if (preparedParam.dynamic) dynamicChild = true
preparedParameters.push(preparedParam)
}
if (dynamic || dynamicChild) {
const data = encode(preparedParameters)
if (dynamic) {
const length = Hex.fromNumber(preparedParameters.length, { size: 32 })
return {
dynamic: true,
encoded:
preparedParameters.length > 0 ? Hex.concat(length, data) : length,
}
}
if (dynamicChild) return { dynamic: true, encoded: data }
}
return {
dynamic: false,
encoded: Hex.concat(...preparedParameters.map(({ encoded }) => encoded)),
}
}
/** @internal */
export declare namespace encodeArray {
type ErrorType =
| AbiParameters.InvalidArrayError
| AbiParameters.ArrayLengthMismatchError
| Hex.concat.ErrorType
| Hex.fromNumber.ErrorType
| Errors.GlobalErrorType
}
/** @internal */
export function encodeBytes(
value: Hex.Hex,
{ type }: { type: string },
): PreparedParameter {
const [, parametersize] = type.split('bytes')
const bytesSize = Hex.size(value)
if (!parametersize) {
let value_ = value
// If the size is not divisible by 32 bytes, pad the end
// with empty bytes to the ceiling 32 bytes.
if (bytesSize % 32 !== 0)
value_ = Hex.padRight(value_, Math.ceil((value.length - 2) / 2 / 32) * 32)
return {
dynamic: true,
encoded: Hex.concat(
Hex.padLeft(Hex.fromNumber(bytesSize, { size: 32 })),
value_,
),
}
}
if (bytesSize !== Number.parseInt(parametersize, 10))
throw new AbiParameters.BytesSizeMismatchError({
expectedSize: Number.parseInt(parametersize, 10),
value,
})
return { dynamic: false, encoded: Hex.padRight(value) }
}
/** @internal */
export declare namespace encodeBytes {
type ErrorType =
| Hex.padLeft.ErrorType
| Hex.padRight.ErrorType
| Hex.fromNumber.ErrorType
| Hex.slice.ErrorType
| Errors.GlobalErrorType
}
/** @internal */
export function encodeBoolean(value: boolean): PreparedParameter {
if (typeof value !== 'boolean')
throw new Errors.BaseError(
`Invalid boolean value: "${value}" (type: ${typeof value}). Expected: \`true\` or \`false\`.`,
)
return { dynamic: false, encoded: Hex.padLeft(Hex.fromBoolean(value)) }
}
/** @internal */
export declare namespace encodeBoolean {
type ErrorType =
| Hex.padLeft.ErrorType
| Hex.fromBoolean.ErrorType
| Errors.GlobalErrorType
}
/** @internal */
export function encodeNumber(
value: number,
{ signed, size }: { signed: boolean; size: number },
): PreparedParameter {
if (typeof size === 'number') {
const max = 2n ** (BigInt(size) - (signed ? 1n : 0n)) - 1n
const min = signed ? -max - 1n : 0n
if (value > max || value < min)
throw new Hex.IntegerOutOfRangeError({
max: max.toString(),
min: min.toString(),
signed,
size: size / 8,
value: value.toString(),
})
}
return {
dynamic: false,
encoded: Hex.fromNumber(value, {
size: 32,
signed,
}),
}
}
/** @internal */
export declare namespace encodeNumber {
type ErrorType = Hex.fromNumber.ErrorType | Errors.GlobalErrorType
}
/** @internal */
export function encodeString(value: string): PreparedParameter {
const hexValue = Hex.fromString(value)
const partsLength = Math.ceil(Hex.size(hexValue) / 32)
const parts: Hex.Hex[] = []
for (let i = 0; i < partsLength; i++) {
parts.push(Hex.padRight(Hex.slice(hexValue, i * 32, (i + 1) * 32)))
}
return {
dynamic: true,
encoded: Hex.concat(
Hex.padRight(Hex.fromNumber(Hex.size(hexValue), { size: 32 })),
...parts,
),
}
}
/** @internal */
export declare namespace encodeString {
type ErrorType =
| Hex.fromNumber.ErrorType
| Hex.padRight.ErrorType
| Hex.slice.ErrorType
| Hex.size.ErrorType
| Errors.GlobalErrorType
}
/** @internal */
export function encodeTuple<
const parameter extends AbiParameters.Parameter & {
components: readonly AbiParameters.Parameter[]
},
>(
value: ParameterToPrimitiveType<parameter>,
options: {
checksumAddress?: boolean | undefined
parameter: parameter
},
): PreparedParameter {
const { checksumAddress, parameter } = options
let dynamic = false
const preparedParameters: PreparedParameter[] = []
for (let i = 0; i < parameter.components.length; i++) {
const param_ = parameter.components[i]!
const index = Array.isArray(value) ? i : param_.name
const preparedParam = prepareParameter({
checksumAddress,
parameter: param_,
value: (value as any)[index!] as readonly unknown[],
})
preparedParameters.push(preparedParam)
if (preparedParam.dynamic) dynamic = true
}
return {
dynamic,
encoded: dynamic
? encode(preparedParameters)
: Hex.concat(...preparedParameters.map(({ encoded }) => encoded)),
}
}
/** @internal */
export declare namespace encodeTuple {
type ErrorType = Hex.concat.ErrorType | Errors.GlobalErrorType
}
/** @internal */
export function getArrayComponents(
type: string,
): [length: number | null, innerType: string] | undefined {
const matches = type.match(/^(.*)\[(\d+)?\]$/)
return matches
? // Return `null` if the array is dynamic.
[matches[2]! ? Number(matches[2]!) : null, matches[1]!]
: undefined
}
/** @internal */
export function hasDynamicChild(param: AbiParameters.Parameter) {
const { type } = param
if (type === 'string') return true
if (type === 'bytes') return true
if (type.endsWith('[]')) return true
if (type === 'tuple') return (param as any).components?.some(hasDynamicChild)
const arrayComponents = getArrayComponents(param.type)
if (
arrayComponents &&
hasDynamicChild({
...param,
type: arrayComponents[1],
} as AbiParameters.Parameter)
)
return true
return false
}