Skip to main content

EIP-7702 Quickstart

EIP-7702 enables Ethereum Externally Owned Accounts (EOAs) to authorize smart contract code at their address, unlocking:

  • Gas Sponsorship: Abstract gas fees through third-party sponsorship or ERC-20 token payments
  • Transaction Batching: Improve UX and security by combining approvals and contract interactions atomically
  • Granular Permissions: Grant specific, limited access to third parties for use cases like recurring payments
  • Forward Compatibility: Maintains compatibility with ERC-4337 and future native account abstraction

Learn more in the dedicated 7702 Overview.

This guide demonstrates how to upgrade your EOA to a Smart Account for batching and gas sponsorship capabilities.

Prerequisites

  • Sepolia Bundler and Paymaster endpoints from the Dashboard
  • Node and a package manager (yarn or npm)

Here's the complete code for you to reference if you prefer to run directly.

Step 1: Get setup

  1. Create a new directory for your project:
mkdir candide-eip7702-upgrade-eoa
cd candide-eip7702-upgrade-eoa
npx tsc --init
  1. Install dependencies
npm i typescript --save-dev
npm i abstractionkit dotenv ethers
  1. Configure Environment Variables
  • Create a .env file and add the following environment variables with your own values
.env
CHAIN_ID=11155111
JSON_RPC_NODE_PROVIDER=https://ethereum-sepolia-rpc.publicnode.com
BUNDLER_URL=https://api.candide.dev/public/v3/sepolia
PAYMASTER_URL=https://api.candide.dev/public/v3/sepolia
  1. Create an empty file and a function to run your script
index.ts
async function main(): Promise<void> {
// Rest of the code will go here...
}

main();

Step 2: Prepare Smart Account Instance

Prepare the smart account instance using Simple7702Account, a fully audited minimalist smart contract account safely authorized by any EOA. It provides full support for smart account features including batching and gas sponsorship.

index.ts
import { Simple7702Account } from "abstractionkit";
import { Wallet } from "ethers";

// For demo: generate a random EOA. In production, use user's EOA/private key
const eoaDelegator = Wallet.createRandom();

const smartAccount = new Simple7702Account(eoaDelegator.address);

console.log("Smart account address (sender): " + smartAccount.accountAddress);

Let's run this code

Terminal
npx ts-node index.ts

If everything worked, you will see the computed smart account address in the console, which is the same as the EOA address.

Details

Result example Account address(sender): 0x32afdcfa1e3bfe70d03ecb55b5c8045c26515c9d

Step 3: Generate callData for Minting NFTs

Beyond upgrading the EOA to a smart account, this example demonstrates executing two NFT mints in a single transaction.

index.ts
import {
MetaTransaction,
getFunctionSelector,
createCallData,
} from "abstractionkit";

// We will be minting two random NFTs in a single tx
const nftContractAddress = "0x9a7af758aE5d7B6aAE84fe4C5Ba67c041dFE5336";
const mintFunctionSignature = 'mint(address)';
const mintFunctionSelector = getFunctionSelector(mintFunctionSignature);
const mintTransactionCallData = createCallData(
mintFunctionSelector,
["address"],
[smartAccount.accountAddress]
);
const transaction1: MetaTransaction = {
to: nftContractAddress,
value: 0n,
data: mintTransactionCallData,
}

const transaction2: MetaTransaction = {
to: nftContractAddress,
value: 0n,
data: mintTransactionCallData,
}

Step 4: Create UserOperation

Now the fun part. Call createUserOperation, which will:

  1. Prepare the eip7702Auth tuple for signature authorization. This is used during the authorization transaction of the EOA.
  2. Determine the nonce and fetch the gas prices from the provided node RPC
  3. Estimate gas limits from the provided bundler

This returns an unsigned user operation. Use calculateUserOperationMaxGasCost to calculate its cost.

index.ts
import {
// ...
calculateUserOperationMaxGasCost
} from "abstractionkit";

const bundlerUrl = process.env.BUNDLER_URL as string;
const chainId = BigInt(process.env.CHAIN_ID as string);
const eoaDelegatorPrivateKey = process.env.PRIVATE_KEY as string;
const jsonRpcNodeProvider = process.env.JSON_RPC_NODE_PROVIDER as string;

let userOperation = await smartAccount.createUserOperation(
[transaction1, transaction2],
jsonRpcNodeProvider,
bundlerUrl,
{
eip7702Auth:{
chainId, // chainId at which the account will be authorized
}
}
);

const cost = calculateUserOperationMaxGasCost(userOperation)
console.log("This useroperation may cost up to : " + cost + " wei")
console.log("Please fund the sender account : " + userOperation.sender +" with more than " + cost + " wei")

Step 5: Sign the Authorization

Sign the eip7702Auth tuple to authorize the smart account code at your EOA address. This is called during the authorization transaction of the EOA.

import { createAndSignEip7702DelegationAuthorization } from "abstractionkit"

userOperation.eip7702Auth = createAndSignEip7702DelegationAuthorization(
BigInt(userOperation.eip7702Auth.chainId),
userOperation.eip7702Auth.address,
BigInt(userOperation.eip7702Auth.nonce),
eoaDelegatorPrivateKey
)

Step 6: Get Paymaster data (Optional)

Optionally sponsor gas for your user transaction or offer them to pay gas in erc-20 tokens.

import { CandidePaymaster } from "abstractionkit";

const paymasterUrl = process.env.PAYMASTER_URL as string;

const paymaster = new CandidePaymaster(paymasterUrl)

let [paymasterUserOperation, _sponsorMetadata] = await paymaster.createSponsorPaymasterUserOperation(
userOperation,
bundlerUrl,
);

