Skip to content
Open
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
36 changes: 36 additions & 0 deletions packages/executor/src/utils/abi-events/AccountDeployed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { AbiEvent, toEventHash } from 'viem'

export const AccountDeployedAbi = <const>{
anonymous: false,
inputs: [
{
indexed: true,
internalType: "bytes32",
name: "userOpHash",
type: "bytes32",
},
{
indexed: true,
internalType: "address",
name: "sender",
type: "address",
},
{
indexed: false,
internalType: "address",
name: "factory",
type: "address",
},
{
indexed: false,
internalType: "address",
name: "paymaster",
type: "address",
},
],
name: "AccountDeployed",
type: "event",
} satisfies AbiEvent
export type AccountDeployedAbi = typeof AccountDeployedAbi

export const ACCOUNT_DEPLOYED_EVENT_HASH = toEventHash(AccountDeployedAbi)
28 changes: 28 additions & 0 deletions packages/executor/src/utils/abi-events/PrivateEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { AbiEvent } from 'viem'

export const PrivateEventAbi = <const>{
anonymous: false,
inputs: [
{
indexed: false,
internalType: "address[]",
name: "allowedViewers",
type: "address[]",
},
{
indexed: true,
internalType: "bytes32",
name: "eventType",
type: "bytes32",
},
{
indexed: false,
internalType: "bytes",
name: "payload",
type: "bytes",
},
],
name: "PrivateEvent",
type: "event",
} satisfies AbiEvent
export type PrivateEventAbi = typeof PrivateEventAbi
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { AbiEvent } from 'viem'

export const SignatureAggregatorChangedAbi = <const>{
anonymous: false,
inputs: [
{
indexed: true,
internalType: "address",
name: "aggregator",
type: "address",
},
],
name: "SignatureAggregatorChanged",
type: "event",
} satisfies AbiEvent
export type SignatureAggregatorChangedAbi = typeof SignatureAggregatorChangedAbi
54 changes: 54 additions & 0 deletions packages/executor/src/utils/abi-events/UserOperationEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { AbiEvent, toEventHash } from 'viem'

export const UserOperationEventAbi = <const>{
anonymous: false,
inputs: [
{
indexed: true,
internalType: "bytes32",
name: "userOpHash",
type: "bytes32",
},
{
indexed: true,
internalType: "address",
name: "sender",
type: "address",
},
{
indexed: true,
internalType: "address",
name: "paymaster",
type: "address",
},
{
indexed: false,
internalType: "uint256",
name: "nonce",
type: "uint256",
},
{
indexed: false,
internalType: "bool",
name: "success",
type: "bool",
},
{
indexed: false,
internalType: "uint256",
name: "actualGasCost",
type: "uint256",
},
{
indexed: false,
internalType: "uint256",
name: "actualGasUsed",
type: "uint256",
},
],
name: "UserOperationEvent",
type: "event",
} satisfies AbiEvent
export type UserOperationEventAbi = typeof UserOperationEventAbi

export const USER_OPERATION_EVENT_HASH = toEventHash(UserOperationEventAbi)
4 changes: 4 additions & 0 deletions packages/executor/src/utils/abi-events/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './AccountDeployed'
export * from './PrivateEvent'
export * from './SignatureAggregatorChanged'
export * from './UserOperationEvent'
96 changes: 96 additions & 0 deletions packages/executor/src/utils/extractEventAbiTopics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { AbiParameter } from "abitype"
import { AbiEvent, decodeAbiParameters, encodeEventTopics, Hex, Log } from "viem"


export class AbiEventTopicsCountMismatchError extends Error {
indexedParams: readonly AbiParameter[]
expectedCount: number
actualCount: number

constructor({
indexedParams,
expectedCount,
actualCount,
}: {
indexedParams: readonly AbiParameter[]
expectedCount: number
actualCount: number
}) {
const indexedParamsStr = indexedParams
.map(p => `${p.type}${p.name ? ` ${p.name}` : ''}`)
.join(', ')

const message = [
`Topics count mismatch: expected ${expectedCount}, got ${actualCount}`,
`Indexed params: (${indexedParamsStr})`,
].join('\n')

super(message)

// Restore prototype chain for instanceof checks
Object.setPrototypeOf(this, AbiEventTopicsCountMismatchError.prototype)
this.name = 'AbiEventTopicsCountMismatchError'
this.indexedParams = indexedParams
this.expectedCount = expectedCount
this.actualCount = actualCount
}
}

