Logo
新用户订阅 RPC,首月享 6.5 折优惠查看优惠
OnFinality Learn
集成与开发阅读约 14 分钟

通过 RPC 发送 ERC-4337 UserOperation:Bundler 语义与 eth_sendUserOperation

面向开发者的 ERC-4337 bundler JSON-RPC 接口指南:无需 SDK 即可构建、估算、提交和轮询 UserOperation。

TL;DR

ERC-4337 将智能账户执行移入替代内存池,其中 UserOperation 不是交易,在 bundler 将其打包之前没有交易哈希。Bundler 暴露由 ERC-7769 定义的专用 JSON-RPC 接口,包括 eth_sendUserOperation、eth_estimateUserOperationGas、eth_getUserOperationReceipt、eth_getUserOperationByHash、eth_supportedEntryPoints 和 eth_chainId。客户端构建并签名 UserOperation,使用 eth_estimateUserOperationGas 进行预检,通过 eth_sendUserOperation 提交,然后轮询 eth_getUserOperationReceipt 而非 eth_getTransactionReceipt。验证失败和模拟失败返回不同的 JSON-RPC 错误对象,且 callGasLimit、verificationGasLimit、preVerificationGas 和 paymasterVerificationGasLimit 等 gas 字段必须正确设置,否则在模拟中通过的估算仍可能在链上回滚。本文聚焦 RPC 合约,让你无需 SDK 即可集成任何 bundler,并针对自己的端点测量其行为。

为什么替代内存池改变了 RPC 接口

ERC-4337 引入了一个替代内存池,用户在其中提交 UserOperation 对象而非交易。UserOperation 是包含 sender、nonce、callData、gas 限制和可选 paymaster 字段的结构化意图;它不是已签名的以太坊交易,无法通过 eth_sendRawTransaction 广播。该结构体、EntryPoint 合约和 bundler 角色的权威定义见 ERC-4337:使用替代内存池的账户抽象

由于 UserOperation 不是交易,它在提交时没有交易哈希。相反,它有一个 userOpHash,基于打包的 UserOperation、EntryPoint 地址和链 ID 计算得出。只有当 bundler 打包该操作后,EntryPoint 才会发出包含该哈希的 UserOperationEvent,此时才存在底层交易哈希。这一区别是大多数集成困惑的根源:使用 userOpHash 轮询 eth_getTransactionReceipt 的客户端将始终收到 null。

替代内存池也是 RPC 接口与通用节点分离的原因。标准以太坊节点暴露用于交易和状态的 eth_* 方法;bundler 则暴露额外的 UserOperation 方法集。这两个服务可以独立运行,因此即使你的通用 RPC 端点健康,bundler 端点也可能被限流或不可用。关于标准交易池如何暴露的背景,请参阅 以太坊 txpool 命名空间与替代内存池

  • UserOperation:意图结构体,不是已签名交易。
  • userOpHash:由操作和 EntryPoint 派生的确定性标识符。
  • 交易哈希:仅在打包并发出事件后存在。
  • Bundler 端点:与通用 RPC 端点不同的独立服务。

Bundler JSON-RPC 方法集及各自返回内容

ERC-7769:ERC-4337 的 JSON-RPC API 标准化了 bundler 必须实现的方法合约。核心方法包括 eth_sendUserOperation、eth_estimateUserOperationGas、eth_getUserOperationReceipt、eth_getUserOperationByHash、eth_supportedEntryPoints 和 eth_chainId。每个方法都封装在 JSON-RPC 2.0 规范定义的 JSON-RPC 2.0 信封中,因此请求携带 jsonrpc、method、params 和 id,响应携带 result 或 error。

eth_sendUserOperation 接受 UserOperation 和 EntryPoint 地址,并返回十六进制字符串形式的 userOpHash。eth_estimateUserOperationGas 接受相同参数外加可选的状态覆盖,并返回 callGasLimit、verificationGasLimit、preVerificationGas 的 gas 估算值,当存在 paymaster 时还返回 paymasterVerificationGasLimit。eth_getUserOperationReceipt 接受 userOpHash,并在打包后返回完整收据,包括交易收据、日志和实际使用的 gas。

eth_getUserOperationByHash 在已知的情况下返回操作及其打包上下文,这对于调试尚未打包的提交很有用。eth_supportedEntryPoints 返回 bundler 接受的 EntryPoint 地址,eth_chainId 返回 bundler 所服务的链 ID。提交前务必调用 eth_supportedEntryPoints 和 eth_chainId,因为你的 EntryPoint 版本与 bundler 支持集之间的不匹配是常见且静默的失败。

  • eth_sendUserOperation -> userOpHash(十六进制字符串)。
  • eth_estimateUserOperationGas -> callGasLimit、verificationGasLimit、preVerificationGas、paymasterVerificationGasLimit。
  • eth_getUserOperationReceipt -> 包含交易收据、日志、实际使用 gas 的收据。
  • eth_getUserOperationByHash -> 操作及打包上下文。
  • eth_supportedEntryPoints -> 接受的 EntryPoint 地址。
  • eth_chainId -> bundler 所服务的链 ID。

