Skip to main content

EIP-7702 Quickstart

EIP-7702 enables Ethereum External Owned Accounts (EOAs) to authorize smart contract code at their own address. This allows:

  • Gas sponsorship: abstract away gas fees, allowing third-party sponsorship or payment in ERC-20 tokens
  • Transaction batching: improve UX and security by combining approvals and contract interactions into a single step
  • Permissions: grant specific, limited access to third-parties, enabling use cases like recurring payments
  • Forward compatibility with end-game AA: Remains compatible with ERC-4337 and future native account abstraction.

To learn more about EIP-7702, visit the dedicated 7702 Overview.

In this example, you will learn how to upgrade your EOA to a Smart Account to leverage features such as batching and gas sponsorship.

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

To prepare the smart account instance, we will use Simple7702Account, a fully audited minimalist smart contract account that can be safely authorized by any EOA. It adds full support for major smart account features like 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

Not only can we upgrade the EOA to a smart account, we will also demonstrate how to execute minting two NFTs, all in the same 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()