Bundler RPC
El Bundler de BOT Chain recibe UserOperations RPC, ejecuta simulaciones, mantiene su cola y utiliza un Relayer para llamar a EntryPoint mediante una transacción normal. El RPC estándar de BOT Chain no ofrece eth_sendUserOperation.

1. UserOperation RPC v0.7
BOT Chain utiliza EntryPoint v0.7 y no es compatible con el formato de campos v0.6.
1.1 Estructura de la solicitud 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 Campos
| Campo | Tipo | Estimación de Gas | Envío final | Descripción |
|---|---|---|---|---|
sender | address (20 bytes, hexadecimal con prefijo 0x) | Obligatorio | Obligatorio | Dirección de la cuenta inteligente, incluso si aún no está desplegada |
nonce | QUANTITY (entero hexadecimal con prefijo 0x, sin ceros iniciales; cero es 0x0) | Obligatorio | Obligatorio | Nonce gestionado por EntryPoint |
factory | address | Condicional | Condicional | Factory utilizado para el primer despliegue |
factoryData | DATA (bytes hexadecimales de longitud par con prefijo 0x; vacío es 0x) | Condicional | Condicional | Calldata para que el Factory cree la cuenta |
callData | DATA | Obligatorio | Obligatorio | Datos de ejecución enviados a la cuenta |
callGasLimit | QUANTITY | Puede omitirse o ser temporal | Obligatorio | Límite de Gas de ejecución |
verificationGasLimit | QUANTITY | Puede omitirse o ser temporal | Obligatorio | Límite de Gas para Factory y validación |
preVerificationGas | QUANTITY | Puede omitirse o ser temporal | Obligatorio | Sobrecoste fijo de calldata y bundle |
maxFeePerGas | QUANTITY | Opcional | Obligatorio | Precio total máximo de Gas |
maxPriorityFeePerGas | QUANTITY | Opcional | Obligatorio | Tarifa prioritaria máxima |
paymaster | address | Condicional | Condicional | Paymaster que paga el Gas |
paymasterVerificationGasLimit | QUANTITY | Condicional | Condicional | Límite de Gas para validar el Paymaster |
paymasterPostOpGasLimit | QUANTITY | Condicional | Condicional | Límite de Gas de postOp |
paymasterData | DATA | Opcional | Opcional | Autorización o política personalizada |
signature | DATA | Obligatoria; puede ser provisional | Obligatoria; autorización real | Interpretada y validada por sender |
2. Construir una UserOperation
Los siguientes fragmentos TypeScript pueden combinarse y ejecutarse en orden. El ejemplo usa BOT Chain Testnet, SimpleAccount y ningún Paymaster. Instala primero viem:
npm install viem
Configuración compartida y función auxiliar 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;
}
El ejemplo lee la clave privada del Owner desde una variable de entorno y solo es apropiado para servidor o pruebas locales. Una aplicación web debe usar una cuenta proporcionada por el monedero y nunca incluir claves privadas en el frontend.
2.1 Determinar sender
sender es la dirección de la cuenta inteligente, no la EOA Owner.
- SimpleAccount la calcula con
SimpleAccountFactory.getAddress(owner, salt). - Una cuenta personalizada sigue sus propias reglas de Factory o despliegue.
- Usa
eth_getCodedel RPC de la cadena para saber sisenderya está desplegada.
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 Establecer los campos Factory
| Estado de la cuenta | factory | factoryData |
|---|---|---|
| Sin desplegar | Proporcionar la dirección del Factory | Proporcionar calldata de creación codificado con ABI |
| Desplegada | Omitir | Omitir |
Para SimpleAccount, factoryData codifica createAccount(owner, salt). Incluir campos Factory para una cuenta ya desplegada provoca AA10 sender already constructed.
const factoryFields = accountDeployed
? {}
: {
factory: FACTORY,
factoryData: encodeFunctionData({
abi: factoryAbi,
functionName: "createAccount",
args: [owner.address, salt],
}),
};
2.3 Leer el nonce
Lee el nonce desde EntryPoint con getNonce(sender, key):
192 bits superiores: clave de nonce
64 bits inferiores: secuencia de esa clave
SimpleAccount usa la clave 0. No utilices el nonce de transacción de la EOA Owner, eth_getTransactionCount de la cuenta ni un contador local no sincronizado.
const nonce = await publicClient.readContract({
address: ENTRY_POINT,
abi: entryPointAbi,
functionName: "getNonce",
args: [sender, 0n],
});
2.4 Generar callData
La implementación de la cuenta define callData:
- SimpleAccount usa
executeoexecuteBatch. - Una cuenta personalizada usa su propia interfaz pública.
callDatadebe cumplir la política aplicada envalidateUserOp.
// Llamada sin valor y con calldata vacío para demostrar la codificación.
// Sustituye estos valores por los de tu contrato y llamada de negocio.
const target = owner.address;
const targetValue = 0n;
const targetCallData = "0x";
const callData = encodeFunctionData({
abi: simpleAccountAbi,
functionName: "execute",
args: [target, targetValue, targetCallData],
});
2.5 Elegir quién paga el Gas
| Escenario | Campos y financiación |
|---|---|
| Sin Paymaster | Omitir todos sus campos y asegurar un depósito suficiente de la cuenta |
| Con Paymaster | Proporcionar dirección, ambos límites de Gas y los datos requeridos |
No incluyas la dirección cero si no se utiliza Paymaster. BOT Chain no ofrece un Paymaster público, pero los desarrolladores pueden desplegar uno propio.
El ejemplo comprueba el depósito de EntryPoint y el saldo nativo antes de estimar:
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 Construir la UserOperation para estimación
Este objeto es el argumento userOp de eth_estimateUserOperationGas(userOp, entryPoint). Solo sirve para simulación y estimación; no envía una transacción.
| Grupo de campos | Durante la estimación |
|---|---|
| sender, nonce, callData | Usar valores finales |
| factory, factoryData | Incluir u omitir según el estado de despliegue |
| Campos Paymaster | Incluir u omitir según el patrocinio |
| Tres límites de Gas | Pueden omitirse o usar valores cero o temporales |
| Precios de Gas | Opcionales; el Bundler sugiere valores |
| signature | Usar un valor provisional con formato y longitud adecuados |
Skandha acepta DATA vacío (0x) y lo sustituye internamente durante la estimación. Si se necesita una firma provisional explícita de 65 bytes para SimpleAccount, utiliza:
0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c
Solo sirve para estimar Gas y no puede enviarse como firma final.
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 Aplicar la estimación de Gas
Tras llamar a eth_estimateUserOperationGas, copia a la UserOperation final:
callGasLimitverificationGasLimitpreVerificationGasmaxFeePerGasmaxPriorityFeePerGas- Los dos límites de Gas del Paymaster, si se usa
Después, todos los campos finales salvo signature deben permanecer sin cambios.
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 Calcular userOpHash
La función recibe la forma RPC sin empaquetar, pero los campos QUANTITY deben convertirse a 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,
});
Es un cálculo puramente off-chain. entryPointAddress, entryPointVersion y chainId participan en el hash y deben coincidir con la red. signature no participa.
2.9 Obtener la signature final
Firma userOpHash mediante un monedero, SDK o servicio de firma.
- SimpleAccount exige que el Owner firme el hash mediante EIP-191 ECDSA.
- Una cuenta personalizada puede usar multisig, Session Keys, firmas agregadas u otro esquema.
La firma debe coincidir con sender.validateUserOp. No modifiques ningún campo incluido en el hash después de firmar.
const signature = await owner.signMessage({
message: { raw: userOpHash },
});
const signedUserOperation = { ...finalUserOperation, signature };
2.10 Enviar y consultar el resultado
Envía la UserOperation final mediante eth_sendUserOperation. El Bundler devuelve un userOpHash; después consulta el recibo para seguir la transacción del bundle y la ejecución.
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. Métodos RPC del Bundler
3.1 eth_supportedEntryPoints
Devuelve las direcciones EntryPoint aceptadas por el Bundler. El resultado debe incluir el despliegue configurado. Comprueba también que eth_chainId del Bundler coincida con el RPC de la cadena.
3.2 eth_estimateUserOperationGas
3.2.1 Parámetros
| Posición | Parámetro | Tipo | Descripción |
|---|---|---|---|
| 1 | userOp | Estimate UserOperation | UserOperation RPC que se simulará |
| 2 | entryPoint | address | EntryPoint compatible con el Bundler |
| 3 | stateOverrides | object, opcional | Sobrescrituras de estado solo para la simulación |
Las sobrescrituras se indexan por dirección y admiten balance, nonce, code, state y stateDiff. Solo afectan a esta simulación.
3.2.2 Resultado
| Campo | Tipo | Descripción |
|---|---|---|
callGasLimit | QUANTITY | Límite de Gas de ejecución |
verificationGasLimit | QUANTITY | Límite de Gas para Factory y validación |
verificationGas | QUANTITY | Alias de Gas de validación devuelto por Skandha |
preVerificationGas | QUANTITY | Sobrecoste fijo de calldata y bundle |
maxFeePerGas | QUANTITY | Precio total máximo sugerido |
maxPriorityFeePerGas | QUANTITY | Tarifa prioritaria máxima sugerida |
paymasterVerificationGasLimit | QUANTITY, opcional | Límite de Gas para validar Paymaster |
paymasterPostOpGasLimit | QUANTITY, opcional | Límite de Gas de postOp |
validAfter | QUANTITY, opcional | Inicio del periodo de validez |
validUntil | QUANTITY, opcional | Fin del periodo de validez |
3.3 eth_sendUserOperation
Recibe la UserOperation RPC final y la dirección EntryPoint. Devuelve un bytes32 userOpHash, no el hash de la transacción externa handleOps.
Que el Bundler acepte la operación solo significa que entró en el flujo de empaquetado; no que ya esté on-chain o haya tenido éxito.
3.4 eth_getUserOperationByHash
Devuelve null si no la encuentra. En caso contrario devuelve:
| Campo | Descripción |
|---|---|
userOperation | UserOperation RPC original |
entryPoint | Punto de ejecución |
blockNumber | Número de bloque, opcional |
blockHash | Hash de bloque, opcional |
transactionHash | Hash de la transacción handleOps, opcional |
Una operación aún en el mempool puede no tener campos de transacción o bloque.
3.5 eth_getUserOperationReceipt
Devuelve null hasta que la operación esté on-chain. Sus campos principales son:
| Campo | Descripción |
|---|---|
userOpHash | Hash de UserOperation |
sender | Cuenta inteligente |
nonce | Nonce de UserOperation |
paymaster | Paymaster que paga el Gas, opcional |
actualGasCost | Coste de Gas real |
actualGasUsed | Gas real utilizado |
success | Si la ejecución de negocio tuvo éxito |
reason | Motivo del fallo, opcional |
logs | Logs relacionados |
receipt | Recibo de la transacción externa handleOps |
Usa success como resultado final. Una transacción externa correcta no implica que todas las UserOperations internas lo sean.
3.6 skandha_getGasPrice
Este método extendido no recibe parámetros y devuelve maxFeePerGas y maxPriorityFeePerGas. Para admitir otros Bundlers, la estrategia alternativa también debe producir ambos campos. eth_gasPrice puede ser una entrada de precio, pero no es una cotización EIP-1559 completa. No establezcas el precio de Gas de UserOperation en cero aunque la base fee on-chain sea cero.
4. Solución de problemas
| Síntoma | Comprobación |
|---|---|
Invalid Entrypoint | Lista del Bundler, red y dirección EntryPoint |
| Falla la validación de parámetros | Codificación QUANTITY/DATA, longitud de dirección y campos condicionales |
AA10 | Una cuenta desplegada todavía incluye campos Factory |
AA21 | Depósito o prefund insuficiente |
AA24 | Firma, hash, chainId o EntryPoint no coinciden |
AA25 | Nonce usado o contrario a la política |
AA31 / AA33 | Depósito insuficiente o validación del Paymaster fallida |
| El recibo no aparece | Estado del mempool, precio de Gas, nonce y Relayer |
Recibo con success: false | Decodificar UserOperationRevertReason y revisar la llamada de negocio |