从构建到收据的客户端调用序列

集成序列有四个阶段:构建并签名、预检、提交和轮询。构建意味着组装 UserOperation 字段、计算 userOpHash 并使用智能账户的签名密钥签名。预检意味着调用 eth_estimateUserOperationGas 获取 gas 限制。提交意味着使用已签名操作调用 eth_sendUserOperation。轮询意味着反复调用 eth_getUserOperationReceipt,直到返回收据或超过超时时间。

轮询阶段是替代内存池与标准交易处理差异最显著的地方。由于 UserOperation 在打包前没有交易哈希,你必须使用 userOpHash 轮询 eth_getUserOperationReceipt,而不是 eth_getTransactionReceipt。适用于标准收据的“未挖出前为 null”语义在这里同样适用,但键控在不同的标识符上;收据轮询的机制在 eth_getTransactionReceipt 返回 null 与收据轮询 中有所介绍。

当 bundler 拒绝提交时,它会返回包含 code、message 和 data 的 JSON-RPC 错误对象。验证失败(EntryPoint 在验证期间拒绝操作)通常表现为 data 中包含 AA 前缀回滚原因的错误。模拟失败(bundler 自身在提交前对操作的模拟失败)可能返回不同的代码或指示模拟失败的消息。确切的代码和消息字符串因 bundler 而异,因此应将错误形状视为需要检查的已记录行为,而非固定合约。

  • 构建并签名:组装字段、计算 userOpHash、签名。
  • 预检:eth_estimateUserOperationGas。
  • 提交:eth_sendUserOperation。
  • 轮询:使用 userOpHash 调用 eth_getUserOperationReceipt。
  • 验证失败与模拟失败:检查错误对象的 code、message 和 data。

Paymaster 数据与决定打包的 gas 字段

UserOperation 携带四个与 gas 相关的字段,必须在提交前设置:callGasLimit、verificationGasLimit、preVerificationGas,以及当 paymaster 赞助操作时的 paymasterVerificationGasLimit。callGasLimit 限制账户 callData 的执行;verificationGasLimit 限制账户和 paymaster 验证;preVerificationGas 覆盖 calldata 和 bundler 开销;paymasterVerificationGasLimit 限制 paymaster 自身的验证。ERC-4337 在 EntryPoint 的验证和执行阶段记录了这些字段及其作用。

Paymaster 数据在 paymasterAndData 字段中传递,其编码是 paymaster 特定的。Paymaster 可能要求签名批准、代币支付或限时赞助,该数据的编码由 paymaster 实现定义,而非 ERC-4337 本身。这是一个已记录的差异点:字段是标准化的,但其内容不是。

在模拟中通过的估算仍可能在链上回滚,因为模拟针对特定状态快照运行。如果账户的 nonce 发生变化、paymaster 的存款耗尽、代币余额变动,或者操作被打包到具有不同 gas 价格的不同区块中,即使估算成功,链上验证也可能失败。将估算视为下限并留出余量,尤其是在 verificationGasLimit 和 preVerificationGas 上。

  • callGasLimit:限制 callData 的执行。
  • verificationGasLimit:限制账户和 paymaster 验证。
  • preVerificationGas:覆盖 calldata 和 bundler 开销。
  • paymasterVerificationGasLimit:限制 paymaster 验证。
  • paymasterAndData 编码:paymaster 特定,因实现而异。

使用纯 fetch 的可运行 Node.js 客户端

以下示例仅使用内置的 fetch API,因此无需 SDK 即可看到 RPC 合约。它首先调用 eth_chainId 和 eth_supportedEntryPoints,然后调用 eth_estimateUserOperationGas,接着调用 eth_sendUserOperation,最后轮询 eth_getUserOperationReceipt。将端点、EntryPoint 地址和 UserOperation 字段替换为你自己的值。签名步骤被省略,因为它取决于你的智能账户实现;在实践中,你在提交前使用账户密钥对 userOpHash 签名。

请注意,该示例将 bundler 端点视为单个 URL。在生产环境中,你可能指向专用 bundler 服务,这是与通用 RPC 端点不同的独立服务。关于端点选择指导,请参阅 以太坊 RPC 端点与提供商选择(RPC Assistant)

const BUNDLER_URL = 'https://your-bundler-endpoint.example';
const ENTRY_POINT = '0x0000000071727De22E5E9d8BAf0edAc6f37da032';