userOperation = paymasterUserOperation;

Step 7: Sign and Submit

  1. Call signUserOperation, which will create a signature for the private key provided of the owner of the EOA.
index.ts
const privateKey = process.env.PRIVATE_KEY as string;

userOperation.signature = smartAccount.signUserOperation(
userOperation,
eoaDelegator.privateKey,
chainId,
);
  1. Use the Bundler URL to send the user operation to the bundler with sendUserOperation, and await the return SendUseroperationResponse object to confirm the on-chain inclusion of the user operation.
index.ts
const sendUserOperationResponse = await smartAccount.sendUserOperation(userOperation, bundlerUrl)

console.log("UserOperation sent. Waiting to be included ......")
  1. Track the userOperation and wait for its inclusion onchain
let userOperationReceiptResult = await sendUserOperationResponse.included()

console.log("Useroperation receipt received.")
console.log(userOperationReceiptResult)

if (userOperationReceiptResult.success) {
console.log("EOA upgraded to a Smart Account and minted two NFTs! The transaction hash is : " + userOperationReceiptResult.receipt.transactionHash)
} else {
console.log("Useroperation execution failed")
}

Now let's run this code again

Terminal
npx ts-node index.ts

You've now enabled smart account features for your EOA! If everything went well, you should see the bundler returning a user operation receipt.

Result
Useroperation sent. Waiting to be included ......
Useroperation receipt received.
{
userOpHash: '0x89a5111d40c4ca45977a28419a08ca33e496a88e973bc995ec6a5a28da564cb5',
entryPoint: '0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108',
sender: '0xbdbc5fbc9ca8c3f514d073ec3de840ac84fc6d31',
nonce: 0n,
paymaster: '0x0000000000000000000000000000000000000000',
actualGasCost: 243581295447n,
actualGasUsed: 84429n,
success: true,
// ...
receipt: {
// ...
transactionHash: '0x763dc353dee853da059b9e8c4b9997cccd4597b4cfcfc5fc3133dcffc778d93a',
// ...
}
}
EOA upgraded to a Smart Account and minted two NFTs! The transaction hash is : 0x763dc353dee853da059b9e8c4b9997cccd4597b4cfcfc5fc3133dcffc778d93a

You can look up the hash on explorers that support user operations like Blockscout.

Full Example

Below is the complete example code that you can copy directly to implement the functionality described in the tutorial.

index.ts
import * as dotenv from 'dotenv'
import {
Simple7702Account,
getFunctionSelector,
createCallData,
createAndSignEip7702DelegationAuthorization,
CandidePaymaster,
} from "abstractionkit";
import { Wallet } from 'ethers';

async function main(): Promise<void> {
//get values from .env
dotenv.config()
const chainId = BigInt(process.env.CHAIN_ID as string)
const bundlerUrl = process.env.BUNDLER_URL as string
const jsonRpcNodeProvider = process.env.JSON_RPC_NODE_PROVIDER as string;
const paymasterUrl = process.env.PAYMASTER_URL as string;

const eoaDelegator = Wallet.createRandom();
const eoaDelegatorPublicAddress = eoaDelegator.address;
const eoaDelegatorPrivateKey = eoaDelegator.privateKey;

// initiate the smart account
const smartAccount = new Simple7702Account(eoaDelegatorPublicAddress);

// We will be minting two random NFTs in a single tx
const nftContractAddress = "0x9a7af758aE5d7B6aAE84fe4C5Ba67c041dFE5336";
const mintFunctionSignature = 'mint(address)';
const mintFunctionSelector = getFunctionSelector(mintFunctionSignature);
const mintTransactionCallData = createCallData(
mintFunctionSelector,
["address"],
[smartAccount.accountAddress]
);
const transaction1 = {
to: nftContractAddress,
value: 0n,
data: mintTransactionCallData,
}

const transaction2 = {
to: nftContractAddress,
value: 0n,
data: mintTransactionCallData,
}

let userOperation = await smartAccount.createUserOperation(
[transaction1, transaction2],
jsonRpcNodeProvider,
bundlerUrl,
{
eip7702Auth:{
chainId: chainId, // chainId at which the account will be authorized
}
}
);

userOperation.eip7702Auth = createAndSignEip7702DelegationAuthorization(
BigInt(userOperation.eip7702Auth.chainId),
userOperation.eip7702Auth.address,
BigInt(userOperation.eip7702Auth.nonce),
eoaDelegatorPrivateKey
)

let paymaster: CandidePaymaster = new CandidePaymaster(
paymasterUrl
)

let [paymasterUserOperation, _sponsorMetadata] = await paymaster.createSponsorPaymasterUserOperation(
userOperation, bundlerUrl)
userOperation = paymasterUserOperation;

userOperation.signature = smartAccount.signUserOperation(
userOperation,
eoaDelegatorPrivateKey,
chainId,
);

let sendUserOperationResponse = await smartAccount.sendUserOperation(
userOperation, bundlerUrl
);

console.log("userOperation: ", userOperation)
console.log("userOp sent! Waiting for inclusion...");
console.log("userOp Hash: ", sendUserOperationResponse.userOperationHash);

let userOperationReceiptResult = await sendUserOperationResponse.included();

console.log("Useroperation receipt received.")
console.log(userOperationReceiptResult)
if (userOperationReceiptResult.success) {
console.log("EOA upgraded to a Smart Account and minted two NFTs! The transaction hash is : " + userOperationReceiptResult.receipt.transactionHash)
} else {
console.log("Useroperation execution failed")
}
}

main()