> For the complete documentation index, see [llms.txt](https://igra-labs.gitbook.io/igralabs-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://igra-labs.gitbook.io/igralabs-docs/for-developers/building-with-account-abstraction.md).

# Building with Account Abstraction

Igra supports both [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) (protocol-level EOA delegation) and [ERC-4337](https://eips.ethereum.org/EIPS/eip-4337) (smart accounts). Prague/Pectra is active from genesis.

## EIP-7702 — Batch Transactions from Any EOA

Send a type-4 transaction that delegates an EOA to [Simple7702Account](https://explorer.igralabs.com/address/0x4Cd241E8d1510e30b2076397afc7508Ae59C66c9) and batches multiple calls in one atomic tx. No smart account deployment needed.

```javascript
// Example: EOA delegates to Simple7702Account for one tx
// Approve token + swap on DEX in a single atomic transaction
// No smart account deployment needed

const authorization = {
  chainId: 38833,
  address: "0x4Cd241E8d1510e30b2076397afc7508Ae59C66c9", // Simple7702Account
  nonce: await provider.getTransactionCount(wallet.address),
};

// Sign authorization with EOA
const signedAuth = await wallet.signAuthorization(authorization);

// Send type-4 tx with batched calls
const tx = await wallet.sendTransaction({
  type: 4,
  authorizationList: [signedAuth],
  to: wallet.address, // calls execute on your own EOA (now delegated)
  data: encodeBatchedCalls([approveCall, swapCall]),
  gasPrice: ethers.parseUnits("1100", "gwei"),
});
```

> **Verifying delegation:** `eth_getCode` currently returns `0x` for delegated EOAs (Reth v1.9.3). The explorer also shows "EOA" because it reads `eth_getCode`. To confirm your delegation is active, call a function from the delegate contract on your EOA address:
>
> ```javascript
> // If this succeeds, delegation is active
> const result = await provider.call({
>   to: yourEOA,
>   data: iface.encodeFunctionData("execute", [targetAddr, 0n, "0x"]),
> });
> ```
>
> The delegation persists across transactions until explicitly revoked.

## ERC-4337 — Create a Smart Account

Deploy a smart account via SimpleAccountFactory:

```javascript
const factory = new ethers.Contract(
  "0x13E9ed32155810FDbd067D4522C492D6f68E5944",
  ["function createAccount(address owner, uint256 salt) returns (address)"],
  wallet
);

const tx = await factory.createAccount(ownerAddress, 0, {
  gasPrice: ethers.parseUnits("1100", "gwei"),
});
```

## Submitting a UserOp via Bundler

Bundler endpoint: `https://bubundler.jobberwocky.co` (community-operated)

The EntryPoint is **v0.8** (ERC-4337 with EIP-712 hashing). Use the **v0.8 RPC field format** — do not include legacy v0.6 fields (`initCode`, `paymasterAndData`). Omit `factory`/`paymaster` fields when not used.

### Setup

```javascript
const ENTRYPOINT = "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108";

function encodeExecute(dest, value, data) {
  return new ethers.Interface([
    "function execute(address dest, uint256 value, bytes calldata func)"
  ]).encodeFunctionData("execute", [dest, value, data]);
}

function packUints(hi, lo) {
  return "0x" + BigInt(hi).toString(16).padStart(32, "0")
              + BigInt(lo).toString(16).padStart(32, "0");
}

function getUserOpHashCalldata(op, paymasterAndData = "0x") {
  const epIface = new ethers.Interface([{
    name: "getUserOpHash",
    type: "function",
    stateMutability: "view",
    inputs: [{ type: "tuple", components: [
      { name: "sender", type: "address" },
      { name: "nonce", type: "uint256" },
      { name: "initCode", type: "bytes" },
      { name: "callData", type: "bytes" },
      { name: "accountGasLimits", type: "bytes32" },
      { name: "preVerificationGas", type: "uint256" },
      { name: "gasFees", type: "bytes32" },
      { name: "paymasterAndData", type: "bytes" },
      { name: "signature", type: "bytes" },
    ]}],
    outputs: [{ type: "bytes32" }],
  }]);

  return epIface.encodeFunctionData("getUserOpHash", [[
    op.sender,
    BigInt(op.nonce),
    "0x",
    op.callData,
    packUints(BigInt(op.verificationGasLimit), BigInt(op.callGasLimit)),
    BigInt(op.preVerificationGas),
    packUints(BigInt(op.maxPriorityFeePerGas), BigInt(op.maxFeePerGas)),
    paymasterAndData,
    "0x",
  ]]);
}
```

### Step 1 — Get the nonce

Always read the nonce fresh before every UserOp:

```javascript
const nonce = await provider.call({
  to: ENTRYPOINT,
  data: new ethers.Interface([
    "function getNonce(address, uint192) view returns (uint256)"
  ]).encodeFunctionData("getNonce", [smartAccountAddress, 0]),
});
```

### Step 2 — Build the UserOp

```javascript
const executeCalldata = encodeExecute(
  "0xTARGET_CONTRACT_ADDRESS", // replace with target address
  0n,
  "0x",
);

const userOp = {
  sender: smartAccountAddress,
  nonce: "0x" + BigInt(nonce).toString(16),
  callData: executeCalldata,
  callGasLimit: "0x186a0",          // 100,000
  verificationGasLimit: "0x30d40",  // 200,000
  preVerificationGas: "0xc350",     // 50,000
  maxFeePerGas: "0x" + (1200000000000n).toString(16),
  maxPriorityFeePerGas: "0x" + (1200000000000n).toString(16),
  signature: "0x" + "ff".repeat(65), // dummy for estimation
};
// Do NOT include initCode or paymasterAndData — those are v0.6 fields.
// The bundler rejects them or fails simulation silently.
```

### Step 3 — Estimate gas

```javascript
const est = await fetch("https://bubundler.jobberwocky.co", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    jsonrpc: "2.0", id: 1,
    method: "eth_estimateUserOperationGas",
    params: [userOp, ENTRYPOINT],
  }),
}).then(r => r.json());

userOp.callGasLimit = est.result.callGasLimit;
userOp.verificationGasLimit = est.result.verificationGasLimit;
userOp.preVerificationGas = est.result.preVerificationGas;
```

### Step 4 — Sign (raw ECDSA, no prefix)

The account validates signatures via `ECDSA.recover(hash, sig)` — use raw signing, not `signMessage` (which adds `\x19Ethereum Signed Message` prefix).

```javascript
// Get the hash from the EntryPoint on-chain
const opHash = await provider.call({
  to: ENTRYPOINT,
  data: getUserOpHashCalldata(userOp), // see Setup above
});

// Raw sign — no prefix
userOp.signature = wallet.signingKey.sign(opHash).serialized;
```

### Step 5 — Send

```javascript
const result = await fetch("https://bubundler.jobberwocky.co", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    jsonrpc: "2.0", id: 1,
    method: "eth_sendUserOperation",
    params: [userOp, ENTRYPOINT],
  }),
}).then(r => r.json());

// Poll for receipt
const receipt = await fetch("https://bubundler.jobberwocky.co", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    jsonrpc: "2.0", id: 1,
    method: "eth_getUserOperationReceipt",
    params: [result.result],
  }),
}).then(r => r.json());
```

### Common Errors

| Error                                        | Cause                                              | Fix                                                                                                   |
| -------------------------------------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| AA23 reverted                                | Signature validation failed                        | Use raw ECDSA (no `signMessage`), get hash from `getUserOpHash()` on-chain, check nonce is fresh      |
| AA21 didn't pay prefund                      | Smart account has insufficient iKAS                | Fund the account: prefund = (verificationGasLimit + callGasLimit + preVerificationGas) × maxFeePerGas |
| "could not parse simulate validation result" | Using v0.6 fields (`initCode`, `paymasterAndData`) | Remove those fields — use v0.8 format                                                                 |
| ECDSAInvalidSignatureLength                  | Signature wrong length                             | Must be exactly 65 bytes (r + s + v)                                                                  |

## Pay Gas in USDC (ERC-20 Paymaster)

The ERC-20 Paymaster at `0xe643D56CBd46b557b11753C6cA579a0da6486CF7` lets users pay gas in USDC instead of holding iKAS. It uses **Mode 2 (guarantor)** — the bundler's sponsor service co-signs each UserOp to authorize USDC payment.

### How it works

1. Build a UserOp (steps 1–2 above)
2. Call `pm_sponsorUserOperation` on the bundler — it returns paymaster fields with a guarantor signature
3. Estimate gas, then re-sponsor (gas values changed → guarantor signature invalidated)
4. Sign the UserOp with the user's key (raw ECDSA, step 4 above)
5. Submit via `eth_sendUserOperation`

The smart account needs zero iKAS. The paymaster's EntryPoint deposit covers gas, and USDC is deducted from the smart account.

### Step-by-step

```javascript
const BUNDLER = "https://bubundler.jobberwocky.co";
const PAYMASTER = "0xe643D56CBd46b557b11753C6cA579a0da6486CF7";
const USDC = "0xA5b8BF902b2844dA17d4506cc827F7F1681735E7";

// Helper: call bundler RPC
async function rpc(method, params) {
  const r = await fetch(BUNDLER, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ jsonrpc: "2.0", method, params, id: 1 }),
  });
  const d = await r.json();
  if (d.error) throw new Error(`${method}: ${d.error.message}`);
  return d.result;
}

// 1. Build UserOp with dummy signature (no paymaster fields yet)
const op = {
  sender: smartAccountAddress,
  nonce: "0x" + BigInt(nonce).toString(16),
  callData: executeCalldata,
  callGasLimit: "0xF4240",           // 1M (high for estimation)
  verificationGasLimit: "0x7A120",   // 500k
  preVerificationGas: "0x186A0",     // 100k
  maxFeePerGas: "0x" + (1200000000000n).toString(16),
  maxPriorityFeePerGas: "0x" + (1200000000000n).toString(16),
  signature: dummySig,
};

// 2. Get paymaster sponsorship (pre-estimate)
const sponsor1 = await rpc("pm_sponsorUserOperation", [op, ENTRYPOINT]);
op.paymaster = sponsor1.paymaster;
op.paymasterData = sponsor1.paymasterData;
op.paymasterVerificationGasLimit = sponsor1.paymasterVerificationGasLimit;
op.paymasterPostOpGasLimit = sponsor1.paymasterPostOpGasLimit;

// 3. Estimate gas (with paymaster fields attached)
const gas = await rpc("eth_estimateUserOperationGas", [op, ENTRYPOINT]);
op.callGasLimit = gas.callGasLimit;
op.verificationGasLimit = gas.verificationGasLimit;
op.preVerificationGas = gas.preVerificationGas;
if (gas.paymasterVerificationGasLimit)
  op.paymasterVerificationGasLimit = gas.paymasterVerificationGasLimit;
if (gas.paymasterPostOpGasLimit)
  op.paymasterPostOpGasLimit = gas.paymasterPostOpGasLimit;

// 4. Re-sponsor (gas values changed → guarantor sig invalidated)
const sponsor2 = await rpc("pm_sponsorUserOperation", [op, ENTRYPOINT]);
op.paymasterData = sponsor2.paymasterData;

// 5. Sign (raw ECDSA — include paymasterAndData in the hash)
const pmV = BigInt(op.paymasterVerificationGasLimit).toString(16).padStart(32, "0");
const pmP = BigInt(op.paymasterPostOpGasLimit).toString(16).padStart(32, "0");
const pmAndData = op.paymaster.toLowerCase() + pmV + pmP + op.paymasterData.slice(2);

const opHash = await provider.call({
  to: ENTRYPOINT,
  data: getUserOpHashCalldata(op, "0x" + pmAndData),
});
op.signature = wallet.signingKey.sign(opHash).serialized;

// 6. Submit
const userOpHash = await rpc("eth_sendUserOperation", [op, ENTRYPOINT]);
```

### USDC approval (bootstrap)

The paymaster needs USDC allowance from the smart account. If this is the first UserOp, bundle the approval into the same call using `executeBatch`:

```javascript
function batch(calls) {
  return new ethers.Interface([
    "function executeBatch((address target, uint256 value, bytes data)[] calls)",
  ]).encodeFunctionData("executeBatch", [calls]);
}

// First UserOp: approve paymaster + do the actual operation
const callData = batch([
  {
    target: USDC,
    value: 0n,
    data: new ethers.Interface(["function approve(address,uint256)"])
      .encodeFunctionData("approve", [PAYMASTER, ethers.MaxUint256]),
  },
  {
    target: "0xTARGET",  // your actual call
    value: 0n,
    data: "0x...",
  },
]);
```

This works because the paymaster only checks allowance in `postOp` (after execution), so the approval and the operation happen atomically in the same UserOp.

### `pm_sponsorUserOperation` reference

**Endpoint:** `https://bubundler.jobberwocky.co` (same as the bundler)

**Method:** `pm_sponsorUserOperation`

**Params:** `[userOp, entryPointAddress]`

**Returns:**

```json
{
  "paymaster": "0xe643D56CBd46b557b11753C6cA579a0da6486CF7",
  "paymasterVerificationGasLimit": "0x30d40",
  "paymasterPostOpGasLimit": "0x186a0",
  "paymasterData": "0x02..."
}
```

The `paymasterData` contains: mode byte (`0x02`) + guarantor address + validity window + guarantor signature. Attach all four returned fields to your UserOp before signing.

**Why call it twice?** The guarantor signs over the full UserOp including gas values. After `eth_estimateUserOperationGas` adjusts gas limits, the first signature is invalid. The second call re-signs with the final gas values.

### Gas cost

A sponsored UserOp costs approximately **$0.01 USDC** in gas fees. The smart account holds zero iKAS — the paymaster's EntryPoint deposit covers native gas.

## Reference

* [EIP-4337](https://eips.ethereum.org/EIPS/eip-4337) — Account Abstraction Using Alt Mempool
* [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) — Set EOA account code
* [Contract Addresses](/igralabs-docs/for-developers/contract-addresses/account-abstraction.md) — Deployed contracts and ABIs
