EIP-7702 Quickstart
EIP-7702 upgrades current Ethereum External Owned Accounts (EOAs) by upgrading them to smart contract accounts
- 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.
While EIP-7702 brings significant improvements to Ethereum EOA wallets, it's essential to understand the full picture:
- Private key access remains: The original key will always maintain admin access to the upgraded account, limiting the security benefits an EOA wallet can offer.
- Portability limitations: Upgraded EOAs to Smart Accounts are tied to a specific wallet, and transferring to a new wallet will be a security-critical operation, unless all wallets adopt the same smart contracts (which is unlikely).
To learn more about EIP-7702, visit the dedicated 7702 Overview.
In this example, you will learn how to upgrade an EOA to a Smart Account to leverage features such as batching and gas sponsorship
Prerequisites
- Node and a package manager (yarn or npm)
- Holesky ETH from Faucets. Shoot us on discord if you need some
Here's the complete code for you to reference if you prefer to run directly.
Step 1: Get setup
- Create a new directory for your project:
mkdir candide-eip7702-upgrade-eoa
cd candide-eip7702-upgrade-eoa
yarn init
- Install required dependencies
yarn add abstractionkit@0.2.14 dotenv
abstractionkit@0.2.14
is experimental version release with EIP-7702 before the official upgrade in Pectra.
- Configure Environment Variables
- Create a
.env
file and add the following environment variables with your own values
CHAIN_ID=17000
JSON_RPC_NODE_PROVIDER=https://ethereum-holesky-rpc.publicnode.com
BUNDLER_URL=https://holesky.voltaire.candidewallet.com/rpc
#EOA public & private key
PRIVATE_KEY=PRIVATE_KEY
PUBLIC_ADDRESS=PUBLIC_KEY
- Create a empty file and a function to run our script
async function main(): Promise<void> {
// Rest of the code will go here...
}
main();
Step 2: Fund your EOA
Let's open up index.ts
We will need to fund the account with native tokens to execute the authorization tx, also know as the delegation to the smart account. We will write some simple checks to verify balance of the EOA before continuing to the next steps.
import * as dotenv from "dotenv";
import { sendJsonRpcRequest } from "abstractionkit";
async function main(): Promise<void> {
//get values from .env
dotenv.config();
const jsonRpcNodeProvider = process.env.JSON_RPC_NODE_PROVIDER as string;
const eoaDelegatorPublicKey = process.env.PUBLIC_ADDRESS as string;
const balance = await sendJsonRpcRequest(
jsonRpcNodeProvider,
"eth_getBalance",
[eoaDelegatorPublicKey, "latest",]
) as string;
if (BigInt(balance) === 0n) {
console.log("Please fund the EOA Address with a sufficient balance of the native token to proceed");
console.log("Address: ", eoaDelegatorPublicKey);
return;
}
}
main();
Step 3: Generate Account Address
To generate an account address, we will be using Simple7702Account, a minimal account that features batching and gas sponsorship.
import { Simple7702Account } from "abstractionkit";
const smartAccount = new Simple7702Account(eoaDelegatorPublicKey);
console.log("Account address(sender) : " + smartAccount.accountAddress);
Let's run this code
npx ts-node index.ts
If everything worked, you will get the calculate address of the smart account in the console.
Details
Result example
Account address(sender): 0x32afdcfa1e3bfe70d03ecb55b5c8045c26515c9dStep 3: Generate the callData
Not only we can upgrade the EOA to a smart account, we will also be demonstrating how to execute minting two NFTs, all in the same transaction.
import {
MetaTransaction,
getFunctionSelector,
createCallData,
} from "abstractionkit";
// We will be mitting 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:
- Compute the r and s values for the
eip7702auth
. These values are part of signature authorization tuple needed to upgrade the EOA - Determine the nonce and fetch the gas prices from the provided node rpc
- Estimate gas limits from the provided bundler
This returns a unsigned user operation. Use calculateUserOperationMaxGasCost
to calculate its cost.
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;
let userOperation = await smartAccount.createUserOperation(
[transaction1, transaction2],
jsonRpcNodeProvider,
bundlerUrl,
{
eoaDelegatorPrivateKey, // needed to comupte the r, s signature values of the eip7702auth
eoaDelegatorChainId: chainId, // chainId at which the account will be upgraded, needed for the eip7702auth
// maxPriorityFeePerGas: 0x4b02333en, // uncomment to override the gas prices if holeskey is unstable
}
);
const cost = calculateUserOperationMaxGasCost(userOperation)
console.log("This useroperation may cost upto : " + cost + " wei")
console.log("Please fund the sender account : " + userOperation.sender +" with more than " + cost + " wei")
Step 5: Sign and Submit
- Call
signUserOperation
, which will create a signature for the private key provided of the owner of the EOA.
const privateKey = process.env.PRIVATE_KEY as string;
userOperation.signature = smartAccount.signUserOperation(
userOperation,
eoaDelegatorPrivateKey,
chainId,
);
- Use the Bundler URL to send the userop to the bundler with
sendUserOperation
, and await the returnSendUseroperationResponse
object to confirm the on-chain inclusion of the user operation.
const sendUserOperationResponse = await smartAccount.sendUserOperation(userOperation, bundlerUrl)
console.log("UserOperation sent. Waiting to be included ......")
- 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
npx ts-node index.ts
You've now sent upgrade your EOA to a Smart Account! 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: '0x0000000071727De22E5E9d8BAf0edAc6f37da032',
sender: '0xbdbc5fbc9ca8c3f514d073ec3de840ac84fc6d31',
nonce: 0n,
paymaster: '0x0000000000000000000000000000000000000000',
actualGasCost: 243581295447n,
actualGasUsed: 84429n,
success: true,
logs: '[{"address":"0x0000000071727de22e5e9d8baf0edac6f37da032","topics":["0x49628fd1471006c1482da88028e9ce4dbb080b815c9b0344d39e5a8e6ec1419f","0x89a5111d40c4ca45977a28419a08ca33e496a88e973bc995ec6a5a28da564cb5","0x000000000000000000000000bdbc5fbc9ca8c3f514d073ec3de840ac84fc6d31","0x0000000000000000000000000000000000000000000000000000000000000000"],"data":"0x0000000000000000000000000000000000000000000000000000000000000015000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000038b6939b5700000000000000000000000000000000000000000000000000000000000149cd","blockHash":"0x51e49209271f31b831f466c256e971053f8d65fb71160a6da36277b56270a74c","blockNumber":"0x347611","blockTimestamp":"0x67c57b18","transactionHash":"0x763dc353dee853da059b9e8c4b9997cccd4597b4cfcfc5fc3133dcffc778d93a","transactionIndex":"0x8","logIndex":"0x3e","removed":false}]',
receipt: {
blockHash: '0x51e49209271f31b831f466c256e971053f8d65fb71160a6da36277b56270a74c',
blockNumber: 3438097n,
from: '0x6eb0296e64fb8d9d946c7b819e4ff55c7167b0ce',
cumulativeGasUsed: 884430n,
gasUsed: 96887n,
logs: '[{"address":"0x0000000071727de22e5e9d8baf0edac6f37da032","topics":["0x2da466a7b24304f47e87fa2e1e5a81b9831ce54fec19055ce277ca2f39ba42c4","0x000000000000000000000000bdbc5fbc9ca8c3f514d073ec3de840ac84fc6d31"],"data":"0x000000000000000000000000000000000000000000000000000000627c080142","blockHash":"0x51e49209271f31b831f466c256e971053f8d65fb71160a6da36277b56270a74c","blockNumber":"0x347611","blockTimestamp":"0x67c57b18","transactionHash":"0x763dc353dee853da059b9e8c4b9997cccd4597b4cfcfc5fc3133dcffc778d93a","transactionIndex":"0x8","logIndex":"0x3c","removed":false},{"address":"0x0000000071727de22e5e9d8baf0edac6f37da032","topics":["0xbb47ee3e183a558b1a2ff0874b079f3fc5478b7454eacf2bfc5af2ff5878f972"],"data":"0x","blockHash":"0x51e49209271f31b831f466c256e971053f8d65fb71160a6da36277b56270a74c","blockNumber":"0x347611","blockTimestamp":"0x67c57b18","transactionHash":"0x763dc353dee853da059b9e8c4b9997cccd4597b4cfcfc5fc3133dcffc778d93a","transactionIndex":"0x8","logIndex":"0x3d","removed":false},{"address":"0x0000000071727de22e5e9d8baf0edac6f37da032","topics":["0x49628fd1471006c1482da88028e9ce4dbb080b815c9b0344d39e5a8e6ec1419f","0x89a5111d40c4ca45977a28419a08ca33e496a88e973bc995ec6a5a28da564cb5","0x000000000000000000000000bdbc5fbc9ca8c3f514d073ec3de840ac84fc6d31","0x0000000000000000000000000000000000000000000000000000000000000000"],"data":"0x0000000000000000000000000000000000000000000000000000000000000015000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000038b6939b5700000000000000000000000000000000000000000000000000000000000149cd","blockHash":"0x51e49209271f31b831f466c256e971053f8d65fb71160a6da36277b56270a74c","blockNumber":"0x347611","blockTimestamp":"0x67c57b18","transactionHash":"0x763dc353dee853da059b9e8c4b9997cccd4597b4cfcfc5fc3133dcffc778d93a","transactionIndex":"0x8","logIndex":"0x3e","removed":false}]',
logsBloom: '0x00000000000000000000000000000400000000000000000000000000000000000008000000000000000800010000000000000000000000000000020000000000000000000000000000000000000000000040000000000000000000000000200000000000020801000000000000000800000000000000000000000000000200000000000000000000000000000000000000000000000000008000000000000000000000000000000000400000000800000000000000000000000002000000000000000000000000410001000000000000000000000000000000000000000020000040000000000000000000000000000000000000000000000000000000000000',
transactionHash: '0x763dc353dee853da059b9e8c4b9997cccd4597b4cfcfc5fc3133dcffc778d93a',
transactionIndex: 8n,
effectiveGasPrice: 2644614n
}
}
EOA upgraded to a Smart Account and minted two Nfts! The transaction hash is : 0x763dc353dee853da059b9e8c4b9997cccd4597b4cfcfc5fc3133dcffc778d93a
You can lookup the hash on explorers that supports user operations like Blockscout.
In the next guides, we will show how to use sponsored gas policies using paymasters.
Full Example
Below is the complete example code that you can copy directly to implement the functionality described in the tutorial.
index.js
import * as dotenv from 'dotenv'
import {
Simple7702Account,
getFunctionSelector,
createCallData,
sendJsonRpcRequest,
} from "abstractionkit";
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 eoaDelegatorPublicKey = process.env.PUBLIC_ADDRESS as string;
const eoaDelegatorPrivateKey = process.env.PRIVATE_KEY as string;
// check balance of EOA before executing the upgrade userOp
const balance = await sendJsonRpcRequest(
jsonRpcNodeProvider,
"eth_getBalance",
[eoaDelegatorPublicKey, "latest",]
) as string;
if (BigInt(balance) === 0n) {
console.log("Please fund the EOA Address with a sufficient balance of the native token to proceed");
console.log("Address: ", eoaDelegatorPublicKey);
return;
}
// initiate the smart account
const smartAccount = new Simple7702Account(eoaDelegatorPublicKey);
// We will be mitting two random NFTs in a single txs
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(
[
//You can batch multiple transactions to be executed in one useroperation.
transaction1, transaction2,
],
jsonRpcNodeProvider, //the node rpc is used to fetch the current nonce and fetch gas prices.
bundlerUrl, //the bundler rpc is used to estimate the gas limits.
{
eoaDelegatorPrivateKey, // needed to comupte the r, s values of the eip7702auth
eoaDelegatorChainId: chainId, // chainId at which the account will be upgraded
// maxPriorityFeePerGas: 0x4b02333en, // uncomment to override the gas prices if holeskey is unstable
}
);
userOperation.signature = smartAccount.signUserOperation(
userOperation,
eoaDelegatorPrivateKey,
chainId,
);
console.log("userOperation: ", userOperation)
let sendUserOperationResponse = await smartAccount.sendUserOperation(
userOperation, bundlerUrl
);
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()