Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/weak-gifts-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ox": patch
---

Added [ERC-7821](https://eips.ethereum.org/EIPS/eip-7821) modules.
134 changes: 134 additions & 0 deletions src/erc7821/Calls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import * as AbiParameters from '../core/AbiParameters.js'
import type * as Address from '../core/Address.js'
import type * as Hex from '../core/Hex.js'

export type Call<bigintType = bigint> = {
data?: Hex.Hex | undefined
to: Address.Address
value?: bigintType | undefined
}

/**
* Encodes a set of ERC-7821 calls.
*
* @example
* ```ts twoslash
* import { Calls } from 'ox/erc7821'
*
* const calls = Calls.encode([
* {
* data: '0xdeadbeef',
* to: '0xcafebabecafebabecafebabecafebabecafebabe',
* value: 1n,
* },
* {
* data: '0xcafebabe',
* to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef',
* value: 2n,
* },
* ])
* // @log: '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000deadbeef000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cafebabe'
* ```
*
* @param calls - Calls to encode.
* @param options - Options for the encoding.
* @returns The encoded calls.
*/
export function encode(calls: readonly Call[], options: encode.Options = {}) {
const { opData } = options
return AbiParameters.encode(getAbiParameters({ opData: !!opData }), [
calls.map((call) => ({
target: call.to,
value: call.value ?? 0n,
data: call.data ?? '0x',
})),
...(opData ? [opData] : []),
] as any)
}

export declare namespace encode {
type Options = {
/** Additional data to include for execution. */
opData?: Hex.Hex | undefined
}
}

export function getAbiParameters({ opData }: getAbiParameters.Options = {}) {

Check failure on line 56 in src/erc7821/Calls.ts

View workflow job for this annotation

GitHub Actions / Verify / Checks

Missing JSDoc comment
return AbiParameters.from([
'struct Call { address target; uint256 value; bytes data; }',
'Call[] calls',
...(opData ? ['bytes opData'] : []),
])
}

export declare namespace getAbiParameters {
type Options = {
opData?: boolean | undefined
}
}

/**
* Decodes a set of ERC-7821 calls from encoded data.
*
* @example
* ```ts twoslash
* import { Calls } from 'ox/erc7821'
*
* const data = Calls.decode('0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000cafebabecafebabecafebabecafebabecafebabe000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004deadbeef00000000000000000000000000000000000000000000000000000000000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004cafebabe00000000000000000000000000000000000000000000000000000000')
* // @log: {
* // @log: calls: [
* // @log: {
* // @log: data: '0xdeadbeef',
* // @log: to: '0xcafebabecafebabecafebabecafebabecafebabe',
* // @log: value: 1n,
* // @log: },
* // @log: {
* // @log: data: '0xcafebabe',
* // @log: to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef',
* // @log: value: 2n,
* // @log: },
* // @log: ]
* // @log: }
* ```
*
* @param data - The encoded calls data.
* @param options - Options for decoding.
* @returns The decoded calls and optional opData.
*/
export function decode(
data: Hex.Hex,
options: decode.Options = {},
): decode.ReturnType {
const { opData: withOpData = false } = options

const decoded = AbiParameters.decode(
getAbiParameters({ opData: withOpData }),
data,
) as readonly unknown[]
const [encodedCalls, opData] = decoded as readonly [
{ target: Address.Address; value: bigint; data: Hex.Hex }[],
Hex.Hex?,
]

const calls = encodedCalls.map((call) => ({
to: call.target,
value: call.value,
data: call.data,
}))

return withOpData
? { calls, opData: opData === '0x' ? undefined : opData }
: { calls }
}

export declare namespace decode {
type Options = {
/** Whether to decode opData if present. */
opData?: boolean | undefined
}

type ReturnType = {
calls: Call[]
opData?: Hex.Hex | undefined
}
}
197 changes: 197 additions & 0 deletions src/erc7821/Execute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import * as AbiFunction from '../core/AbiFunction.js'
import type * as Hex from '../core/Hex.js'
import { AbiParameters } from '../index.js'
import * as Calls from './Calls.js'

export type Batch = {
calls: readonly Call[]
opData?: Hex.Hex | undefined
}

export type Call = Calls.Call

export const abiFunction = {
type: 'function',
name: 'execute',
inputs: [
{
name: 'mode',
type: 'bytes32',
internalType: 'bytes32',
},
{
name: 'executionData',
type: 'bytes',
internalType: 'bytes',
},
],
outputs: [],
stateMutability: 'payable',
} satisfies AbiFunction.AbiFunction

export const mode = {
default: '0x0100000000000000000000000000000000000000000000000000000000000000',
opData: '0x0100000000007821000100000000000000000000000000000000000000000000',
batchOfBatches:
'0x0100000000007821000200000000000000000000000000000000000000000000',
} as const

/**
* Decodes calls from ERC-7821 `execute` function data.
*
* @example
* ```ts twoslash
* import { Execute } from 'ox/erc7821'
*
* const { calls } = Execute.decodeData('0x...')
* ```
*
* @param data - The encoded data.
* @returns The decoded calls and optional opData.
*/
export function decodeData(data: Hex.Hex): decodeData.ReturnType {
const [m, executionData] = AbiFunction.decodeData(abiFunction, data) as [
Hex.Hex,
Hex.Hex,
]
return Calls.decode(executionData, { opData: m !== mode.default })
}

export declare namespace decodeData {
type ReturnType = {
calls: Call[]
opData?: Hex.Hex | undefined
}
}

/**
* Decodes batches from ERC-7821 `execute` function data in "batch of batches" mode.
*
* @example
* ```ts twoslash
* import { Execute } from 'ox/erc7821'
*
* const batches = Execute.decodeBatchOfBatchesData('0x...')
* ```
*
* @param data - The encoded data.
* @returns The decoded batches.
*/
export function decodeBatchOfBatchesData(
data: Hex.Hex,
): decodeBatchOfBatchesData.ReturnType {
const [, executionData] = AbiFunction.decodeData(abiFunction, data) as [
Hex.Hex,
Hex.Hex,
]

const [encodedBatches] = AbiParameters.decode(
AbiParameters.from('bytes[]'),
executionData,
) as readonly [Hex.Hex[]]

return encodedBatches.map((encodedBatch) => {
// Try decoding with opData first
try {
const decoded = Calls.decode(encodedBatch, { opData: true })
if (decoded.opData) {
return {
calls: decoded.calls,
opData: decoded.opData,
}
}
// If opData is undefined, return without it
return { calls: decoded.calls }
} catch {
// If decoding with opData fails, decode without it
const decoded = Calls.decode(encodedBatch, { opData: false })
return { calls: decoded.calls }
}
})
}

export declare namespace decodeBatchOfBatchesData {
type ReturnType = Batch[]
}

/**
* Encodes calls for the ERC-7821 `execute` function with "batch of batches" mode.
*
* @example
* ```ts twoslash
* import { Execute } from 'ox/erc7821'
*
* const data = Execute.encodeBatchOfBatchesData([
* {
* calls: [
* {
* data: '0xcafebabe',
* to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef',
* value: 1n,
* }
* ]
* },
* {
* calls: [
* {
* data: '0xdeadbeef',
* to: '0xcafebabecafebabecafebabecafebabecafebabe',
* value: 2n,
* }
* ],
* opData: '0xcafebabe',
* }
* ])
* ```
*
* @param calls - The calls to encode.
* @param options - The options.
* @returns The encoded data.
*/
export function encodeBatchOfBatchesData(batches: readonly Batch[]) {
const b = AbiParameters.encode(AbiParameters.from('bytes[]'), [
batches.map((b) => {
const batch = b as Batch
return Calls.encode(batch.calls, {
opData: batch.opData,
})
}),
])
return AbiFunction.encodeData(abiFunction, [mode.batchOfBatches, b])
}

/**
* Encodes calls for the ERC-7821 `execute` function.
*
* @example
* ```ts twoslash
* import { Execute } from 'ox/erc7821'
*
* const data = Execute.encodeData([
* {
* data: '0xcafebabe',
* to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef',
* value: 1n,
* }
* ])
* ```
*
* @param calls - The calls to encode.
* @param options - The options.
* @returns The encoded data.
*/
export function encodeData(
calls: readonly Call[],
options: encodeData.Options = {},
) {
const { opData } = options
const c = Calls.encode(calls, { opData })
const m = opData ? mode.opData : mode.default
return AbiFunction.encodeData(abiFunction, [m, c])
}

export declare namespace encodeData {
type Options = {
opData?: Hex.Hex | undefined
}
}
Loading
Loading