-
Notifications
You must be signed in to change notification settings - Fork 0
feat: private event utilities #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/update-sd-dependency-match-lock-viem
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) |
| 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 |
| 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) |
| 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' | ||
| 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> = | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm sorry but, I am a bit confused and I need your help to clarify why we need these utils... |
||
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error is actually thrown. Remove the class and the doc or throw the error. |
||
| */ | ||
| 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
|
||
|
|
||
| return topics as AbiEventTopicsArray<ABI> | ||
| } | ||
| 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 = ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rename this function from |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should consider using the SDInterface.parseLog method from @appliedblockchain/silentdatarollup-ethers-provider instead of this custom unwrapPrivateEvent implementation. The ethers provider has an implementation that handles to parse the private logs. See https://github.com/appliedblockchain/silent-data-rollup-providers/blob/main/packages/ethers-provider/src/sdInterface.ts#L28 for reference. However, since we're using viem not ethers there might be conflicts. If the only conflict is only TS let's ignore add a comment and a working vversion for Viem will come in the future. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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, | |
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ABI organization is clean, but does this follow the Skandha pattern? The existing codebase has contract types in packages/types/src/contracts/EPv7/core/ and factories in packages/types/src/contracts/EPv7/factories/.
If so then my comment doesn't make sense, if not, let's use the skanda pattern.
We should do surgical modifications/additions. Because at some point we will update our version with the most up to date version of them.