Skip to main content

Getting Started

Learn how to create a smart wallet and to send your first user operation

Candide builds open-source software for ERC-4337 account abstraction standard. This guarantees smooth compatibility with any platform that follow the ERC-4337 protocol, fostering effortless integration, and providing the flexibility of independent hosting to avoid any vendor lock-in.

Quickstart

Prerequisites

  • Node and a package manager (yarn or npm)
  • Some Sepolia ETH
  • If you need, the full example of this tutorial on Github

Step 1: Get setup

  1. Create a new directory for your project:
mkdir safe-batch-mint
cd safe-batch-mint
  1. Install required dependencies
yarn add abstractionkit
  1. Configure Environment Variables
  • Create a .env file in ./src and add the following environment variables with your own values
  • We will be using Sepolia Testnet for this example
  • yarn add dotenv to load environment variables from the .env file
.env
CHAIN_ID=11155111
BUNDLER_URL=https://sepolia.test.voltaire.candidewallet.com/rpc
JSON_RPC_NODE_PROVIDER=NODE_PROVIDER_URL

#account owner pub & private key
PRIVATE_KEY=YOUR_PRIVATE_KEY
PUBLIC_ADDRESS=YOUR_PUBLIC_KEY
info

We host free bundlers endpoints, you do not need to create an account.

  1. Create a empty file and a function to run our script
index.ts
async function main(): Promise<void> {
// Rest of the code will go here...
}

main();

Step 2: Generate Account Address

Let's open up index.ts

To generate an account address, we will be using the Safe Module/Fallback as our base smart account. An extension to the Safe contract that implements the ERC4337 interface. We will control the Account with a single EOA owner.

Since this is our first transaction, we will need to deploy the account in the same transaction

index.ts
import * as dotenv from "dotenv";
import { SafeAccountV0_2_0 as SafeAccount } from "abstractionkit";

async function main(): Promise<void> {
//get values from .env
dotenv.config();

const ownerPublicAddress = process.env.PUBLIC_ADDRESS as string
const smartAccount = SafeAccount.initializeNewAccount(
[ownerPublicAddress],
)
console.log("Account address(sender) : " + smartAccount.accountAddress);
}

main();

Let's run this code with npx ts-node index.ts

If everything worked, you will get the address of the account in the console.

Terminal
Account address(sender): 0x32afdcfa1e3bfe70d03ecb55b5c8045c26515c9d
tip

Now that you have generated the smart account, go ahead and fund it with some Sepolia ETH here.

In another example, we will show you how to sponsor transactions with a paymaster.

Step 3: Generate the callData

We will be creating two transactions to mint 2 NFTs. You can use your fav library (like ethers or viem) to construct the calldata.

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

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 determine the nonce, fetch the gas prices, estimate gas limits and return a userop to be signed. Use calculateUserOperationMaxGasCost to calculate its cost.

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

const jsonRpcNodeProvider = process.env.JSON_RPC_NODE_PROVIDER as string
const bundlerUrl = process.env.BUNDLER_URL as string

let userOperation = await smartAccount.createUserOperation(
[transaction1, transaction2], // you can batch multiple transactions to be executed in one userop
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
)

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")
info

You can override any of the returned values using the overrides parameter.

Step 6: Sign and Submit

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

userOperation.signature = smartAccount.signUserOperation(
userOperation,
[privateKey],
chainId,
)
info

Since Safe is a multisig wallet, signUserOperation can take an array of private keys for multiple owners of the Account.

  1. Use the Bundler RPC to send the userop 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 its inclusion onchain
let userOperationReceiptResult = await sendUserOperationResponse.included()

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

if (userOperationReceiptResult.success) {
console.log("Two Nfts were minted. The transaction hash is : " + userOperationReceiptResult.receipt.transactionHash)
} else {
console.log("Useroperation execution failed")
}

Now let's run this code again with npx ts-node index.ts

