跳到主要内容

Bundler RPC

BotChain Bundler 接收 RPC UserOperation、执行模拟、维护 UserOperation 队列,并由 Relayer 通过普通交易调用 EntryPoint。普通 BotChain 链 RPC 不提供 eth_sendUserOperation。

UserOperation 内存池流程

1. RPC UserOperation v0.7​

BotChain 使用 EntryPoint v0.7,不兼容 v0.6 字段格式。

1.1 Bundler RPC 调用结构​

{
"sender": "0x...",
"nonce": "0x...",
"factory": "0x...",
"factoryData": "0x...",
"callData": "0x...",
"callGasLimit": "0x...",
"verificationGasLimit": "0x...",
"preVerificationGas": "0x...",
"maxFeePerGas": "0x...",
"maxPriorityFeePerGas": "0x...",
"paymaster": "0x...",
"paymasterVerificationGasLimit": "0x...",
"paymasterPostOpGasLimit": "0x...",
"paymasterData": "0x...",
"signature": "0x..."
}

1.2 字段表​

字段类型Gas 估算最终发送说明
senderaddress(20 字节、0x 前缀十六进制地址)必填必填智能账户地址,包括尚未部署的反事实地址
nonceQUANTITY(无多余前导零的 0x 前缀十六进制整数;零为 0x0)必填必填EntryPoint 管理的账户 nonce
factoryaddress条件字段条件字段首次部署账户使用的 Factory
factoryDataDATA(偶数长度的 0x 前缀十六进制 bytes;空值为 0x)条件字段条件字段Factory 创建账户的调用数据
callDataDATA必填必填发送给智能账户的业务执行数据
callGasLimitQUANTITY可省略或使用临时值必填账户业务执行 Gas 上限
verificationGasLimitQUANTITY可省略或使用临时值必填Factory 和账户验证 Gas 上限
preVerificationGasQUANTITY可省略或使用临时值必填calldata 与 bundle 固定开销
maxFeePerGasQUANTITY可选必填最高总 Gas 单价
maxPriorityFeePerGasQUANTITY可选必填最高优先费
paymasteraddress条件字段条件字段支付 Gas 的 Paymaster
paymasterVerificationGasLimitQUANTITY条件字段条件字段Paymaster 验证 Gas 上限
paymasterPostOpGasLimitQUANTITY条件字段条件字段Paymaster postOp Gas 上限
paymasterDataDATA可选可选Paymaster 自定义授权或策略数据
signatureDATA必填,可用占位值必填,真实授权由 sender 账户解释和验证

2. UserOperation 构造​

以下 TypeScript 片段可以按顺序拼接执行,示例使用 BotChain Testnet、SimpleAccount 和无 Paymaster 路径。先安装 viem:

npm install viem

公共配置和 RPC 辅助函数:

import {
createPublicClient,
encodeFunctionData,
http,
parseAbi,
toHex,
type Hex,
} from "viem";
import { getUserOperationHash } from "viem/account-abstraction";
import { privateKeyToAccount } from "viem/accounts";

const CHAIN_ID = 968;
const RPC_URL = "https://rpc.bohr.life";
const BUNDLER_URL = "https://bundler.bohr.life/rpc/";
const ENTRY_POINT = "0x0000000071727De22E5E9d8BAf0edAc6f37da032";
const FACTORY = "0xBC88d6012b3bf8426C2851d3798cEB5257658332";

const ownerPrivateKey = process.env.OWNER_PRIVATE_KEY;
if (!ownerPrivateKey) throw new Error("OWNER_PRIVATE_KEY is required");

const owner = privateKeyToAccount(ownerPrivateKey as Hex);
const publicClient = createPublicClient({ transport: http(RPC_URL) });

const factoryAbi = parseAbi([
"function getAddress(address owner, uint256 salt) view returns (address)",
"function createAccount(address owner, uint256 salt) returns (address)",
]);
const entryPointAbi = parseAbi([
"function getNonce(address sender, uint192 key) view returns (uint256)",
"function balanceOf(address account) view returns (uint256)",
]);
const simpleAccountAbi = parseAbi([
"function execute(address dest, uint256 value, bytes func)",
]);

async function bundlerRpc<T>(
method: string,
params: unknown[] = [],
): Promise<T> {
const response = await fetch(BUNDLER_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method,
params,
}),
});

const payload = (await response.json()) as {
result?: T;
error?: {
code: number;
message: string;
data?: unknown;
};
};

