Skip to content
Merged
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: 34 additions & 2 deletions src/Bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type BundlerErrorCode,
BundlerErrorCodeDict,
ensureError,
type JsonRpcErrorCode,
parseAaCode,
} from "./errors";
import {
Expand Down Expand Up @@ -250,8 +251,31 @@ export class Bundler implements Transport {
params: [useroperationhash],
});
if (jsonRpcResult == null) return null;
// the wire format carries hex strings; convert the numeric fields
// to the bigints the declared type promises
const wireOp = jsonRpcResult.userOperation as unknown as Record<string, unknown>;
const userOperation = { ...wireOp };
for (const field of [
"nonce",
"callGasLimit",
"verificationGasLimit",
"preVerificationGas",
"maxFeePerGas",
"maxPriorityFeePerGas",
"paymasterVerificationGasLimit",
"paymasterPostOpGasLimit",
]) {
if (wireOp[field] != null) {
userOperation[field] = BigInt(wireOp[field] as string | bigint);
}
}
return {
...jsonRpcResult,
userOperation: userOperation as unknown as
| UserOperationV6
| UserOperationV7
| UserOperationV8
| UserOperationV9,
blockNumber: jsonRpcResult.blockNumber == null ? null : BigInt(jsonRpcResult.blockNumber),
};
} catch (err) {
Expand All @@ -267,7 +291,9 @@ export class Bundler implements Transport {
*
* - `AbstractionKitError` passes through unchanged (already domain-translated).
* - {@link ProviderRpcError} with a known 4337 code → inner code from
* {@link BundlerErrorCodeDict}.
* {@link BundlerErrorCodeDict}; -32601 is the standard JSON-RPC
* METHOD_NOT_FOUND (neither ERC-7769 nor bundler implementations assign
* it any 4337-specific meaning — an invalid hash arrives as -32602).
* - Anything else → inner `UNKNOWN_ERROR`.
*
* @internal
Expand Down Expand Up @@ -295,8 +321,14 @@ function translateBundlerError(
}
const code = (err as ProviderRpcError | undefined)?.code;
const codeString = code != null ? String(code) : "";
const innerCode: BundlerErrorCode | BasicErrorCode =
let innerCode: BundlerErrorCode | BasicErrorCode | JsonRpcErrorCode =
codeString in BundlerErrorCodeDict ? BundlerErrorCodeDict[codeString] : "UNKNOWN_ERROR";
// Standard JSON-RPC codes without 4337 semantics keep their standard names.
if (codeString === "-32601") {
innerCode = "METHOD_NOT_FOUND";
} else if (codeString === "-32603") {
innerCode = "INTERNAL_ERROR";
}
const error = ensureError(err);
// The EntryPoint AAxx code lives only inside the message text. Parse it once
// here so callers can branch on a stable code instead of matching the message.
Expand Down
6 changes: 5 additions & 1 deletion src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export type BundlerErrorCode =
| "INSUFFICIENT_STAKE"
| "UNSUPPORTED_SIGNATURE_AGGREGATOR"
| "INVALID_SIGNATURE"
| "PAYMASTER_DEPOSIT_TOO_LOW"
/** @deprecated no longer produced: -32601 was mismapped to this code; an
* invalid hash surfaces as INVALID_FIELDS (-32602) per the bundler spec
* tests and Voltaire, while -32601 is the standard METHOD_NOT_FOUND. */
| "INVALID_USEROPERATION_HASH"
| "EXECUTION_REVERTED";

Expand Down Expand Up @@ -65,7 +69,7 @@ export const BundlerErrorCodeDict: Dictionary<BundlerErrorCode> = {
"-32505": "INSUFFICIENT_STAKE",
"-32506": "UNSUPPORTED_SIGNATURE_AGGREGATOR",
"-32507": "INVALID_SIGNATURE",
"-32601": "INVALID_USEROPERATION_HASH",
"-32508": "PAYMASTER_DEPOSIT_TOO_LOW",
"-32521": "EXECUTION_REVERTED",
};

Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export type GasEstimationResult = {
/** Result of eth_getUserOperationByHash. Null if not found. */
export type UserOperationByHashResult = {
/** The UserOperation object */
userOperation: UserOperationV6 | UserOperationV7;
userOperation: UserOperationV6 | UserOperationV7 | UserOperationV8 | UserOperationV9;
/** The EntryPoint address */
entryPoint: string;
/** Block number (null if pending) */
Expand Down
52 changes: 48 additions & 4 deletions test/transport/Bundler.error-mapping.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,59 @@ describe("Bundler error mapping (moved from sendJsonRpcRequest)", () => {
}
});

test("-32601 → outer BUNDLER_ERROR, inner INVALID_USEROPERATION_HASH (Bundler-specific override of the standard JSON-RPC meaning)", async () => {
const t = mockTransportThatThrows(rpcError(-32601, "not found"));
test("-32601 → inner METHOD_NOT_FOUND (standard JSON-RPC meaning; an invalid userOpHash arrives as -32602 per bundler behavior, e.g. Voltaire)", async () => {
const t = mockTransportThatThrows(rpcError(-32601, "method not found"));
for (const call of [
(b) => b.getUserOperationReceipt("0xabc"),
(b) => b.chainId(),
]) {
const b = new ak.Bundler(t);
try {
await call(b);
throw new Error("expected throw");
} catch (err) {
expect(err.code).toBe("BUNDLER_ERROR");
expect(err.cause.code).toBe("METHOD_NOT_FOUND");
}
}
});

test("-32508 → inner PAYMASTER_DEPOSIT_TOO_LOW (ERC-7769; raised by Voltaire's mempool)", async () => {
const t = mockTransportThatThrows(
rpcError(-32508, "paymaster deposit too low for all mempool UserOperations"),
);
const b = new ak.Bundler(t);
try {
await b.chainId();
throw new Error("expected throw");
} catch (err) {
expect(err.code).toBe("BUNDLER_ERROR");
expect(err.cause.code).toBe("PAYMASTER_DEPOSIT_TOO_LOW");
expect(err.cause.errno).toBe(-32508);
}
});

test("-32603 → inner INTERNAL_ERROR (standard JSON-RPC meaning; bundler internal failure)", async () => {
const t = mockTransportThatThrows(rpcError(-32603, "unexpected internal error"));
const b = new ak.Bundler(t);
try {
await b.chainId();
throw new Error("expected throw");
} catch (err) {
expect(err.code).toBe("BUNDLER_ERROR");
expect(err.cause.code).toBe("INTERNAL_ERROR");
}
});

test("-32602 → inner INVALID_FIELDS (how an invalid userOpHash actually surfaces)", async () => {
const t = mockTransportThatThrows(rpcError(-32602, "Missing/invalid userOpHash"));
const b = new ak.Bundler(t);
try {
await b.getUserOperationReceipt("0xabc");
await b.getUserOperationReceipt("0xnothex");
throw new Error("expected throw");
} catch (err) {
expect(err.code).toBe("BUNDLER_ERROR");
expect(err.cause.code).toBe("INVALID_USEROPERATION_HASH");
expect(err.cause.code).toBe("INVALID_FIELDS");
}
});

Expand Down
70 changes: 70 additions & 0 deletions test/transport/Bundler.getUserOperationByHash.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// getUserOperationByHash must convert the wire-format hex strings of the
// returned userOperation into the bigints its declared type promises,
// like getUserOperationReceipt and estimateUserOperationGas already do.

const ak = require("../../dist/index.cjs");

const WIRE_V7_OP = {
sender: "0x1a02592A3484c2077d2E5D24482497F85e1980C6",
nonce: "0x0",
callData: "0x",
callGasLimit: "0x5208",
verificationGasLimit: "0x186a0",
preVerificationGas: "0xb3b0",
maxFeePerGas: "0x3b9aca00",
maxPriorityFeePerGas: "0x3b9aca00",
paymaster: "0x084f4dB6bae8fBb7fb9709c0A25532E21C7A097E",
paymasterVerificationGasLimit: "0x30d40",
paymasterPostOpGasLimit: "0xafc8",
paymasterData: "0x",
signature: "0x",
};

describe("Bundler.getUserOperationByHash", () => {
function mockTransport(result) {
return { request: async () => result };
}

test("converts numeric userOperation fields to bigint", async () => {
const b = new ak.Bundler(
mockTransport({
userOperation: WIRE_V7_OP,
entryPoint: "0x0000000071727De22E5E9d8BAf0edAc6f37da032",
blockNumber: "0x10",
blockHash: "0x" + "ab".repeat(32),
transactionHash: "0x" + "cd".repeat(32),
}),
);
const res = await b.getUserOperationByHash("0x" + "11".repeat(32));
expect(res.blockNumber).toBe(16n);
expect(res.userOperation.nonce).toBe(0n);
expect(res.userOperation.callGasLimit).toBe(0x5208n);
expect(res.userOperation.verificationGasLimit).toBe(0x186a0n);
expect(res.userOperation.preVerificationGas).toBe(0xb3b0n);
expect(res.userOperation.maxFeePerGas).toBe(1000000000n);
expect(res.userOperation.maxPriorityFeePerGas).toBe(1000000000n);
expect(res.userOperation.paymasterVerificationGasLimit).toBe(0x30d40n);
expect(res.userOperation.paymasterPostOpGasLimit).toBe(0xafc8n);
// non-numeric fields untouched
expect(res.userOperation.sender).toBe(WIRE_V7_OP.sender);
expect(res.userOperation.signature).toBe("0x");
});

test("returns null for an unknown hash and keeps pending blockNumber null", async () => {
const bNull = new ak.Bundler(mockTransport(null));
expect(await bNull.getUserOperationByHash("0x" + "22".repeat(32))).toBeNull();

const bPending = new ak.Bundler(
mockTransport({
userOperation: WIRE_V7_OP,
entryPoint: "0x0000000071727De22E5E9d8BAf0edAc6f37da032",
blockNumber: null,
blockHash: null,
transactionHash: null,
}),
);
const res = await bPending.getUserOperationByHash("0x" + "33".repeat(32));
expect(res.blockNumber).toBeNull();
expect(res.userOperation.nonce).toBe(0n);
});
});
Loading