You've now sent your first user operation! 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: '0x1acede61123ab7116eb29c797aeaec3c03615c37732ba66428524aebdb4b4514',
entryPoint: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789',
sender: '0xb8741a449d50ed0dcfe395287f85be152884c8d9',
nonce: 0n,
paymaster: '0x3fe285dcd76bcce4ac92d38a6f2f8e964041e020',
actualGasCost: 8078496n,
actualGasUsed: 504906n,
success: true,
logs: '[{"address":"0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789","blockHash":"0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c","blockNumber":"0x9efe5f","data":"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000007b44a0000000000000000000000000000000000000000000000000000000000007b44a","logIndex":"0x9a","removed":false,"topics":["0x49628fd1471006c1482da88028e9ce4dbb080b815c9b0344d39e5a8e6ec1419f","0x1acede61123ab7116eb29c797aeaec3c03615c37732ba66428524aebdb4b4514","0x000000000000000000000000b8741a449d50ed0dcfe395287f85be152884c8d9","0x0000000000000000000000003fe285dcd76bcce4ac92d38a6f2f8e964041e020"],"transactionHash":"0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa","transactionIndex":"0x1d"}]',
receipt: {
blockHash: '0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c',
blockNumber: 10419807n,
from: '0x3cfdc212769c890907bce93d3d8c2c53de6a7a89',
cumulativeGasUsed: 6978990n,
gasUsed: 507053n,
logs: '[{"address":"0xb8741a449d50ed0dcfe395287f85be152884c8d9","blockHash":"0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c","blockNumber":"0x9efe5f","data":"0x","logIndex":"0x90","removed":false,"topics":["0xecdf3a3effea5783a3c4c2140e677577666428d44ed9d474a0b3a4c9943f8440","0x000000000000000000000000a581c4a4db7175302464ff3c06380bc3270b4037"],"transactionHash":"0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa","transactionIndex":"0x1d"},{"address":"0xb8741a449d50ed0dcfe395287f85be152884c8d9","blockHash":"0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c","blockNumber":"0x9efe5f","data":"0x000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008ecd4ec46d4d2a6b64fe960b3d64e8b94b2234eb000000000000000000000000a581c4a4db7175302464ff3c06380bc3270b40370000000000000000000000000000000000000000000000000000000000000001000000000000000000000000bdbc5fbc9ca8c3f514d073ec3de840ac84fc6d31","logIndex":"0x91","removed":false,"topics":["0x141df868a6331af528e38c83b7aa03edc19be66e37ae67f9285bf4f8e3c6a1a8","0x0000000000000000000000004e1dcf7ad4e460cfd30791ccc4f9c8a4f820ec67"],"transactionHash":"0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa","transactionIndex":"0x1d"},{"address":"0x4e1dcf7ad4e460cfd30791ccc4f9c8a4f820ec67","blockHash":"0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c","blockNumber":"0x9efe5f","data":"0x00000000000000000000000029fcb43b46531bca003ddc8fcb67ffe91900c762","logIndex":"0x92","removed":false,"topics":["0x4f51faf6c4561ff95f067657e43439f0f856d97c04d9ec9070a6199ad418e235","0x000000000000000000000000b8741a449d50ed0dcfe395287f85be152884c8d9"],"transactionHash":"0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa","transactionIndex":"0x1d"},{"address":"0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789","blockHash":"0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c","blockNumber":"0x9efe5f","data":"0x0000000000000000000000004e1dcf7ad4e460cfd30791ccc4f9c8a4f820ec670000000000000000000000003fe285dcd76bcce4ac92d38a6f2f8e964041e020","logIndex":"0x93","removed":false,"topics":["0xd51a9c61267aa6196961883ecf5ff2da6619c37dac0fa92122513fb32c032d2d","0x1acede61123ab7116eb29c797aeaec3c03615c37732ba66428524aebdb4b4514","0x000000000000000000000000b8741a449d50ed0dcfe395287f85be152884c8d9"],"transactionHash":"0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa","transactionIndex":"0x1d"},{"address":"0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789","blockHash":"0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c","blockNumber":"0x9efe5f","data":"0x","logIndex":"0x94","removed":false,"topics":["0xbb47ee3e183a558b1a2ff0874b079f3fc5478b7454eacf2bfc5af2ff5878f972"],"transactionHash":"0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa","transactionIndex":"0x1d"},{"address":"0xb8741a449d50ed0dcfe395287f85be152884c8d9","blockHash":"0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c","blockNumber":"0x9efe5f","data":"0x000000000000000000000000a581c4a4db7175302464ff3c06380bc3270b403700000000000000000000000038869bf66a61cf6bdb996a6ae40d5853fd43b526000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001048d80ff0a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000b200d9de104e3386d9a45a61bce269c43e48b534e4e7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041249c58b00d9de104e3386d9a45a61bce269c43e48b534e4e7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041249c58b000000000000000000000000000000000000000000000000000000000000000000000000000000000000","logIndex":"0x95","removed":false,"topics":["0xb648d3644f584ed1c2232d53c46d87e693586486ad0d1175f8656013110b714e"],"transactionHash":"0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa","transactionIndex":"0x1d"},{"address":"0xd9de104e3386d9a45a61bce269c43e48b534e4e7","blockHash":"0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c","blockNumber":"0x9efe5f","data":"0x","logIndex":"0x96","removed":false,"topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000b8741a449d50ed0dcfe395287f85be152884c8d9","0x0000000000000000000000000000000000000000000000000000000000000056"],"transactionHash":"0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa","transactionIndex":"0x1d"},{"address":"0xd9de104e3386d9a45a61bce269c43e48b534e4e7","blockHash":"0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c","blockNumber":"0x9efe5f","data":"0x","logIndex":"0x97","removed":false,"topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000b8741a449d50ed0dcfe395287f85be152884c8d9","0x0000000000000000000000000000000000000000000000000000000000000057"],"transactionHash":"0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa","transactionIndex":"0x1d"},{"address":"0xb8741a449d50ed0dcfe395287f85be152884c8d9","blockHash":"0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c","blockNumber":"0x9efe5f","data":"0x","logIndex":"0x98","removed":false,"topics":["0x6895c13664aa4f67288b25d7a21d7aaa34916e355fb9b6fae0a139a9085becb8","0x000000000000000000000000a581c4a4db7175302464ff3c06380bc3270b4037"],"transactionHash":"0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa","transactionIndex":"0x1d"},{"address":"0x3fe285dcd76bcce4ac92d38a6f2f8e964041e020","blockHash":"0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c","blockNumber":"0x9efe5f","data":"0x0000000000000000000000000000000000000000000000000000000000000000","logIndex":"0x99","removed":false,"topics":["0xa050a122b4c0e369e3385eb6b7cccd8019638b2764de67bec0af99130ddf8471","0x1acede61123ab7116eb29c797aeaec3c03615c37732ba66428524aebdb4b4514","0x000000000000000000000000b8741a449d50ed0dcfe395287f85be152884c8d9","0x0000000000000000000000000000000000000000000000000000000000000000"],"transactionHash":"0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa","transactionIndex":"0x1d"},{"address":"0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789","blockHash":"0xe19e52b4c222c1cdbc765f1a4e196ff4bf40c5550926e02974570e1845e88e2c","blockNumber":"0x9efe5f","data":"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000007b44a0000000000000000000000000000000000000000000000000000000000007b44a","logIndex":"0x9a","removed":false,"topics":["0x49628fd1471006c1482da88028e9ce4dbb080b815c9b0344d39e5a8e6ec1419f","0x1acede61123ab7116eb29c797aeaec3c03615c37732ba66428524aebdb4b4514","0x000000000000000000000000b8741a449d50ed0dcfe395287f85be152884c8d9","0x0000000000000000000000003fe285dcd76bcce4ac92d38a6f2f8e964041e020"],"transactionHash":"0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa","transactionIndex":"0x1d"}]',
logsBloom: '0x0000040000009000000000000000010080000000000000000000000000000000000800000000000000020001000004040010000000000000800002000000000000001000000000000000000c0002000000000000010000080040000000000000020000000a0000000500002000000800008000000100000000000014000000000800010020000200008000000040000000000200000400000000000000000000000004000000000000500000000004000210000000000000000002001000000020200082000000000001000008000000000000002060000000100000000026000000082000010000000000000008100220000000000000000000000010000200',
transactionHash: '0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa',
transactionIndex: 29n,
effectiveGasPrice: 16n
}
}
Two Nfts were minted. The transaction hash is : 0xf43576a07f39660737a342c99a187eb70ac59d89bc0df92ff3c1bb8a8da370aa

