メインコンテンツまでスキップ

Bundler RPC

BOT Chain Bundler は RPC UserOperation を受け取り、シミュレーション、キュー管理、パッケージ化を行い、Relayer から通常のトランザクションで EntryPoint を呼び出します。通常のチェーン RPC は eth_sendUserOperation を提供しません。

UserOperation mempool フロー

1. RPC UserOperation v0.7​

BOT Chain は EntryPoint v0.7 を使用し、v0.6 のフィールド形式とは互換性がありません。

1.1 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必須必須未デプロイの反実仮想アドレスを含むスマートアカウント
nonceQUANTITY必須必須EntryPoint 管理の nonce
factory / factoryDataaddress / DATA条件付き条件付き初回デプロイ用 Factory と calldata
callDataDATA必須必須アカウントへ送る実行データ
callGasLimitQUANTITY省略または仮値可必須実行 Gas 上限
verificationGasLimitQUANTITY省略または仮値可必須Factory・検証 Gas 上限
preVerificationGasQUANTITY省略または仮値可必須calldata・bundle 固定コスト
maxFeePerGas / maxPriorityFeePerGasQUANTITY任意必須最大総 Gas 単価と優先手数料
paymasteraddress条件付き条件付きGas を支払う Paymaster
paymasterVerificationGasLimit / paymasterPostOpGasLimitQUANTITY条件付き条件付きPaymaster の検証・postOp 上限
paymasterDataDATA任意任意独自の認可・ポリシーデータ
signatureDATAダミー可実認可必須sender が解釈・検証

QUANTITY は余分な先頭ゼロのない 0x 付き整数(ゼロは 0x0)、DATA は偶数長の 0x 付き bytes(空は 0x)です。

2. UserOperation の構築​

次の TypeScript 断片は順番に組み合わせて実行できます。BOT Chain Testnet、SimpleAccount、Paymaster なしの例です。

npm install viem
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 message = payload.error
? `${payload.error.code}: ${payload.error.message}`
: `HTTP ${response.status}`;
throw new Error(message);
}
if (payload.result === undefined) {
throw new Error(`Bundler returned no result for ${method}`);
}
return payload.result;
}

秘密鍵の環境変数はサーバーまたはローカルテスト専用です。ブラウザーではウォレット提供の署名アカウントを使い、秘密鍵をコードに含めないでください。

2.1 sender の決定​

sender は Owner EOA ではなくスマートアカウントです。SimpleAccount は getAddress(owner, salt) で算出し、eth_getCode でデプロイ済みか確認します。

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 エンコードした作成 calldata
デプロイ済み省略省略

デプロイ済みアカウントに Factory フィールドを付けると AA10 sender already constructed になります。

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

2.3 nonce の読み取り​

EntryPoint.getNonce(sender, key) を使用します。上位 192 bit が key、下位 64 bit が sequence です。SimpleAccount の既定 key は 0 です。EOA nonce や eth_getTransactionCount で代用しないでください。

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

2.4 callData の生成​

callData はアカウント実装が定義します。SimpleAccount は execute / executeBatch を使用し、validateUserOp の認可ポリシーに従う必要があります。

// 実際のコントラクト、金額、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 を使わない場合は関連フィールドをすべて省略し、アカウントの deposit または残高を用意します。使う場合は Paymaster アドレス、2 つの Gas 上限、独自データを指定します。未使用時にゼロアドレスを入れないでください。BOT Chain は公開 Paymaster を提供していません。

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) に渡すオブジェクトです。sender、nonce、callData は最終値、Factory・Paymaster は条件に応じて指定し、Gas 上限はゼロまたは仮値、署名は適切な形式・長さのダミーを使えます。

SimpleAccount の 65-byte ダミー署名(推定専用):

0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c
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 推定値の反映​

返された callGasLimit、verificationGasLimit、preVerificationGas、2 つの Gas 価格、および Paymaster 使用時の 2 上限を最終オブジェクトへ反映します。その後は 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 の計算​

QUANTITY を bigint に変換して計算します。EntryPoint アドレス、バージョン、chainId が hash に含まれ、signature は含まれません。

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,
});

2.9 最終 signature​

SimpleAccount は Owner による EIP-191 ECDSA 署名を要求します。独自アカウントは multisig、Session Key、集約署名などを使用できます。署名後に hash 対象フィールドを変更しないでください。

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

2.10 送信と結果照会​

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((r) => setTimeout(r, 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 アドレス配列を返します。設定済みアドレスが含まれ、Bundler の eth_chainId がチェーン RPC と一致することを確認します。

3.2 eth_estimateUserOperationGas​

引数は推定用 userOp、対応 entryPoint、任意の stateOverrides です。state override は balance、nonce、code、state、stateDiff に対応し、そのシミュレーションだけに作用します。

主な返り値は callGasLimit、verificationGasLimit(Skandha の別名 verificationGas)、preVerificationGas、2 つの Gas 価格です。Paymaster 使用時は検証・postOp Gas 上限、必要に応じて validAfter / validUntil も返します。

3.3 eth_sendUserOperation​

最終 UserOperation と EntryPoint を受け取り、外側の handleOps トランザクション hash ではなく bytes32 userOpHash を返します。受理はパッケージ化フローへの参加を意味するだけで、オンチェーン成功を保証しません。

3.4 eth_getUserOperationByHash​

未検出なら null、検出時は元の userOperation、entryPoint、任意の blockNumber、blockHash、transactionHash を返します。mempool 内ではブロック・トランザクション情報がない場合があります。

3.5 eth_getUserOperationReceipt​

オンチェーン化前は null です。userOpHash、sender、nonce、任意の paymaster、actualGasCost、actualGasUsed、success、任意の reason、logs、外側の receipt を返します。最終結果は success を確認してください。

3.6 skandha_getGasPrice​

引数なしで maxFeePerGas と maxPriorityFeePerGas を返します。他 Bundler 向けフォールバックでも両方を生成してください。eth_gasPrice は入力には使えますが EIP-1559 の完全な 2 フィールド見積もりではありません。base fee がゼロでも UserOperation Gas 価格をゼロにしないでください。

4. トラブルシューティング​

症状確認項目
Invalid Entrypoint対応リスト、ネットワーク、EntryPoint アドレス
パラメータ検証失敗QUANTITY / DATA、アドレス長、条件付きフィールド
AA10デプロイ済みアカウントに Factory フィールドが残っている
AA21deposit または prefund 不足
AA24署名、hash、chainId、EntryPoint の不一致
AA25nonce 使用済みまたはポリシー違反
AA31 / AA33Paymaster deposit または検証失敗
receipt が長時間ないmempool、Gas 見積もり、nonce、Relayer
success: falseUserOperationRevertReason とビジネス呼び出し