if (!response.ok || payload.error) {
const errorMessage = payload.error
? `${payload.error.code}: ${payload.error.message}`
: `HTTP ${response.status}`;

throw new Error(errorMessage);
}

if (payload.result === undefined) {
throw new Error(`Bundler returned no result for ${method}`);
}

return payload.result;
}

示例通过环境变量读取 Owner 私钥,仅适合服务端或本地测试。浏览器应用应改用钱包提供的签名账户,不要把私钥写入前端代码。

2.1 确定 sender​

sender 是智能账户地址,不是 Owner EOA。

  • SimpleAccount 使用 SimpleAccountFactory.getAddress(owner, salt) 计算地址。

  • 自定义账户按自己的 Factory 或部署规则确定地址。

  • 通过链 RPC eth_getCode 判断 sender 是否已经部署。

const salt = 0n;

const sender = await publicClient.readContract({
address: FACTORY,
abi: factoryAbi,
functionName: "getAddress",
args: [owner.address, salt],
});

const accountCode = await publicClient.getCode({ address: sender });
const accountDeployed = accountCode !== undefined && accountCode !== "0x";

2.2 决定 Factory 字段​

账户状态factoryfactoryData
尚未部署提供 Factory 地址提供创建账户的 ABI 调用数据
已经部署省略省略

BotChain SimpleAccount 的 factoryData 对应 createAccount(owner, salt)。已部署账户继续携带 Factory 字段会触发 AA10 sender already constructed。

const factoryFields = accountDeployed
? {}
: {
factory: FACTORY,
factoryData: encodeFunctionData({
abi: factoryAbi,
functionName: "createAccount",
args: [owner.address, salt],
}),
};

2.3 读取 nonce​

nonce 来自 EntryPoint 的 getNonce(sender, key):

高 192 位:nonce key
低 64 位:该 key 的 sequence

BotChain SimpleAccount 默认使用 key 0。不要使用 Owner EOA 的交易 nonce、智能账户的 eth_getTransactionCount 或未与 EntryPoint 同步的本地计数器。

const nonce = await publicClient.readContract({
address: ENTRY_POINT,
abi: entryPointAbi,
functionName: "getNonce",
args: [sender, 0n],
});

2.4 生成 callData​

callData 由智能账户实现定义:

  • BotChain SimpleAccount 使用 execute 或 executeBatch。

  • 自定义账户使用自己公开的执行接口。

  • callData 必须符合账户在 validateUserOp 中实施的授权策略。

// 这里使用无金额、无 calldata 的调用演示编码过程。
// 实际接入时替换为业务合约地址、转账金额和业务 calldata。
const target = owner.address;
const targetValue = 0n;
const targetCallData = "0x";

const callData = encodeFunctionData({
abi: simpleAccountAbi,
functionName: "execute",
args: [target, targetValue, targetCallData],
});

2.5 决定 Gas 支付方​

场景字段与资金要求
不使用 Paymaster省略全部 Paymaster 字段,并确保账户 deposit 足够
使用 Paymaster提供 Paymaster 地址、两个 Gas 上限和实现要求的数据

未使用 Paymaster 时不要填写零地址。BotChain 没有公共 Paymaster,但允许开发者部署自己的实现。

以下示例不使用 Paymaster,因此不添加任何 Paymaster 字段,并在估算前检查 EntryPoint deposit 和账户原生币余额:

const [entryPointDeposit, senderBalance] = await Promise.all([
publicClient.readContract({
address: ENTRY_POINT,
abi: entryPointAbi,
functionName: "balanceOf",
args: [sender],
}),
publicClient.getBalance({ address: sender }),
]);

if (entryPointDeposit === 0n && senderBalance === 0n) {
throw new Error(
"Fund the sender or deposit funds for it in EntryPoint before estimating",
);
}

2.6 构造待估算 UserOperation​

该对象作为 eth_estimateUserOperationGas(userOp, entryPoint) 的 userOp 参数,仅用于 Bundler 模拟和 Gas 估算,不会发送交易。

字段组估算阶段
sender、nonce、callData使用最终值
factory、factoryData按账户部署状态提供或省略
paymaster 字段按赞助模式提供或省略
三个 Gas 上限可省略、使用零值或临时值
Gas 价格可选;Bundler 会返回建议值
signature使用格式和长度适合账户验证的占位值

当前 Skandha 接受空 DATA 0x 并在估算时替换为内部占位值。需要显式提供与真实签名相同长度时,BotChain SimpleAccount 可使用以下 65 字节占位签名:

