-
Notifications
You must be signed in to change notification settings - Fork 0
feat: entry point service private event support #6
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/private-event-utilities
Are you sure you want to change the base?
Changes from all commits
b7a6274
2413215
52ef3fc
c322029
59ad0f0
eede633
3fa6709
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 |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ import { | |
| import { SkandhaAPI } from "./modules/skandha"; | ||
| import { JsonRpcRequest, JsonRpcResponse } from "./interface"; | ||
| import { Server } from "./server"; | ||
| import { pickSDAuthHeaders, SDAuthHeaders } from '@skandha/types/lib/api/interfaces' | ||
|
|
||
| export interface RpcHandlerOptions { | ||
| config: Config; | ||
|
|
@@ -94,14 +95,18 @@ export class ApiApp { | |
| request, | ||
| req.ip, | ||
| req.headers.authorization | ||
| // Silent Data auth headers not supported for websocket requests | ||
| ) | ||
| ); | ||
| } | ||
| } else { | ||
| const sdAuthHeaders = pickSDAuthHeaders(req.headers); | ||
|
|
||
| response = await this.handleRpcRequest( | ||
| body as JsonRpcRequest, | ||
| req.ip, | ||
| req.headers.authorization | ||
| req.headers.authorization, | ||
| sdAuthHeaders, | ||
| ); | ||
| } | ||
| return res.status(HttpStatus.OK).send(response); | ||
|
|
@@ -124,6 +129,7 @@ export class ApiApp { | |
| ); | ||
| if (!wsRpc) { | ||
| try { | ||
| // Silent Data auth headers not supported for websocket requests | ||
| response = await this.handleRpcRequest(request, ""); | ||
| } catch (err) { | ||
| const { jsonrpc, id } = request; | ||
|
|
@@ -190,7 +196,8 @@ export class ApiApp { | |
| private async handleRpcRequest( | ||
| request: JsonRpcRequest, | ||
| ip: string, | ||
| authKey?: string | ||
| authKey?: string, | ||
| sdAuthHeaders?: SDAuthHeaders, | ||
| ): Promise<JsonRpcResponse> { | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| let result: any; | ||
|
|
@@ -290,10 +297,22 @@ export class ApiApp { | |
| break; | ||
| } | ||
| case BundlerRPCMethods.eth_getUserOperationReceipt: | ||
| result = await this.ethApi.getUserOperationReceipt(params[0]); | ||
| if (!sdAuthHeaders) { | ||
|
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. Instead of just checking |
||
| throw new RpcError( | ||
|
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 need a new error code (e.g., MISSING_AUTH_HEADERS = -32604) instead of METHOD_NOT_FOUND. The error message should be: "Silent Data authentication headers are required for this method. Please include x-timestamp and x-signature (or x-eip712-signature) headers in your request." |
||
| "Missing Silent Data auth headers", | ||
| RpcErrorCodes.METHOD_NOT_FOUND | ||
| ); | ||
| } | ||
|
Comment on lines
+300
to
+305
|
||
| result = await this.ethApi.getUserOperationReceipt(params[0], sdAuthHeaders); | ||
| break; | ||
| case BundlerRPCMethods.eth_getUserOperationByHash: | ||
| result = await this.ethApi.getUserOperationByHash(params[0]); | ||
| if (!sdAuthHeaders) { | ||
| throw new RpcError( | ||
| "Missing Silent Data auth headers", | ||
| RpcErrorCodes.METHOD_NOT_FOUND | ||
| ); | ||
| } | ||
| result = await this.ethApi.getUserOperationByHash(params[0], sdAuthHeaders); | ||
| break; | ||
| case BundlerRPCMethods.web3_clientVersion: | ||
| result = this.web3Api.clientVersion(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,6 +2,7 @@ import RpcError from "@skandha/types/lib/api/errors/rpc-error"; | |||||
| import * as RpcErrorCodes from "@skandha/types/lib/api/errors/rpc-error-codes"; | ||||||
| import { | ||||||
| EstimatedUserOperationGas, | ||||||
| SDAuthHeaders, | ||||||
| UserOperationByHashResponse, | ||||||
| UserOperationReceipt, | ||||||
| } from "@skandha/types/lib/api/interfaces"; | ||||||
|
|
@@ -478,27 +479,31 @@ export class Eth { | |||||
| * with the addition of entryPoint, blockNumber, blockHash and transactionHash | ||||||
| */ | ||||||
| async getUserOperationByHash( | ||||||
| hash: string | ||||||
| hash: string, | ||||||
| sdAuthHeaders: SDAuthHeaders, | ||||||
| ): Promise<UserOperationByHashResponse | null> { | ||||||
| const entry = await this.mempoolService.getEntryByHash(hash); | ||||||
|
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. Nice one! We are skipping the mempool because it's up to the Custom RPC to decide on the Auth 👍 |
||||||
| if (entry) { | ||||||
| if (entry.status < MempoolEntryStatus.Submitted || entry.transaction) { | ||||||
| let transaction: GetTransactionReturnType | undefined = undefined; | ||||||
| if (entry.transaction) { | ||||||
| transaction = await this.publicClient.getTransaction({ | ||||||
| hash: entry.transaction as Hex | ||||||
| }); | ||||||
| } | ||||||
| return { | ||||||
| userOperation: hexlifyUserOp(entry.userOp), | ||||||
| entryPoint: entry.entryPoint, | ||||||
| transactionHash: transaction?.hash, | ||||||
| blockHash: transaction?.blockHash, | ||||||
| blockNumber: transaction?.blockNumber, | ||||||
| }; | ||||||
| } | ||||||
| } | ||||||
| const rpcUserOp = await this.entryPointService.getUserOperationByHash(hash); | ||||||
| // Silent Data auth is checked in the chain RPC, mempool check not supported | ||||||
|
||||||
| // Silent Data auth is checked in the chain RPC, mempool check not supported | |
| // Silent Data Auth is checked in the chain RPC, mempool check not supported |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,6 +16,7 @@ import { Logger } from "@skandha/types/lib"; | |||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||
| UserOperationReceipt, | ||||||||||||||||||||||||||
| UserOperationByHashResponse, | ||||||||||||||||||||||||||
| SDAuthHeaders, | ||||||||||||||||||||||||||
| } from "@skandha/types/lib/api/interfaces"; | ||||||||||||||||||||||||||
| import { deepHexlify } from "@skandha/utils/lib/hexlify"; | ||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||
|
|
@@ -56,7 +57,11 @@ import { | |||||||||||||||||||||||||
| decodeFunctionData, | ||||||||||||||||||||||||||
| GetContractReturnType, | ||||||||||||||||||||||||||
| toHex, | ||||||||||||||||||||||||||
| createPublicClient, | ||||||||||||||||||||||||||
| http, | ||||||||||||||||||||||||||
| } from "viem"; | ||||||||||||||||||||||||||
| import { USER_OPERATION_EVENT_HASH, UserOperationEventAbi } from '../../../utils/abi-events' | ||||||||||||||||||||||||||
| import { isPrivateEventLogWithArgs, unwrapPrivateEvent } from '../../../utils/unwrapPrivateEvent' | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export class EntryPointV7Service implements IEntryPointService { | ||||||||||||||||||||||||||
|
|
@@ -245,27 +250,80 @@ export class EntryPointV7Service implements IEntryPointService { | |||||||||||||||||||||||||
| /** UserOp Events */ | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| async getUserOperationEvent( | ||||||||||||||||||||||||||
| userOpHash: Hex | ||||||||||||||||||||||||||
| userOpHash: Hex, | ||||||||||||||||||||||||||
| sdAuthHeaders: SDAuthHeaders, | ||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||
| const { | ||||||||||||||||||||||||||
| 'x-from-block': headerFromBlock, | ||||||||||||||||||||||||||
| ...headersToForward | ||||||||||||||||||||||||||
| } = sdAuthHeaders | ||||||||||||||||||||||||||
|
Comment on lines
+256
to
+259
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||
| const blockNumber = await this.publicClient.getBlockNumber(); | ||||||||||||||||||||||||||
| let fromBlock = blockNumber - BigInt(this.networkConfig.receiptLookupRange); | ||||||||||||||||||||||||||
| let minBlockNumber = blockNumber - BigInt(this.networkConfig.receiptLookupRange); | ||||||||||||||||||||||||||
| // underflow check | ||||||||||||||||||||||||||
| if (fromBlock < 0) { | ||||||||||||||||||||||||||
| fromBlock = BigInt(0); | ||||||||||||||||||||||||||
| if (minBlockNumber < 0) { | ||||||||||||||||||||||||||
| minBlockNumber = BigInt(0); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| let fromBlock = headerFromBlock | ||||||||||||||||||||||||||
| ? BigInt(headerFromBlock) | ||||||||||||||||||||||||||
| : undefined | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // ensure value is within the receipt lookup range | ||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||
| fromBlock === undefined || | ||||||||||||||||||||||||||
| fromBlock > blockNumber || | ||||||||||||||||||||||||||
| fromBlock < minBlockNumber | ||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||
| this.logger.warn(`fromBlock is out of range, using minBlockNumber: ${minBlockNumber}`) | ||||||||||||||||||||||||||
| fromBlock = minBlockNumber | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| const logs = await this.publicClient.getLogs({ | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Create a public client with custom transport that includes sdAuthHeaders | ||||||||||||||||||||||||||
| const clientWithHeaders = createPublicClient({ | ||||||||||||||||||||||||||
| chain: this.publicClient.chain, | ||||||||||||||||||||||||||
| transport: http(this.networkConfig.rpcEndpoint, { | ||||||||||||||||||||||||||
| fetchOptions: { | ||||||||||||||||||||||||||
| headers: Object | ||||||||||||||||||||||||||
| .entries(headersToForward) | ||||||||||||||||||||||||||
| .reduce((headersToSend, [key, value]) => { | ||||||||||||||||||||||||||
|
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. Very minor: consider using Object.fromEntries with filter for better readability. |
||||||||||||||||||||||||||
| if (value !== undefined) { | ||||||||||||||||||||||||||
| headersToSend[key] = value | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| return headersToSend | ||||||||||||||||||||||||||
| }, {} as Record<string, string>), | ||||||||||||||||||||||||||
|
Comment on lines
+288
to
+295
|
||||||||||||||||||||||||||
| headers: Object | |
| .entries(headersToForward) | |
| .reduce((headersToSend, [key, value]) => { | |
| if (value !== undefined) { | |
| headersToSend[key] = value | |
| } | |
| return headersToSend | |
| }, {} as Record<string, string>), | |
| headers: Object.fromEntries( | |
| Object.entries(headersToForward) | |
| .filter(([_, value]) => value !== undefined) | |
| ), |
Copilot
AI
Nov 12, 2025
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 error message includes UserOperationEventAbi.name, but if the ABI object doesn't have a name property or it's undefined, this will log 'undefined'. Consider adding a fallback value like UserOperationEventAbi.name || 'UserOperationEvent' to ensure a meaningful error message.
| `Failed to unwrap PrivateEvent for ${UserOperationEventAbi.name}: ${error instanceof Error ? error.message : JSON.stringify(error)}` | |
| `Failed to unwrap PrivateEvent for ${UserOperationEventAbi.name || 'UserOperationEvent'}: ${error instanceof Error ? error.message : JSON.stringify(error)}` |
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.
This block code is not related to websockets but to batch requests.
I'm not sure if the bundler supports batch requests well in the custom RPC, but we should still extract sdAuthHeaders before the loop and pass them to each handleRpcRequest call. This ensures batch requests work with private event methods if they're supported.