You can lookup the user operation hash on a dedicated ERC-4337 explorer like JiffyScan.

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 {
SafeAccountV0_2_0 as SafeAccount,
MetaTransaction,
calculateUserOperationMaxGasCost,
getFunctionSelector,
createCallData,
} 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 ownerPublicAddress = process.env.PUBLIC_ADDRESS as string
const ownerPrivateKey = process.env.PRIVATE_KEY as string

//initializeNewAccount only needed when the smart account
//have not been deployed yet for its first useroperation.
//You can store the accountAddress to use it to initialize
//the SafeAccount object for the following useroperations
let smartAccount = SafeAccount.initializeNewAccount(
[ownerPublicAddress],
)

//After the account contract is deployed, no need to call initializeNewAccount
//let smartAccount = new SafeAccount(accountAddress)

console.log("Account address(sender) : " + smartAccount.accountAddress)

//create two meta transaction to mint two NFTs
//you can use favorite method (like ethers.js) to construct the call data
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,
}

//createUserOperation will determine the nonce, fetch the gas prices,
//estimate gas limits and return a useroperation to be signed.
//you can override all these values using the overrides parameter.
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.
)

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"
)

//Safe is a multisig that can have multiple owners/signers
//signUserOperation will create a signature for the provided
//privateKeys
userOperation.signature = smartAccount.signUserOperation(
userOperation,
[ownerPrivateKey],
chainId
)
console.log(userOperation)

//use the bundler rpc to send a userOperation
//sendUserOperation will return a SendUseroperationResponse object
//that can be awaited for the useroperation to be included onchain
const sendUserOperationResponse = await smartAccount.sendUserOperation(
userOperation, bundlerUrl
)

console.log("Useroperation sent. Waiting to be included ......")
//included will return a UserOperationReceiptResult when
//useroperation is included onchain
let userOperationReceiptResult = await sendUserOperationResponse.included()

console.log("Useroperation receipt received.")
console.log(userOperationReceiptResult)
if(userOperationReceiptResult.success){
console.log("Two Nfts were minted. The transaction hash is : " + userOperationReceiptResult.receipt.transactionHash)
}else{
console.log("Useroperation execution failed")
}
}

main()