0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c

占位签名只用于 Gas 估算,不能用于最终发送。

const DUMMY_SIGNATURE =
"0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c";

const gasPrice = await bundlerRpc<{
maxFeePerGas: Hex;
maxPriorityFeePerGas: Hex;
}>("skandha_getGasPrice");

const estimateUserOperation = {
sender,
nonce: toHex(nonce),
...factoryFields,
callData,
callGasLimit: "0x0",
verificationGasLimit: "0x0",
preVerificationGas: "0x0",
maxFeePerGas: gasPrice.maxFeePerGas,
maxPriorityFeePerGas: gasPrice.maxPriorityFeePerGas,
signature: DUMMY_SIGNATURE,
};

2.7 写回 Gas 估算​

eth_estimateUserOperationGas 返回后,将以下值写入最终 UserOperation:

  • callGasLimit

  • verificationGasLimit

  • preVerificationGas

  • maxFeePerGas

  • maxPriorityFeePerGas

  • 使用 Paymaster 时的两个 Paymaster Gas 上限

写回后,除 signature 外的最终字段应保持不变。

type UserOperationGasEstimate = {
callGasLimit: Hex;
verificationGasLimit?: Hex;
verificationGas?: Hex;
preVerificationGas: Hex;
maxFeePerGas?: Hex;
maxPriorityFeePerGas?: Hex;
};

const gasEstimate = await bundlerRpc<UserOperationGasEstimate>(
"eth_estimateUserOperationGas",
[estimateUserOperation, ENTRY_POINT],
);

const verificationGasLimit =
gasEstimate.verificationGasLimit ?? gasEstimate.verificationGas;
if (!verificationGasLimit) {
throw new Error("Bundler did not return verificationGasLimit");
}

const finalUserOperation = {
...estimateUserOperation,
callGasLimit: gasEstimate.callGasLimit,
verificationGasLimit,
preVerificationGas: gasEstimate.preVerificationGas,
maxFeePerGas: gasEstimate.maxFeePerGas ?? gasPrice.maxFeePerGas,
maxPriorityFeePerGas:
gasEstimate.maxPriorityFeePerGas ?? gasPrice.maxPriorityFeePerGas,
};

2.8 计算 userOpHash​

传入的是 Bundler RPC 形态的未打包 UserOperation,但 QUANTITY 字段需要先转换为 bigint:

const sdkUserOperation = {
...finalUserOperation,
nonce: BigInt(finalUserOperation.nonce),
callGasLimit: BigInt(finalUserOperation.callGasLimit),
verificationGasLimit: BigInt(finalUserOperation.verificationGasLimit),
preVerificationGas: BigInt(finalUserOperation.preVerificationGas),
maxFeePerGas: BigInt(finalUserOperation.maxFeePerGas),
maxPriorityFeePerGas: BigInt(finalUserOperation.maxPriorityFeePerGas),
};

const userOpHash = getUserOperationHash({
userOperation: sdkUserOperation,
entryPointAddress: ENTRY_POINT,
entryPointVersion: "0.7",
chainId: CHAIN_ID,
});

该函数是纯链下计算。entryPointAddress、entryPointVersion 和 chainId 都参与最终哈希,必须与目标网络一致。signature 不参与哈希,因此此时仍可保留占位签名。

2.9 获取最终 signature​

通过账户钱包、SDK 或签名服务对 userOpHash 生成最终 signature。

  • BotChain SimpleAccount 要求 Owner 对 UserOperation hash 进行 EIP-191 ECDSA 签名。

  • 自定义账户可以使用多签、Session Key、聚合签名或其他验证方案。

真实签名必须与 sender.validateUserOp 的实现一致。签名后不得修改任何参与 hash 的字段。

const signature = await owner.signMessage({
message: { raw: userOpHash },
});

const signedUserOperation = {
...finalUserOperation,
signature,
};

2.10 发送并查询结果​

最终 RPC UserOperation 交给 eth_sendUserOperation。Bundler 返回 userOpHash,应用再通过查询接口跟踪打包交易和执行 receipt。

const submittedUserOpHash = await bundlerRpc<Hex>(
"eth_sendUserOperation",
[signedUserOperation, ENTRY_POINT],
);

if (submittedUserOpHash.toLowerCase() !== userOpHash.toLowerCase()) {
throw new Error("Bundler returned an unexpected userOpHash");
}