async function rpc(method, params) {
  const res = await fetch(BUNDLER_URL, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
  });
  const json = await res.json();
  if (json.error) throw new Error(JSON.stringify(json.error));
  return json.result;
}

async function main() {
  const chainId = await rpc('eth_chainId', []);
  const entryPoints = await rpc('eth_supportedEntryPoints', []);
  console.log('chainId', chainId, 'entryPoints', entryPoints);

  const userOp = {
    sender: '0xYourSmartAccountAddress',
    nonce: '0x0',
    callData: '0x',
    callGasLimit: '0x0',
    verificationGasLimit: '0x0',
    preVerificationGas: '0x0',
    maxFeePerGas: '0x0',
    maxPriorityFeePerGas: '0x0',
    paymasterAndData: '0x',
    signature: '0x'
  };

  const gas = await rpc('eth_estimateUserOperationGas', [userOp, ENTRY_POINT]);
  console.log('gas estimate', gas);

  const signedOp = { ...userOp, ...gas, signature: '0xYourSignature' };
  const userOpHash = await rpc('eth_sendUserOperation', [signedOp, ENTRY_POINT]);
  console.log('userOpHash', userOpHash);

  for (let i = 0; i < 30; i++) {
    const receipt = await rpc('eth_getUserOperationReceipt', [userOpHash]);
    if (receipt) {
      console.log('included', receipt.receipt.transactionHash);
      return;
    }
    await new Promise(r => setTimeout(r, 4000));
  }
  console.log('not included within timeout');
}

main().catch(err => { console.error(err); process.exit(1); });

针对你的 EntryPoint 测量你的 bundler

由于 bundler 行为因提供商而异,可靠地表征集成的唯一方法是进行测量。针对你自己的端点和 EntryPoint 运行上述客户端,并将结果记录在表格中。下表是模板;用你观察到的值填写,而不是本文中的数字。在没有自行测量的情况下,不要假设任何提供商特定的延迟、吞吐量或速率限制。

至少测量以下内容:返回的链 ID 和支持的 EntryPoint、代表性操作的 gas 估算、从 eth_sendUserOperation 到第一个非 null 的 eth_getUserOperationReceipt 的时间,以及故意无效操作的错误对象形状。对多个操作重复测量以观察方差。如果你运行多个 bundler,对每个运行相同的表格,以便按你自己的标准进行比较。

  • 结果表列:bundler 端点、链 ID、支持的 EntryPoint、估算(callGasLimit / verificationGasLimit / preVerificationGas / paymasterVerificationGasLimit)、提交到收据时间、无效操作的错误代码、无效操作的错误消息、备注。
  • 每行至少运行三次以观察方差。
  • 记录确切的错误对象,而非转述,以便在代码中匹配。
  • 如果需要冗余,与第二个 bundler 进行比较。

失败模式与 AA 前缀回滚原因

EntryPoint 验证失败表现为以 AA 为前缀的回滚原因。常见示例包括 AA21(sender 未支付预付款)、AA22(已过期或未到期)、AA23(验证期间回滚)、AA24(签名错误)、AA25(无效账户 nonce)和 AA31(paymaster 未支付预付款)。这些字符串在 ERC-4337 中有记录,由 EntryPoint 合约发出,因此在使用兼容 EntryPoint 版本的 bundler 之间是一致的。当你看到 AA 前缀原因时,失败发生在验证中,而非 callData 执行中。

Nonce 键管理是并行操作失败的常见来源。ERC-4337 使用 256 位 nonce,其中 192 位为键,64 位为序列,允许每个账户有多个独立的 nonce 流。如果你使用相同的 nonce 键和序列并行提交多个 UserOperation,只有一个能被包含;其他会以 AA25 失败。为独立流使用不同的 nonce 键,并在每个流内递增序列。关于标准交易 nonce 模型,请参阅 使用 eth_getTransactionCount 进行 EVM nonce 管理

返回 userOpHash 但从未打包操作的 bundler 是一种不同的失败模式。操作可能从替代内存池中被丢弃、相对于当前条件定价过低,或等待在 nonce 缺口之后。轮询 eth_getUserOperationByHash 以查看 bundler 是否仍知道该操作,并根据当前条件检查你的 maxFeePerGas 和 maxPriorityFeePerGas。关于费用估算背景,请参阅 使用 eth_feeHistory 估算 gas 价格

链 ID 和 EntryPoint 版本不匹配在提交前是静默的。如果你的客户端针对一条链,但 bundler 服务另一条链,或者你的 EntryPoint 地址不在 eth_supportedEntryPoints 中,bundler 可能拒绝操作或返回未提及不匹配的错误。在构建操作前务必验证 eth_chainId 和 eth_supportedEntryPoints。

  • AA21:sender 未支付预付款。
  • AA22:已过期或未到期。
  • AA23:验证期间回滚。
  • AA24:签名错误。
  • AA25:无效账户 nonce。
  • AA31:paymaster 未支付预付款。
  • Nonce 键:为并行流使用不同的键。
  • userOpHash 但未打包:检查 eth_getUserOperationByHash 和费用字段。
  • 链 ID 和 EntryPoint 不匹配:构建前验证。

