Saltar al contenido principal

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.

Flujo del mempool de UserOperations

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​

CampoTipoEstimación de GasEnvío finalDescripción
senderaddress (20 bytes, hexadecimal con prefijo 0x)ObligatorioObligatorioDirección de la cuenta inteligente, incluso si aún no está desplegada
nonceQUANTITY (entero hexadecimal con prefijo 0x, sin ceros iniciales; cero es 0x0)ObligatorioObligatorioNonce gestionado por EntryPoint
factoryaddressCondicionalCondicionalFactory utilizado para el primer despliegue
factoryDataDATA (bytes hexadecimales de longitud par con prefijo 0x; vacío es 0x)CondicionalCondicionalCalldata para que el Factory cree la cuenta
callDataDATAObligatorioObligatorioDatos de ejecución enviados a la cuenta
callGasLimitQUANTITYPuede omitirse o ser temporalObligatorioLímite de Gas de ejecución
verificationGasLimitQUANTITYPuede omitirse o ser temporalObligatorioLímite de Gas para Factory y validación
preVerificationGasQUANTITYPuede omitirse o ser temporalObligatorioSobrecoste fijo de calldata y bundle
maxFeePerGasQUANTITYOpcionalObligatorioPrecio total máximo de Gas
maxPriorityFeePerGasQUANTITYOpcionalObligatorioTarifa prioritaria máxima
paymasteraddressCondicionalCondicionalPaymaster que paga el Gas
paymasterVerificationGasLimitQUANTITYCondicionalCondicionalLímite de Gas para validar el Paymaster
paymasterPostOpGasLimitQUANTITYCondicionalCondicionalLímite de Gas de postOp
paymasterDataDATAOpcionalOpcionalAutorización o política personalizada
signatureDATAObligatoria; puede ser provisionalObligatoria; autorización realInterpretada 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_getCode del RPC de la cadena para saber si sender ya 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 cuentafactoryfactoryData
Sin desplegarProporcionar la dirección del FactoryProporcionar calldata de creación codificado con ABI
DesplegadaOmitirOmitir

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 execute o executeBatch.
  • Una cuenta personalizada usa su propia interfaz pública.
  • callData debe cumplir la política aplicada en validateUserOp.
// 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​

EscenarioCampos y financiación
Sin PaymasterOmitir todos sus campos y asegurar un depósito suficiente de la cuenta
Con PaymasterProporcionar 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 camposDurante la estimación
sender, nonce, callDataUsar valores finales
factory, factoryDataIncluir u omitir según el estado de despliegue
Campos PaymasterIncluir u omitir según el patrocinio
Tres límites de GasPueden omitirse o usar valores cero o temporales
Precios de GasOpcionales; el Bundler sugiere valores
signatureUsar 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:

  • callGasLimit
  • verificationGasLimit
  • preVerificationGas
  • maxFeePerGas
  • maxPriorityFeePerGas
  • 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ónParámetroTipoDescripción
1userOpEstimate UserOperationUserOperation RPC que se simulará
2entryPointaddressEntryPoint compatible con el Bundler
3stateOverridesobject, opcionalSobrescrituras 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​

CampoTipoDescripción
callGasLimitQUANTITYLímite de Gas de ejecución
verificationGasLimitQUANTITYLímite de Gas para Factory y validación
verificationGasQUANTITYAlias de Gas de validación devuelto por Skandha
preVerificationGasQUANTITYSobrecoste fijo de calldata y bundle
maxFeePerGasQUANTITYPrecio total máximo sugerido
maxPriorityFeePerGasQUANTITYTarifa prioritaria máxima sugerida
paymasterVerificationGasLimitQUANTITY, opcionalLímite de Gas para validar Paymaster
paymasterPostOpGasLimitQUANTITY, opcionalLímite de Gas de postOp
validAfterQUANTITY, opcionalInicio del periodo de validez
validUntilQUANTITY, opcionalFin 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:

CampoDescripción
userOperationUserOperation RPC original
entryPointPunto de ejecución
blockNumberNúmero de bloque, opcional
blockHashHash de bloque, opcional
transactionHashHash 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:

CampoDescripción
userOpHashHash de UserOperation
senderCuenta inteligente
nonceNonce de UserOperation
paymasterPaymaster que paga el Gas, opcional
actualGasCostCoste de Gas real
actualGasUsedGas real utilizado
successSi la ejecución de negocio tuvo éxito
reasonMotivo del fallo, opcional
logsLogs relacionados
receiptRecibo 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íntomaComprobación
Invalid EntrypointLista del Bundler, red y dirección EntryPoint
Falla la validación de parámetrosCodificación QUANTITY/DATA, longitud de dirección y campos condicionales
AA10Una cuenta desplegada todavía incluye campos Factory
AA21Depósito o prefund insuficiente
AA24Firma, hash, chainId o EntryPoint no coinciden
AA25Nonce usado o contrario a la política
AA31 / AA33Depósito insuficiente o validación del Paymaster fallida
El recibo no apareceEstado del mempool, precio de Gas, nonce y Relayer
Recibo con success: falseDecodificar UserOperationRevertReason y revisar la llamada de negocio