Skip to content

Commit

Permalink
feat: add checksumAddress as an option to `AbiParameters.{encode|de…
Browse files Browse the repository at this point in the history
…code}`

Co-Authored-By: gregfromstl <[email protected]>
  • Loading branch information
jxom and gregfromstl committed Dec 18, 2024
1 parent 3421ab7 commit c09d165
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 21 deletions.
5 changes: 5 additions & 0 deletions .changeset/tricky-pens-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ox": patch
---

Added `checksumAddress` as an option to `AbiParameters.{encode|decode}`.
27 changes: 25 additions & 2 deletions src/core/AbiParameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,12 @@ export function decode<
export function decode(
parameters: AbiParameters,
data: Bytes.Bytes | Hex.Hex,
options: { as?: 'Array' | 'Object' | undefined } = {},
options: {
as?: 'Array' | 'Object' | undefined
checksumAddress?: boolean | undefined
} = {},
): readonly unknown[] | Record<string, unknown> {
const { as = 'Array' } = options
const { as = 'Array', checksumAddress = false } = options

const bytes = typeof data === 'string' ? Bytes.fromHex(data) : data
const cursor = Cursor.create(bytes)
Expand All @@ -95,6 +98,7 @@ export function decode(
const param = parameters[i] as Parameter
cursor.setPosition(consumed)
const [data, consumed_] = internal.decodeParameter(cursor, param, {
checksumAddress,
staticPosition: 0,
})
consumed += consumed_
Expand All @@ -112,6 +116,12 @@ export declare namespace decode {
* @default "Array"
*/
as?: as | 'Object' | 'Array' | undefined
/**
* Whether decoded addresses should be checksummed.
*
* @default false
*/
checksumAddress?: boolean | undefined
}

type ReturnType<
Expand Down Expand Up @@ -175,14 +185,18 @@ export function encode<
values: parameters extends AbiParameters
? internal.ToPrimitiveTypes<parameters>
: never,
options?: encode.Options,
): Hex.Hex {
const { checksumAddress = false } = options ?? {}

if (parameters.length !== values.length)
throw new LengthMismatchError({
expectedLength: parameters.length as number,
givenLength: values.length as any,
})
// Prepare the parameters to determine dynamic types to encode.
const preparedParameters = internal.prepareParameters({
checksumAddress,
parameters: parameters as readonly Parameter[],
values: values as any,
})
Expand All @@ -197,6 +211,15 @@ export declare namespace encode {
| internal.encode.ErrorType
| internal.prepareParameters.ErrorType
| Errors.GlobalErrorType

type Options = {
/**
* Whether addresses should be checked against their checksum.
*
* @default false
*/
checksumAddress?: boolean | undefined
}
}

/**
Expand Down
11 changes: 11 additions & 0 deletions src/core/_test/AbiParameters.decode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,17 @@ test('options: as = Object', () => {
}
})

test('options: checksumAddress = true', () => {
const result = AbiParameters.decode(
AbiParameters.from('(uint256 x, bool y, address z)'),
'0x00000000000000000000000000000000000000000000000000000000000001a40000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac',
{ checksumAddress: true },
)
expect(result).toEqual([
{ x: 420n, y: true, z: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC' },
])
})

describe('seaport', () => {
test('cancel', () => {
const cancel = AbiItem.fromAbi(seaportContractConfig.abi, 'cancel')
Expand Down
36 changes: 36 additions & 0 deletions src/core/_test/AbiParameters.encode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1725,6 +1725,42 @@ describe('seaport', () => {
})
})

test('options: checksumAddress = true', () => {
expect(
AbiParameters.encode(
AbiParameters.from('(uint256 x, bool y, address z)'),
[
{
x: 420n,
y: true,
z: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC',
},
],
{ checksumAddress: true },
),
).toMatchInlineSnapshot(
'"0x00000000000000000000000000000000000000000000000000000000000001a40000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac"',
)

expect(() =>
AbiParameters.encode(
AbiParameters.from('(uint256 x, bool y, address z)'),
[
{
x: 420n,
y: true,
z: '0xa5cC3c03994DB5b0d9A5eEdD10CabaB0813678AC',
},
],
{ checksumAddress: true },
),
).toThrowErrorMatchingInlineSnapshot(`
[Address.InvalidAddressError: Address "0xa5cC3c03994DB5b0d9A5eEdD10CabaB0813678AC" is invalid.
Details: Address does not match its checksum counterpart.]
`)
})

test('invalid type', () => {
expect(() =>
AbiParameters.encode([{ name: 'x', type: 'lol' }], [69]),
Expand Down
91 changes: 72 additions & 19 deletions src/core/internal/abiParameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,25 @@ export type Tuple = ParameterToPrimitiveType<TupleAbiParameter>
export function decodeParameter(
cursor: Cursor.Cursor,
param: AbiParameters.Parameter,
{ staticPosition }: { staticPosition: number },
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 }, { length, staticPosition })
return decodeArray(
cursor,
{ ...param, type },
{ checksumAddress, length, staticPosition },
)
}
if (param.type === 'tuple')
return decodeTuple(cursor, param as TupleAbiParameter, { staticPosition })

if (param.type === 'address') return decodeAddress(cursor)
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 })
Expand All @@ -100,9 +108,15 @@ const sizeOfLength = 32
const sizeOfOffset = 32

/** @internal */
export function decodeAddress(cursor: Cursor.Cursor) {
export function decodeAddress(
cursor: Cursor.Cursor,
options: { checksum?: boolean | undefined } = {},
) {
const { checksum = false } = options
const value = cursor.readBytes(32)
return [Hex.fromBytes(Bytes.slice(value, -20)), 32]
const wrap = (address: Hex.Hex) =>
checksum ? Address.checksum(address) : address
return [wrap(Hex.fromBytes(Bytes.slice(value, -20))), 32]
}

export declare namespace decodeAddress {
Expand All @@ -116,8 +130,14 @@ export declare namespace decodeAddress {
export function decodeArray(
cursor: Cursor.Cursor,
param: AbiParameters.Parameter,
{ length, staticPosition }: { length: number | null; staticPosition: number },
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) {
Expand All @@ -142,6 +162,7 @@ export function decodeArray(
// 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_
Expand All @@ -168,6 +189,7 @@ export function decodeArray(
// 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)
Expand All @@ -184,6 +206,7 @@ export function decodeArray(
const value: unknown[] = []
for (let i = 0; i < length; ++i) {
const [data, consumed_] = decodeParameter(cursor, param, {
checksumAddress,
staticPosition: staticPosition + consumed,
})
consumed += consumed_
Expand Down Expand Up @@ -278,8 +301,10 @@ export type TupleAbiParameter = AbiParameters.Parameter & {
export function decodeTuple(
cursor: Cursor.Cursor,
param: TupleAbiParameter,
{ staticPosition }: { staticPosition: number },
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
Expand All @@ -305,6 +330,7 @@ export function decodeTuple(
const component = param.components[i]!
cursor.setPosition(start + consumed)
const [data, consumed_] = decodeParameter(cursor, component, {
checksumAddress,
staticPosition: start,
})
consumed += consumed_
Expand All @@ -321,6 +347,7 @@ export function decodeTuple(
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
Expand Down Expand Up @@ -374,9 +401,11 @@ export declare namespace decodeString {
export function prepareParameters<
const parameters extends AbiParameters.AbiParameters,
>({
checksumAddress,
parameters,
values,
}: {
checksumAddress?: boolean | undefined
parameters: parameters
values: parameters extends AbiParameters.AbiParameters
? ToPrimitiveTypes<parameters>
Expand All @@ -385,7 +414,11 @@ export function prepareParameters<
const preparedParameters: PreparedParameter[] = []
for (let i = 0; i < parameters.length; i++) {
preparedParameters.push(
prepareParameter({ parameter: parameters[i]!, value: values[i] }),
prepareParameter({
checksumAddress,
parameter: parameters[i]!,
value: values[i],
}),
)
}
return preparedParameters
Expand All @@ -400,20 +433,23 @@ export declare namespace prepareParameters {
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,
Expand All @@ -423,11 +459,14 @@ export function prepareParameter<
}
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)
return encodeAddress(value as unknown as Hex.Hex, {
checksum: checksumAddress,
})
}
if (parameter.type === 'bool') {
return encodeBoolean(value as unknown as boolean)
Expand Down Expand Up @@ -503,8 +542,12 @@ export declare namespace encode {
}

/** @internal */
export function encodeAddress(value: Hex.Hex): PreparedParameter {
Address.assert(value, { strict: false })
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),
Expand All @@ -522,14 +565,14 @@ export declare namespace encodeAddress {
/** @internal */
export function encodeArray<const parameter extends AbiParameters.Parameter>(
value: ParameterToPrimitiveType<parameter>,
{
length,
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)
Expand All @@ -543,7 +586,11 @@ export function encodeArray<const parameter extends AbiParameters.Parameter>(
let dynamicChild = false
const preparedParameters: PreparedParameter[] = []
for (let i = 0; i < value.length; i++) {
const preparedParam = prepareParameter({ parameter, value: value[i] })
const preparedParam = prepareParameter({
checksumAddress,
parameter,
value: value[i],
})
if (preparedParam.dynamic) dynamicChild = true
preparedParameters.push(preparedParam)
}
Expand Down Expand Up @@ -697,14 +744,20 @@ export function encodeTuple<
},
>(
value: ParameterToPrimitiveType<parameter>,
{ parameter }: { parameter: 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[],
})
Expand Down

0 comments on commit c09d165

Please sign in to comment.