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
27 changes: 23 additions & 4 deletions packages/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -94,14 +95,18 @@ export class ApiApp {
request,
req.ip,
req.headers.authorization
// Silent Data auth headers not supported for websocket requests

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.

)
);
}
} 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);
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -290,10 +297,22 @@ export class ApiApp {
break;
}
case BundlerRPCMethods.eth_getUserOperationReceipt:
result = await this.ethApi.getUserOperationReceipt(params[0]);
if (!sdAuthHeaders) {

Choose a reason for hiding this comment

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

Instead of just checking if (!sdAuthHeaders), create a validation method that checks for required headers. At minimum, either x-signature OR x-eip712-signature must be present, and x-timestamp should be present.

throw new RpcError(

Choose a reason for hiding this comment

The 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
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

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

Using METHOD_NOT_FOUND error code for missing authentication headers is semantically incorrect. This error code typically indicates the RPC method doesn't exist. Consider using INVALID_REQUEST or creating a specific authentication error code like UNAUTHORIZED to accurately represent the authentication failure.

Copilot uses AI. Check for mistakes.
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();
Expand Down
11 changes: 7 additions & 4 deletions packages/api/src/modules/eth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Eth } from "@skandha/executor/lib/modules/eth";
import {
EstimatedUserOperationGas,
SDAuthHeaders,
UserOperationByHashResponse,
UserOperationReceipt,
} from "@skandha/types/lib/api/interfaces";
Expand Down Expand Up @@ -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<UserOperationByHashResponse | null> {
return await this.ethModule.getUserOperationByHash(hash);
return await this.ethModule.getUserOperationByHash(hash, sdAuthHeaders);
}

/**
Expand All @@ -64,9 +66,10 @@ export class EthAPI {
* @returns a UserOperation receipt
*/
async getUserOperationReceipt(
hash: string
hash: string,
sdAuthHeaders: SDAuthHeaders,
): Promise<UserOperationReceipt | null> {
return await this.ethModule.getUserOperationReceipt(hash);
return await this.ethModule.getUserOperationReceipt(hash, sdAuthHeaders);
}

/**
Expand Down
50 changes: 28 additions & 22 deletions packages/executor/src/modules/eth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);

Choose a reason for hiding this comment

The 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
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

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

[nitpick] Corrected capitalization: 'auth' should be capitalized as 'Auth' to match the naming convention used elsewhere in the codebase (e.g., 'SDAuthHeaders').

Suggested change
// 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

Copilot uses AI. Check for mistakes.
// 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);
}
Expand All @@ -511,10 +516,11 @@ export class Eth {
* @returns a UserOperation receipt
*/
async getUserOperationReceipt(
hash: string
hash: string,
sdAuthHeaders: SDAuthHeaders,
): Promise<UserOperationReceipt | null> {
const rpcUserOp = await this.entryPointService.getUserOperationReceipt(
hash
hash, sdAuthHeaders,
);
if (!rpcUserOp && this.blockscoutApi) {
return await this.blockscoutApi.getUserOperationReceipt(hash);
Expand Down
11 changes: 7 additions & 4 deletions packages/executor/src/services/EntryPointService/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -39,7 +40,8 @@ export class EntryPointService {
/** View functions */

async getUserOperationByHash(
userOpHash: string
userOpHash: string,
sdAuthHeaders: SDAuthHeaders,
): Promise<UserOperationByHashResponse | null> {
if (!userOpHash) {
throw new RpcError(
Expand All @@ -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 */
Expand All @@ -59,7 +61,8 @@ export class EntryPointService {
}

async getUserOperationReceipt(
userOpHash: string
userOpHash: string,
sdAuthHeaders: SDAuthHeaders,
): Promise<UserOperationReceipt | null> {
if (!userOpHash) {
throw new RpcError(
Expand All @@ -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 */
Expand Down
86 changes: 73 additions & 13 deletions packages/executor/src/services/EntryPointService/versions/0.0.7.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

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

The x-from-block header is extracted and excluded from forwarding, but if it's undefined, headerFromBlock will be undefined. This could lead to unexpected behavior in the subsequent logic. Consider adding explicit undefined handling or documentation explaining the expected behavior when this header is absent.

Copilot uses AI. Check for mistakes.

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]) => {

Choose a reason for hiding this comment

The 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
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

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

[nitpick] This reduce operation to filter undefined values could be simplified using Object.fromEntries with filter: Object.fromEntries(Object.entries(headersToForward).filter(([_, value]) => value !== undefined)). This would improve readability and reduce code complexity.

Suggested change
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 uses AI. Check for mistakes.
},
}),
})

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)}`
Copy link

Copilot AI Nov 12, 2025

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.

Suggested change
`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)}`

Copilot uses AI. Check for mistakes.
);
}
}
} catch (err) {
this.logger.error(err);
Expand All @@ -278,9 +336,10 @@ export class EntryPointV7Service implements IEntryPointService {
}

async getUserOperationReceipt(
hash: Hex
hash: Hex,
sdAuthHeaders: SDAuthHeaders,
): Promise<UserOperationReceipt | null> {
const event = await this.getUserOperationEvent(hash);
const event = await this.getUserOperationEvent(hash, sdAuthHeaders);
if (!event) {
return null;
}
Expand All @@ -300,9 +359,10 @@ export class EntryPointV7Service implements IEntryPointService {
}

async getUserOperationByHash(
hash: Hex
hash: Hex,
sdAuthHeaders: SDAuthHeaders,
): Promise<UserOperationByHashResponse | null> {
const event = await this.getUserOperationEvent(hash);
const event = await this.getUserOperationEvent(hash, sdAuthHeaders);
if (!event) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -36,11 +37,16 @@ export interface IEntryPointService {
simulateValidation(userOp: UserOperation): Promise<any>;

getUserOperationEvent(
userOpHash: string
userOpHash: string,
sdAuthHeaders: SDAuthHeaders,
): Promise<any>;
getUserOperationReceipt(hash: string): Promise<UserOperationReceipt | null>;
getUserOperationReceipt(
hash: string,
sdAuthHeaders: SDAuthHeaders,
): Promise<UserOperationReceipt | null>;
getUserOperationByHash(
hash: string
hash: string,
sdAuthHeaders: SDAuthHeaders,
): Promise<UserOperationByHashResponse | null>;

encodeHandleOps(userOps: UserOperation[], beneficiary: string): Hex;
Expand Down
Loading