/**
* Extracts indexed parameters into a fixed length array of hex
*
* @param Inputs - the inputs of the event (the indexed parameters)
* @param Acc - the accumulator (the array of hex)
* @returns the indexed parameters as a fixed length array of hex
*/
type ExtractIndexedAsHex<
Inputs extends readonly any[],
Acc extends Hex[] = []
> = Inputs extends readonly [infer First, ...infer Rest]
? First extends { indexed: true }
? ExtractIndexedAsHex<Rest, [...Acc, Hex]>
: ExtractIndexedAsHex<Rest, Acc>
: Acc;

/**
* Builds the topics fixed-length array type
*
* @param ABI - the abi of the event to extract the topics from
* @returns the topics fixed-length array type
*/
type AbiEventTopicsArray<ABI extends AbiEvent> =
ABI['inputs'] extends readonly [...infer Inputs]
? [Hex, ...ExtractIndexedAsHex<Inputs>]
: [Hex];

/**
* Extracts the topics from an event
*
* @param abi - the abi of the event to extract the topics from
* @param payload - the payload of the event
* @returns fixed-length array with the encoded event topics
* @throws an {@link AbiEventTopicsCountMismatchError} if the topics count mismatch
*/
export function extractEventAbiTopics<
ABI extends AbiEvent
>(
abi: ABI,
payload: Hex,
): AbiEventTopicsArray<ABI> {
const allParams = decodeAbiParameters<ABI['inputs']>(abi.inputs, payload)

const indexedValues: unknown[] = []
abi.inputs.forEach((input, index) => {
if (input.indexed) {
indexedValues.push(allParams[index])
}
})

// @ts-expect-error - dynamically extracted indexedValues cannot be proven to match generic tuple type
const topics = encodeEventTopics({
abi: [abi],
args: indexedValues
})
Comment on lines +89 to +93
Copy link

Copilot AI Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @ts-expect-error directive suppresses type checking without validation. Consider using @ts-ignore with a more specific explanation, or better yet, add runtime validation to ensure indexedValues matches the expected structure before encoding.

Copilot uses AI. Check for mistakes.

return topics as AbiEventTopicsArray<ABI>
}
51 changes: 51 additions & 0 deletions packages/executor/src/utils/unwrapPrivateEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { AbiEvent, decodeEventLog, Log } from 'viem'
import { PrivateEventAbi } from './abi-events'
import { extractEventAbiTopics } from './extractEventAbiTopics'

type PrivateEventLog = Log<bigint, number, false, PrivateEventAbi>
type PrivateEventLogWithArgs = PrivateEventLog & {
args: Required<PrivateEventLog['args']>
}

/**
* Checks if a private event log has all the args defined.
*
* @param x - the private event log to check if it has args
* @returns true if the private event log has args, false otherwise
*/
export const isPrivateEventLogWithArgs = (
x: PrivateEventLog,
): x is PrivateEventLogWithArgs => (
x.args.allowedViewers !== undefined &&
x.args.eventType !== undefined &&
x.args.payload !== undefined
)

/**
* Unwraps a private event log into a regular event log.
*
* @param abi - the abi of the event to unwrap
* @param ev - the private event log to unwrap
* @returns the unwrapped event log
* @throws if there is an error decoding the event log or extracting the topics
*/
export function unwrapPrivateEvent<ABI extends AbiEvent>(
abi: ABI,
ev: PrivateEventLogWithArgs
): Log<bigint, number, false, ABI> {
const decoded = decodeEventLog({
abi: [abi],
data: ev.args.payload,
topics: [], // empty so viem decodes all params from the payload
});
const topics = extractEventAbiTopics(abi, ev.args.payload)

// Note: decoded.args has type `readonly unknown[] | ...` because viem can't
// narrow the type when topics is empty, but at runtime it is correctly decoded.
return {
...ev,
eventName: abi.name,
args: decoded.args,
topics,
} as unknown as Log<bigint, number, false, ABI>
}
Comment on lines +45 to +51
Copy link

Copilot AI Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double type assertion (as unknown as) bypasses type safety. Consider restructuring the return type or adding runtime validation to ensure the shape matches Log<bigint, number, false, ABI> before returning.

Suggested change
return {
...ev,
eventName: abi.name,
args: decoded.args,
topics,
} as unknown as Log<bigint, number, false, ABI>
}
// Runtime validation: ensure decoded.args is an object and matches ABI inputs
if (
typeof decoded.args !== 'object' ||
decoded.args === null
) {
throw new Error('Decoded args is not an object');
}
// Optionally, check that all ABI inputs are present in decoded.args
if (
!abi.inputs.every(input => input.name in (decoded.args as object))
) {
throw new Error('Decoded args does not match ABI inputs');
}
return {
...ev,
eventName: abi.name,
args: decoded.args,
topics,
};

Copilot uses AI. Check for mistakes.