区分已记录行为与提供商差异

ERC-4337 和 ERC-7769 定义了 UserOperation 结构体、EntryPoint 合约、替代内存池以及 bundler RPC 接口的方法合约。这些是你可以跨合规实现依赖的已记录行为。JSON-RPC 2.0 规范定义了包含 code、message 和 data 的错误对象信封,bundler 使用它返回验证和模拟失败。

因 bundler 或提供商而异的内容包括:验证和模拟失败的特定错误代码和消息字符串、支持的 EntryPoint 版本、bundler 端点的速率限制和可用性、接受的 paymaster 数据编码,以及定价过低操作的打包策略。将这些视为提供商特定,并针对你自己的端点进行测量,而不是假设固定合约。这就是为什么上面的结果表是模板而非一组预期值。

Bundler 端点是与通用 RPC 端点不同的独立服务。它可能独立于你的标准 RPC 提供商而被限流或不可用,并且可能服务不同的链集。如果你需要两者,请规划两个端点和两个故障域。关于通用以太坊端点选项,请参阅 以太坊 RPC 端点与提供商选择(RPC Assistant)以太坊网络页面

  • 已记录:UserOperation 结构体、EntryPoint、替代内存池、方法合约、JSON-RPC 错误信封。
  • 因 bundler 而异:错误代码和消息、支持的 EntryPoint、速率限制、paymaster 编码、打包策略。
  • Bundler 端点和通用 RPC 端点是具有独立故障域的独立服务。

Bundler RPC 模型的局限与权衡

Bundler RPC 模型在你的客户端和链之间增加了一个服务依赖。UserOperation 只有在 bundler 选择打包时才会被包含,因此仅提交并不能保证打包。这是替代内存池的有意权衡:它支持赞助和批处理,但也意味着你的客户端必须处理没有交易哈希且没有标准替换语义的待处理状态。

Gas 估算是建议性的。在模拟中通过的估算仍可能在链上回滚,因为模拟和打包之间状态会变化。Paymaster 赞助增加了另一个依赖:如果 paymaster 的存款耗尽或其策略改变,即使你的账户有资金,操作也会验证失败。Nonce 键管理为并行操作增加了复杂性,AA 前缀回滚原因要求你将错误字符串映射到补救步骤。

在操作上,你应该将 bundler 端点视为独立的可用性域。它可能独立于你的通用 RPC 端点而被限流或不可用,并且其支持的 EntryPoint 集可能变化。如果你需要冗余,对多个 bundler 运行结果表,并根据测量行为而非假设进行路由。关于相关可靠性模式,请参阅 JSON-RPC 幂等性与重复请求安全

  • 提交不保证打包。
  • 估算是建议性的,可能与链上执行有差异。
  • Paymaster 赞助增加了第二个依赖。
  • Nonce 键管理为并行操作增加了复杂性。
  • Bundler 端点是独立的可用性域。

将智能账户与 bundler 集成的后续步骤

首先针对你选择的 bundler 调用 eth_chainId 和 eth_supportedEntryPoints,然后使用最小操作运行上面的 Node.js 客户端并填写结果表。一旦有了基线,添加 paymaster 赞助并测量 gas 字段如何变化。然后故意测试失败路径:提交带有错误签名的操作以观察 AA24 错误形状,并提交两个具有相同 nonce 键的操作以观察 AA25。

对于生产环境,将客户端包装在重试和超时逻辑中,并使用有界超时轮询 eth_getUserOperationReceipt,而不是无限期轮询。如果可用性重要,保留备用 bundler 端点,并定期重新运行结果表,因为提供商行为可能变化。如果你需要在 bundler 之外获得通用以太坊 RPC 访问,请查看 RPC 定价API 服务 来规划你的端点拓扑。

关于以太坊 RPC 集成模式的更广泛背景,OnFinality Learn 中心 收集了关于收据、nonce、费用估算和幂等性的相关指南。将这些与本文一起使用,以构建一个完整的客户端,同时处理标准交易路径和 ERC-4337 替代内存池路径。

  • 首先验证链 ID 和支持的 EntryPoint。
  • 运行客户端并填写结果表。
  • 添加 paymaster 赞助并重新测量 gas 字段。
  • 故意测试 AA24 和 AA25 失败路径。
  • 如果需要,添加重试、超时和备用 bundler。

永远不用担心基础设施

OnFinality 消除了 DevOps 的繁重工作,让您能够更聪明、更快地构建。

开始