Skip to main content

How to Support Gas Payment in ERC-20 Tokens

Learn how to offer the ability for users to pay gas in ERC-20s using a Paymaster.

If you are coming from the previous guide, you only need adjust the paymaster step. You will need an API key from the dashboard, and then you can skip directly to step 5.

Quickstart

Prerequisites

  • Node and a package manager (yarn or npm)
  • Bundler and Paymaster API key from the Candide Dashboard
  • Some Candide Test Tokens (CTT). Request CTT on our Discord
  • If you need, fork the full example of this tutorial on on Github

Step 1: Get setup

  1. Create a new directory for your project:
mkdir smart-wallet-pay-gas-erc20
cd smart-wallet-pay-gas-erc20
yarn init
  1. Install required dependencies
yarn add abstractionkit dotenv
  1. Configure Environment Variables
  • Create a .env file and add the following environment variables with your own values
  • We will be using Sepolia Testnet for this example
.env
CHAIN_ID=11155111
BUNDLER_URL=https://api.candide.dev/bundler/version/network/YOUR_API_KEY
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 Bundler endpoints on testnets. 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_3_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

Terminal
npx ts-node index.ts

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

Result example
Account address(sender): 0x32afdcfa1e3bfe70d03ecb55b5c8045c26515c9d

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_3_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.

index.ts
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
)

Step 5: Get Paymaster Data

  1. Import CandidePaymaster, and initiate an instance with its URL. You can get a free API key on the dashboard.
import {
...
CandidePaymaster,
} from "abstractionkit";

const paymasterRPC = process.env.PAYMASTER_RPC as string;
const tokenAddress = process.env.TOKEN_ADDRESS as string;

const paymaster: CandidePaymaster = new CandidePaymaster(paymasterRPC)

userOperation = await paymaster.createTokenPaymasterUserOperation(
smartAccount,
userOperation,
tokenAddress,
bundlerUrl,
)

const cost = await paymaster.calculateUserOperationErc20TokenMaxGasCost(
userOperation,
tokenAddress
)

console.log("This useroperation may cost upto : " + cost + " wei in CTT token")
console.log("Please fund the sender account : " + userOperation.sender +" with more than "+ cost + " wei CTT token")
info

Don't forget to fund the account with ERC-20s to pay for gas. We are using Candide Test Tokens (CTT) for this example. Request some on our Discord.

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,
)
  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 for 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

Terminal
npx ts-node index.ts

You've now sent your first user operation with a token paymaster! If everything went well, you should see the bundler returning a 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

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_3_0 as SafeAccount,
MetaTransaction,
CandidePaymaster,
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
const paymasterRPC = process.env.PAYMASTER_RPC as string;
const paymasterTokenAddress = process.env.PAYMASTER_TOKEN_ADDRESS 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.
)

let paymaster: CandidePaymaster = new CandidePaymaster(
paymasterRPC,
)

userOperation = await paymaster.createTokenPaymasterUserOperation(
smartAccount,
userOperation,
paymasterTokenAddress,
bundlerUrl,
)

const cost = await paymaster.calculateUserOperationErc20TokenMaxGasCost(
userOperation,
paymasterTokenAddress
)
console.log("This useroperation may cost upto : " + cost + " wei in CTT token")
console.log(
"Please fund the sender account : " +
userOperation.sender +
" with more than "+ cost + " wei CTT token"
)
console.log("This example uses a Candide token paymaster.")
console.log("Please visit https://dashboard.candide.dev/ to get a token paymaster url.")
console.log("Please visit our Discord to get some CTT token for testing")

//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()