Gate Chain 账户抽象 Bundler API 集成指南
本文档介绍如何使用 ERC-4337 账户抽象标准,向 Gate Chain Bundler 提交 UserOperation。
核心目标:构造
UserOperation,获取 Gas 估算,完成签名,并通过 Gate Chain Bundler 将其提交上链。
1. 网络参数
Gate Chain 主网
| 参数 | 值 |
|---|---|
| Chain ID | 86 |
| RPC(HTTP,推荐) | https://evm.nodeinfo.cc |
| RPC(HTTP,备用) | https://evm-1.nodeinfo.cc / https://evm.gatenode.cc |
| RPC(WSS) | wss://evm-ws.gatenode.cc |
| Bundler | https://gatechain-bundler.gatenode.cc |
| EntryPoint v0.8 | 通过 eth_supportedEntryPoints 查询(标准地址:0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108) |
| 区块浏览器 | https://www.gatescan.org/gatechain |
| Gas 代币 | GT |
| AA 标准 | ERC-4337 v0.7 |
Gate Chain 测试网(Meteora)
| 参数 | 值 |
|---|---|
| Chain ID | 85 |
| RPC(HTTP) | https://meteora-evm.gatenode.cc |
| RPC(WSS) | wss://meteora-ws.gatenode.cc |
| Bundler | https://gatechain-meteora-bundler.gatenode.cc |
| 区块浏览器 | https://www.gatescan.org/gatechain-testnet |
2. 关于 Gate Chain
Gate Chain 是一条兼容 EVM 的 Layer 1 区块链。
Gate Chain 作为 Gate Layer L2 的结算层。两个网络使用相同的 AA 集成方式,但在网络参数和 Gas 费用模型上有所不同——详见第 4 节。
3. ERC-4337 概念
ERC-4337 允许智能合约账户在无需 EOA 的情况下发起交易。用户不再提交原始交易,而是向 Bundler 提交 UserOperation 对象。
标准执行流程:
- 用户或 dApp 构造并签名一个
UserOperation - Bundler 从替代内存池(alt-mempool)收集
UserOperation - Bundler 将它们打包成一笔交易
- 该交易被发送至 EntryPoint 合约
- EntryPoint 验证并执行每个操作
Paymaster 为可选项。 当 paymaster 为空时,智能账户从其自身的 EntryPoint 存款中支付 Gas(需提前通过 depositTo() 充值)。当 paymaster 有值时,Paymaster 合约代替用户赞助 Gas。
4. Gas 费用机制
Gate Chain 是 Layer 1 网络,使用标准 EIP-1559 Gas 模型。
每笔交易的费用计算公式为:
totalFee = gasUsed × (baseFee + priorityFee)
| 组成部分 | 说明 |
|---|---|
baseFee | 网络基础费,每个区块动态调整(EIP-1559) |
priorityFee | 给验证者的可选小费,可为零或极小值 |
对于 UserOperation,preVerificationGas 主要覆盖 Bundler 的 calldata 开销,callGasLimit 和 verificationGasLimit 基于实际的链上执行成本。请始终使用 eth_estimateUserOperationGas 获取准确值,而非手动估算。
5. EntryPoint 合约地址
查询 Bundler 以获取当前活跃的 EntryPoint 地址:
curl -s "<GATECHAIN_BUNDLER_ENDPOINT>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_supportedEntryPoints",
"params": []
}'
返回示例:
{
"jsonrpc": "2.0",
"id": 1,
"result": ["0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108"]
}
始终在运行时通过
eth_supportedEntryPoints动态获取 EntryPoint 地址,不要硬编码。不同版本的地址不同,硬编码会导致静默验证失败。
6. UserOperation 字段说明(v0.7)
ERC-4337 v0.7 将 initCode 和 paymasterAndData 拆分为独立字段。
| 字段 | 类型 | 说明 |
|---|---|---|
sender | address | 智能账户地址 |
nonce | uint256 | 从 EntryPoint.getNonce(sender, key) 获取 |
factory | address | 账户工厂地址(仅首次部署时填写,已部署则省略) |
factoryData | bytes | 工厂调用数据(仅首次部署时填写,已部署则设为 0x) |
callData | bytes | 在智能账户上执行的调用编码数据 |
callGasLimit | uint256 | 执行阶段的 Gas 上限 |
verificationGasLimit | uint256 | 验证阶段的 Gas 上限 |
preVerificationGas | uint256 | 覆盖 Bundler 打包成本的开销 Gas |
maxFeePerGas | uint256 | 最高 Gas 价格(baseFee + priorityFee) |
maxPriorityFeePerGas | uint256 | 给验证者的最高小费 |
paymaster | address | Paymaster 合约地址;自付时留空 |
paymasterVerificationGasLimit | uint256 | Paymaster 验证阶段的 Gas(无 Paymaster 时设为 0x0) |
paymasterPostOpGasLimit | uint256 | Paymaster 后处理阶段的 Gas(无 Paymaster 时设为 0x0) |
paymasterData | bytes | Paymaster 专属数据(无 Paymaster 时设为 0x) |
signature | bytes | 账户所有者对 UserOperation 哈希的签名 |
eip7702Auth | object | (可选) EIP-7702 授权元组——详见第 10 节 |
7. 集成流程
第 1 步 — 查询 EntryPoint
curl -s "<GATECHAIN_BUNDLER_ENDPOINT>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_supportedEntryPoints","params":[]}'
第 2 步 — 获取优先费
curl -s "<GATECHAIN_NODE_RPC>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"eth_maxPriorityFeePerGas","params":[]}'
将返回值作为 maxPriorityFeePerGas,加上当前 baseFee 即得 maxFeePerGas。
通过 Gate Chain RPC 获取当前 baseFee:
curl -s "<GATECHAIN_NODE_RPC>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getBlockByNumber","params":["latest",false]}'
从响应中读取 baseFeePerGas 字段。
第 3 步 — 估算 Gas
将所有 Gas 字段设为 0x0,并使用占位签名(dummy signature)发送 UserOperation:
curl -s "<GATECHAIN_BUNDLER_ENDPOINT>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "eth_estimateUserOperationGas",
"params": [
{
"sender": "<SENDER_ADDRESS>",
"nonce": "<NONCE_HEX>",
"callData": "<CALL_DATA_HEX>",
"callGasLimit": "0x0",
"verificationGasLimit": "0x0",
"preVerificationGas": "0x0",
"maxPriorityFeePerGas": "<MAX_PRIORITY_FEE_HEX>",
"maxFeePerGas": "<MAX_FEE_HEX>",
"signature": "0x000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000011b"
},
"<ENTRY_POINT_ADDRESS>"
]
}'
当不需要 factory 或 paymaster 时,请完全省略这些字段。将地址字段设为空字符串("")会导致 Bundler 返回 Invalid params 错误。
关于占位签名(dummy signature):Gas 估算需要一个格式有效的 65 字节 ECDSA 签名。传入空的 0x 会导致 ECDSA.recover() 回滚,从而破坏估算流程。请使用上述占位签名,或任何格式有效、但不匹配账户所有者密钥的 65 字节十六进制值。
返回示例:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"preVerificationGas": "<HEX>",
"verificationGasLimit": "<HEX>",
"callGasLimit": "<HEX>",
"paymasterVerificationGasLimit": "0x0"
}
}
第 4 步 — 签名
计算 UserOperation 哈希,并用账户所有者私钥签名:
userOpHash = keccak256(abi.encode(
keccak256(packed(userOp fields)),
entryPointAddress,
chainId // GateChain 主网:86
))
signature = owner.signMessage(userOpHash) // 需要 ETH 签名消息前缀
第 5 步 — 提交
curl -s "<GATECHAIN_BUNDLER_ENDPOINT>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 4,
"method": "eth_sendUserOperation",
"params": [
{
"sender": "<SENDER_ADDRESS>",
"nonce": "<NONCE_HEX>",
"callData": "<CALL_DATA_HEX>",
"callGasLimit": "<CALL_GAS_LIMIT_HEX>",
"verificationGasLimit": "<VERIFICATION_GAS_LIMIT_HEX>",
"preVerificationGas": "<PRE_VERIFICATION_GAS_HEX>",
"maxPriorityFeePerGas": "<MAX_PRIORITY_FEE_HEX>",
"maxFeePerGas": "<MAX_FEE_HEX>",
"signature": "<SIGNED_USER_OPERATION_HEX>"
},
"<ENTRY_POINT_ADDRESS>"
]
}'
返回示例:
{
"jsonrpc": "2.0",
"id": 4,
"result": "<USER_OPERATION_HASH>"
}
第 6 步 — 轮询回执
curl -s "<GATECHAIN_BUNDLER_ENDPOINT>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 5,
"method": "eth_getUserOperationReceipt",
"params": ["<USER_OPERATION_HASH>"]
}'
待确认时返回 null。建议每 3–5 秒轮询一次,并采用指数退避策略。确认后的结果包含 transactionHash 和 "success": true。
GateChain 出块时间约为 4-30 秒。
8. 自付模式 vs. Paymaster 模式
自付模式(无 Paymaster)
从 UserOperation 中完全省略所有 paymaster 和 factory 字段。不要将地址字段设为空字符串("")——Bundler 会拒绝请求。直接从 JSON 对象中去掉这些字段即可。
提交前,请确保智能账户在 EntryPoint 中有足够的 GT 存款:
entryPoint.depositTo{value: amount}(smartAccountAddress);
Paymaster 赞助模式
当 Paymaster 赞助 Gas 时,填写以下字段:
"paymaster": "<PAYMASTER_CONTRACT_ADDRESS>",
"paymasterVerificationGasLimit": "<HEX>",
"paymasterPostOpGasLimit": "<HEX>",
"paymasterData": "<PAYMASTER_SIGNED_DATA_HEX>"
paymasterData 由 Paymaster 服务在验证并联署 UserOperation 后提供。
9. SDK 快速入门(quick-start.js)
在生产环境中,quick-start.js 对原始 JSON-RPC 流程进行了封装。
安装依赖
npm install viem @aa-sdk/core @account-kit/smart-contracts
定义 Gate Chain 链
import { LocalAccountSigner, getEntryPoint } from '@aa-sdk/core';
import { createLightAccount } from '@account-kit/smart-contracts';
import {
createPublicClient,
defineChain,
http,
parseEther,
toHex,
encodeFunctionData,
getContract,
} from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
const {
PRIVATE_KEY,
RPC_URL = 'https://evm.nodeinfo.cc',
BUNDLER_URL = 'https://gatechain-bundler.gatenode.cc',
ENTRY_POINT = '0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108',
LIGHT_ACCOUNT_FACTORY = '0x13E9ed32155810FDbd067D4522C492D6f68E5944',
RECIPIENT_ADDRESS = '0xRECIPIENT_ADDRESS',
CHAIN_ID = '86',
TRANSFER_GT = '0.001',
} = process.env;
if (!PRIVATE_KEY) throw new Error('Missing PRIVATE_KEY');
const chain = defineChain({
id: Number(CHAIN_ID),
name: `GateChain-${CHAIN_ID}`,
nativeCurrency: { name: 'GT', symbol: 'GT', decimals: 18 },
rpcUrls: { default: { http: [RPC_URL] } },
});
const publicClient = createPublicClient({ chain, transport: http(RPC_URL) });
async function bundlerRpc(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 { result, error } = await res.json();
if (error) throw new Error(`${method}: ${JSON.stringify(error)}`);
return result;
}
async function main() {
// 1. 创建智能账户
const owner = LocalAccountSigner.privateKeyToAccountSigner(PRIVATE_KEY);
const ownerAddr = await owner.getAddress();
const entryPointDef = { ...getEntryPoint(chain, { version: '0.7.0' }), address: ENTRY_POINT };
// 向工厂查询正确的反事实地址
const factoryContract = getContract({
address: LIGHT_ACCOUNT_FACTORY,
abi: [
{
name: 'getAddress',
type: 'function',
stateMutability: 'view',
inputs: [
{ name: 'owner', type: 'address' },
{ name: 'salt', type: 'uint256' },
],
outputs: [{ name: '', type: 'address' }],
},
],
client: publicClient,
});
const accountAddress = await factoryContract.read.getAddress([ownerAddr, 0n]);
const account = await createLightAccount({
chain,
transport: http(RPC_URL),
signer: owner,
factoryAddress: LIGHT_ACCOUNT_FACTORY,
accountAddress,
version: 'v2.0.0',
entryPoint: entryPointDef,
});
const sender = account.address;
console.log('Smart account address:', sender);
// 2. 检查部署状态 & 获取 initCode
const code = await publicClient.getCode({ address: sender });
const isDeployed = code && code !== '0x';
const initCode = await account.getInitCode();
const needsDeploy = !isDeployed && initCode && initCode !== '0x';
// 3. 构建 callData
const callData = encodeFunctionData({
abi: [
{
name: 'execute',
type: 'function',
stateMutability: 'nonpayable',
inputs: [
{ name: 'dest', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'func', type: 'bytes' },
],
outputs: [],
},
],
functionName: 'execute',
args: [RECIPIENT_ADDRESS, parseEther(TRANSFER_GT), '0x'],
});
// 4. 获取 nonce
const nonce = await publicClient.readContract({
address: ENTRY_POINT,
abi: entryPointDef.abi,
functionName: 'getNonce',
args: [sender, 0n],
});
// 5. Gas 限制 & 费用(需为整数 gwei)
// 注意:请根据实际交易复杂度调整这些值。
// 生产环境中应使用 eth_estimateUserOperationGas 获取精确值。
const callGasLimit = 200_000n;
const verificationGasLimit = 500_000n;
const preVerificationGas = 100_000n;
const maxPriorityFeePerGas = 1_000_000_000n; // 1 gwei
const maxFeePerGas = 20_000_000_000n; // 20 gwei
// 6. 通过 EntryPoint.getUserOpHash() 签名
const normalizedPk = PRIVATE_KEY.startsWith('0x') ? PRIVATE_KEY : `0x${PRIVATE_KEY}`;
const eoa = privateKeyToAccount(normalizedPk);
const packedForHash = {
sender,
nonce,
initCode: needsDeploy ? initCode : '0x',
callData,
accountGasLimits:
'0x' +
verificationGasLimit.toString(16).padStart(32, '0') +
callGasLimit.toString(16).padStart(32, '0'),
preVerificationGas,
gasFees:
'0x' +
maxPriorityFeePerGas.toString(16).padStart(32, '0') +
maxFeePerGas.toString(16).padStart(32, '0'),
paymasterAndData: '0x',
signature: '0x',
};
const userOpHash = await publicClient.readContract({
address: ENTRY_POINT,
abi: entryPointDef.abi,
functionName: 'getUserOpHash',
args: [packedForHash],
});
const signature = await eoa.sign({ hash: userOpHash });
// 7. 发送
const hash = await bundlerRpc('eth_sendUserOperation', [
{
sender,
nonce: toHex(nonce),
callData,
callGasLimit: toHex(callGasLimit),
verificationGasLimit: toHex(verificationGasLimit),
preVerificationGas: toHex(preVerificationGas),
maxPriorityFeePerGas: toHex(maxPriorityFeePerGas),
maxFeePerGas: toHex(maxFeePerGas),
signature,
...(needsDeploy
? { factory: initCode.slice(0, 42), factoryData: '0x' + initCode.slice(42) }
: {}),
},
ENTRY_POINT,
]);
console.log('UserOp hash:', hash);
// 8. 等待回执
let receipt;
for (let i = 0; i < 30; i++) {
receipt = await bundlerRpc('eth_getUserOperationReceipt', [hash]);
if (receipt?.receipt?.transactionHash) break;
await new Promise((r) => setTimeout(r, 4000));
}
console.log('Tx hash:', receipt.receipt.transactionHash);
}
main().catch((err) => {
console.error('Failed:', err);
process.exit(1);
});
10. EIP-7702 授权字段
EIP-7702 允许 EOA 在单笔交易中将其代码临时委托给智能合约。结合 ERC-4337 使用时,eip7702Auth 字段使 EOA 账户无需单独部署即可获得智能账户能力。
该字段为可选项,仅在 UserOperation 需要 EIP-7702 代码委托时填写。
字段结构:
"eip7702Auth": {
"chainId": "0x56",
"address": "<DELEGATED_CONTRACT_ADDRESS>",
"nonce": "<EOA_NONCE_HEX>",
"yParity": "<0x0 或 0x1>",
"r": "<SIGNATURE_R_HEX>",
"s": "<SIGNATURE_S_HEX>"
}
Gate Chain 主网的 chainId 为 0x56(十进制 86)。当该字段存在时,Bundler 会将授权元组加入交易的 authorizationList,并以 EIP-7702 交易类型提交。UserOperation 的哈希计算也会纳入被委托的合约地址。
11. 智能账户部署
首次使用时,智能账户尚未在链上存在。账户地址是反事实的——由工厂地址和所有者地址确定性推导而来。
要在首次操作的同一笔交易中部署新智能账户,请填写 factory 和 factoryData:
"factory": "<ACCOUNT_FACTORY_ADDRESS>",
"factoryData": "<ENCODED_CONSTRUCTOR_CALLDATA>"
账户部署完成后(nonce > 0),后续操作省略这两个字段。
如果 factory 非空,EntryPoint 会在验证阶段调用 factory.createAccount(factoryData)。部署后的地址必须与 sender 一致,否则操作将被拒绝。
12. RPC 方法参考
Bundler 方法
| 方法 | 说明 |
|---|---|
eth_supportedEntryPoints | 返回当前活跃的 EntryPoint 合约地址 |
eth_estimateUserOperationGas | 估算 UserOperation 的 Gas 限制 |
eth_sendUserOperation | 将已签名的 UserOperation 提交至内存池 |
eth_getUserOperationReceipt | 返回执行结果(待确认时返回 null) |
eth_getUserOperationByHash | 根据哈希返回完整的 UserOperation 详情 |
标准 EVM 方法(调用 Gate Chain RPC)
| 方法 | 说明 |
|---|---|
eth_getBlockByNumber | 获取最新区块以读取 baseFeePerGas |
eth_call | 模拟合约调用(不发送交易) |
eth_getTransactionReceipt | 查询链上交易回执 |
数值格式
请求和响应中的所有数值均使用带 0x 前缀的十六进制字符串。示例:gasLimit: 21000 → "0x5208"。
13. 错误代码参考
| 代码 | 名称 | 原因 | 解决方案 |
|---|---|---|---|
AA10 | Sender 已存在 | 提供了 factory 但账户已部署 | 移除 factory 和 factoryData 字段 |
AA13 | Init code 失败 | 账户创建期间工厂调用回滚 | 检查工厂地址和 factoryData 编码 |
AA21 | 未支付预付款 | 智能账户在 EntryPoint 中存款不足 | 提交前调用 entryPoint.depositTo() |
AA23 | 验证时回滚 | validateUserOp() 返回失败 | 检查签名、nonce 和账户验证逻辑 |
AA25 | 账户 nonce 无效 | nonce 不匹配 | 重新查询 EntryPoint.getNonce() 并重建 UserOp |
AA31 | Paymaster 存款不足 | Paymaster 在 EntryPoint 中余额不足 | 补充 Paymaster 存款 |
AA33 | Paymaster 验证时回滚 | validatePaymasterUserOp() 失败 | 检查 paymasterData 和 Paymaster 策略条件 |
AA40 | 超出验证 Gas 上限 | verificationGasLimit 过低 | 重新运行 eth_estimateUserOperationGas |
AA51 | 预付款低于实际 Gas 成本 | maxFeePerGas 低于当前网络水平 | 重新查询 eth_maxPriorityFeePerGas 并重建 UserOp |
14. 故障排查
EntryPoint 地址不匹配 — 始终在运行时通过 eth_supportedEntryPoints 动态获取 EntryPoint 地址。硬编码在版本不同时会导致静默验证失败。
Gas 估算失败 — eth_estimateUserOperationGas 报错的常见原因:callData 编码无效、账户余额不足以通过预付款检查,或验证逻辑在模拟时回滚。
回执持续返回 null — 操作仍在内存池中。每 3–5 秒轮询一次。如果超过 1 分钟仍为 null(GateChain 出块约 5 秒),操作可能因费用过低被丢弃。请更新 Gas 值后重新提交。
Nonce 冲突 — 每个 sender 在内存池中每个 nonce 同时只能有一个待确认的 UserOperation。要替换卡住的操作,请以相同 nonce 重新提交,并将 maxPriorityFeePerGas 提高至少 10%。
混淆 Gate Chain 与 Gate Layer — Gate Chain(Chain ID 86)和 Gate Layer(Chain ID 10088)是独立的网络。UserOperation 哈希包含 chainId,因此两条链之间的签名不可互换。请确认 SDK 配置使用了目标网络正确的链定义和 Bundler 端点。