diff --git a/packages/api/src/app.ts b/packages/api/src/app.ts index 71020920..e3cdf4c0 100644 --- a/packages/api/src/app.ts +++ b/packages/api/src/app.ts @@ -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 { // 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) { + throw new RpcError( + "Missing Silent Data auth headers", + RpcErrorCodes.METHOD_NOT_FOUND + ); + } + 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(); diff --git a/packages/api/src/modules/eth.ts b/packages/api/src/modules/eth.ts index af5d7df6..5cd8c327 100644 --- a/packages/api/src/modules/eth.ts +++ b/packages/api/src/modules/eth.ts @@ -1,6 +1,7 @@ import { Eth } from "@skandha/executor/lib/modules/eth"; import { EstimatedUserOperationGas, + SDAuthHeaders, UserOperationByHashResponse, UserOperationReceipt, } from "@skandha/types/lib/api/interfaces"; @@ -53,9 +54,10 @@ export class EthAPI { * with the addition of entryPoint, blockNumber, blockHash and transactionHash */ async getUserOperationByHash( - hash: string + hash: string, + sdAuthHeaders: SDAuthHeaders, ): Promise { - return await this.ethModule.getUserOperationByHash(hash); + return await this.ethModule.getUserOperationByHash(hash, sdAuthHeaders); } /** @@ -64,9 +66,10 @@ export class EthAPI { * @returns a UserOperation receipt */ async getUserOperationReceipt( - hash: string + hash: string, + sdAuthHeaders: SDAuthHeaders, ): Promise { - return await this.ethModule.getUserOperationReceipt(hash); + return await this.ethModule.getUserOperationReceipt(hash, sdAuthHeaders); } /** diff --git a/packages/executor/src/modules/eth.ts b/packages/executor/src/modules/eth.ts index d8b0bd04..942fc8a3 100644 --- a/packages/executor/src/modules/eth.ts +++ b/packages/executor/src/modules/eth.ts @@ -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 { - const entry = await this.mempoolService.getEntryByHash(hash); - 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 + // const entry = await this.mempoolService.getEntryByHash(hash); + // 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, sdAuthHeaders, + ); if (!rpcUserOp && this.blockscoutApi) { return await this.blockscoutApi.getUserOperationByHash(hash); } @@ -511,10 +516,11 @@ export class Eth { * @returns a UserOperation receipt */ async getUserOperationReceipt( - hash: string + hash: string, + sdAuthHeaders: SDAuthHeaders, ): Promise { const rpcUserOp = await this.entryPointService.getUserOperationReceipt( - hash + hash, sdAuthHeaders, ); if (!rpcUserOp && this.blockscoutApi) { return await this.blockscoutApi.getUserOperationReceipt(hash); diff --git a/packages/executor/src/services/EntryPointService/service.ts b/packages/executor/src/services/EntryPointService/service.ts index 07f46580..c5c7a2bb 100644 --- a/packages/executor/src/services/EntryPointService/service.ts +++ b/packages/executor/src/services/EntryPointService/service.ts @@ -2,6 +2,7 @@ import { UserOperation } from "@skandha/types/lib/contracts/UserOperation"; import { IDbController, Logger } from "@skandha/types/lib"; import { + SDAuthHeaders, UserOperationByHashResponse, UserOperationReceipt, } from "@skandha/types/lib/api/interfaces"; @@ -39,7 +40,8 @@ export class EntryPointService { /** View functions */ async getUserOperationByHash( - userOpHash: string + userOpHash: string, + sdAuthHeaders: SDAuthHeaders, ): Promise { if (!userOpHash) { throw new RpcError( @@ -49,7 +51,7 @@ export class EntryPointService { } for (const [_, entryPoint] of Object.entries(this.entryPoints)) { try { - const res = entryPoint.getUserOperationByHash(userOpHash); + const res = entryPoint.getUserOperationByHash(userOpHash, sdAuthHeaders); if (res) return res; } catch (err) { /* empty */ @@ -59,7 +61,8 @@ export class EntryPointService { } async getUserOperationReceipt( - userOpHash: string + userOpHash: string, + sdAuthHeaders: SDAuthHeaders, ): Promise { if (!userOpHash) { throw new RpcError( @@ -69,7 +72,7 @@ export class EntryPointService { } for (const [_, entryPoint] of Object.entries(this.entryPoints)) { try { - const res = entryPoint.getUserOperationReceipt(userOpHash); + const res = entryPoint.getUserOperationReceipt(userOpHash, sdAuthHeaders); if (res) return res; } catch (err) { /* empty */ diff --git a/packages/executor/src/services/EntryPointService/versions/0.0.7.ts b/packages/executor/src/services/EntryPointService/versions/0.0.7.ts index 1e58c28a..ca58bddf 100644 --- a/packages/executor/src/services/EntryPointService/versions/0.0.7.ts +++ b/packages/executor/src/services/EntryPointService/versions/0.0.7.ts @@ -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 + 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]) => { + if (value !== undefined) { + headersToSend[key] = value + } + return headersToSend + }, {} as Record), + }, + }), + }) + + const privateLogs = await clientWithHeaders.getLogs({ address: this.address, event: parseAbiItem([ - 'event UserOperationEvent(bytes32 indexed userOpHash, address indexed sender, address indexed paymaster, uint256 nonce, bool success, uint256 actualGasCost, uint256 actualGasUsed)' + 'event PrivateEvent(address[] allowedViewers, bytes32 indexed eventType, bytes payload)', ]), fromBlock, args: { - userOpHash + eventType: USER_OPERATION_EVENT_HASH, } }); - if(logs[0]) { - return logs[0]; + + for (const log of privateLogs) { + if (!isPrivateEventLogWithArgs(log)) { + continue; + } + + try { + const unwrapped = unwrapPrivateEvent(UserOperationEventAbi, log); + + if (unwrapped.args.userOpHash === userOpHash) { + return unwrapped; + } + } catch (error) { + this.logger.error( + `Failed to unwrap PrivateEvent for ${UserOperationEventAbi.name}: ${error instanceof Error ? error.message : JSON.stringify(error)}` + ); + } } } catch (err) { this.logger.error(err); @@ -278,9 +336,10 @@ export class EntryPointV7Service implements IEntryPointService { } async getUserOperationReceipt( - hash: Hex + hash: Hex, + sdAuthHeaders: SDAuthHeaders, ): Promise { - const event = await this.getUserOperationEvent(hash); + const event = await this.getUserOperationEvent(hash, sdAuthHeaders); if (!event) { return null; } @@ -300,9 +359,10 @@ export class EntryPointV7Service implements IEntryPointService { } async getUserOperationByHash( - hash: Hex + hash: Hex, + sdAuthHeaders: SDAuthHeaders, ): Promise { - const event = await this.getUserOperationEvent(hash); + const event = await this.getUserOperationEvent(hash, sdAuthHeaders); if (!event) { return null; } diff --git a/packages/executor/src/services/EntryPointService/versions/base.ts b/packages/executor/src/services/EntryPointService/versions/base.ts index 7e49db13..fc39ef2e 100644 --- a/packages/executor/src/services/EntryPointService/versions/base.ts +++ b/packages/executor/src/services/EntryPointService/versions/base.ts @@ -4,6 +4,7 @@ import { UserOperation } from "@skandha/types/lib/contracts/UserOperation"; import { IStakeManager } from "@skandha/types/lib/contracts/EPv7/core/StakeManager"; import { UserOperationEventEvent } from "@skandha/types/lib/contracts/EPv6/EntryPoint"; import { + SDAuthHeaders, UserOperationByHashResponse, UserOperationReceipt, } from "@skandha/types/lib/api/interfaces"; @@ -36,11 +37,16 @@ export interface IEntryPointService { simulateValidation(userOp: UserOperation): Promise; getUserOperationEvent( - userOpHash: string + userOpHash: string, + sdAuthHeaders: SDAuthHeaders, ): Promise; - getUserOperationReceipt(hash: string): Promise; + getUserOperationReceipt( + hash: string, + sdAuthHeaders: SDAuthHeaders, + ): Promise; getUserOperationByHash( - hash: string + hash: string, + sdAuthHeaders: SDAuthHeaders, ): Promise; encodeHandleOps(userOps: UserOperation[], beneficiary: string): Hex; diff --git a/packages/types/src/api/interfaces.ts b/packages/types/src/api/interfaces.ts index a341e765..371d7f22 100644 --- a/packages/types/src/api/interfaces.ts +++ b/packages/types/src/api/interfaces.ts @@ -125,3 +125,32 @@ export type UserOperationStatus = { transaction?: string; reason?: string; }; + +const SDAuthHeaders = [ + 'x-timestamp', + 'x-signature', + 'x-from-block', // consumed by the bundler + 'x-signer-swc', + 'x-delegate', + 'x-eip712-signature', + 'x-delegate-signature', + 'x-eip712-delegate-signature', +] +export type SDAuthHeaders = Record< + typeof SDAuthHeaders[number], + string | undefined +> + +export function pickSDAuthHeaders( + requestHeaders: Record, +): SDAuthHeaders { + return SDAuthHeaders.reduce( + (sdHeaders, header) => { + if (header in requestHeaders) { + sdHeaders[header] = requestHeaders[header] + } + return sdHeaders + }, + {} as SDAuthHeaders, + ) +}