type UserOperationReceipt = {
success: boolean;
receipt: { transactionHash: Hex };
};

let userOperationReceipt: UserOperationReceipt | null = null;
for (let attempt = 0; attempt < 60 && !userOperationReceipt; attempt++) {
userOperationReceipt = await bundlerRpc<UserOperationReceipt | null>(
"eth_getUserOperationReceipt",
[submittedUserOpHash],
);
if (!userOperationReceipt) {
await new Promise((resolve) => setTimeout(resolve, 2_000));
}
}

if (!userOperationReceipt) {
throw new Error("Timed out waiting for UserOperation receipt");
}

console.log({
userOpHash: submittedUserOpHash,
transactionHash: userOperationReceipt.receipt.transactionHash,
success: userOperationReceipt.success,
});

3. Bundler RPC 方法​

3.1 eth_supportedEntryPoints​

返回 Bundler 接受的 EntryPoint 地址数组。结果应包含部署信息中配置的 EntryPoint。应用还应确认 Bundler eth_chainId 与链 RPC 一致。

3.2 eth_estimateUserOperationGas​

3.2.1 参数​

位置参数类型说明
1userOpEstimate UserOperation待模拟的 RPC UserOperation
2entryPointaddressBundler 支持的 EntryPoint
3stateOverridesobject,可选仅用于模拟的账户状态覆盖

状态覆盖按地址索引,支持 balance、nonce、code、state 和 stateDiff,只影响本次模拟。

3.2.2 返回​

字段类型说明
callGasLimitQUANTITY账户业务执行 Gas 上限
verificationGasLimitQUANTITYFactory 和账户验证 Gas 上限
verificationGasQUANTITYSkandha 返回的验证 Gas 别名
preVerificationGasQUANTITYcalldata 与 bundle 固定开销
maxFeePerGasQUANTITY建议最高总 Gas 单价
maxPriorityFeePerGasQUANTITY建议最高优先费
paymasterVerificationGasLimitQUANTITY,可选Paymaster 验证 Gas 上限
paymasterPostOpGasLimitQUANTITY,可选Paymaster postOp Gas 上限
validAfterQUANTITY,可选验证有效期开始时间
validUntilQUANTITY,可选验证有效期结束时间

3.3 eth_sendUserOperation​

参数为最终 RPC UserOperation 和 EntryPoint 地址。返回 bytes32 userOpHash,不是外层 handleOps 交易 hash。

Bundler 接受 UserOperation 只表示进入待打包流程,不表示已经上链或执行成功。

3.4 eth_getUserOperationByHash​

查不到时返回 null,否则返回:

字段说明
userOperation原始 RPC UserOperation
entryPoint执行入口
blockNumber已打包区块号,可选
blockHash已打包区块 hash,可选
transactionHash外层 handleOps 交易 hash,可选

UserOperation 仍在 mempool 时,可能没有交易和区块字段。

3.5 eth_getUserOperationReceipt​

未上链时返回 null。上链后的主要字段为:

字段说明
userOpHashUserOperation hash
sender智能账户
nonceUserOperation nonce
paymasterGas 支付 Paymaster,可选
actualGasCost实际 Gas 成本
actualGasUsed实际 Gas 用量
success账户业务执行是否成功
reason失败原因,可选
logsUserOperation 相关日志
receipt外层 handleOps 交易回执

最终业务结果以 success 为准。外层交易成功不代表其中每个 UserOperation 都成功。

3.6 skandha_getGasPrice​

该扩展方法不需要参数,返回 maxFeePerGas 和 maxPriorityFeePerGas。若需兼容其他 Bundler,回退策略仍必须生成这两个字段;链 RPC 的 eth_gasPrice 只能作为定价输入,不能被误认为完整的 EIP-1559 双字段报价。即使链上 base fee 为零,也不要将 UserOperation Gas 价格设置为零。

4. 常见问题​

现象检查项
Invalid EntrypointBundler 支持列表、网络和 EntryPoint 地址
参数校验失败QUANTITY、DATA、地址长度和条件字段
AA10已部署账户仍携带 Factory 字段
AA21账户 deposit 或 prefund 不足
AA24账户签名、hash、chainId 或 EntryPoint 不一致
AA25nonce 已使用或不符合账户策略
AA31 / AA33Paymaster deposit 或验证失败
长期没有 receiptmempool 状态、Gas 报价、nonce 和 Relayer 状态
receipt success: false解码 UserOperationRevertReason 并检查业务调用