Skip to main content
Version: SDK V2

Paying for gas with ERC20 Tokens

In this guide, we will edit the functionality in the gasless transaction section to switch from minting an NFT with no gas costs for the user, to instead minting the NFT and using USDC to pay for the gas costs.

For this tutorial you will need some test USDC tokens. You can use Uniswap while connected to the Polygon Mumbai network in your EOA to swap test Matic for USDC POS tokens which are one of the supported tokens on test nets for our SDK. You can get additional test matic at the polygon faucet.

Make sure to swap for Polygon Mumbai USDC test tokens at this address:

0xda5289fcaaf71d52a80a254da614a192b693e977 on Uniswap

Click to view code from previous section

import { config } from "dotenv";
import { IBundler, Bundler } from "@biconomy/bundler";
import { ChainId } from "@biconomy/core-types";
import {
BiconomySmartAccountV2,
DEFAULT_ENTRYPOINT_ADDRESS,
} from "@biconomy/account";
import {
ECDSAOwnershipValidationModule,
DEFAULT_ECDSA_OWNERSHIP_MODULE,
} from "@biconomy/modules";
import { ethers } from "ethers";
import {
IPaymaster,
BiconomyPaymaster,
IHybridPaymaster,
PaymasterMode,
SponsorUserOperationDto,
} from "@biconomy/paymaster";

config();

const provider = new ethers.providers.JsonRpcProvider(
"https://rpc.ankr.com/polygon_mumbai"
);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY || "", provider);

const bundler: IBundler = new Bundler({
bundlerUrl: "https://bundler.biconomy.io/api/v2/80001/nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f44",
chainId: ChainId.POLYGON_MUMBAI,
entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS,
});

const paymaster: IPaymaster = new BiconomyPaymaster({
paymasterUrl: "https://paymaster.biconomy.io/api/v1/80001/Tpk8nuCUd.70bd3a7f-a368-4e5a-af14-80c7f1fcda1a",
});

async function createAccount() {
const module = await ECDSAOwnershipValidationModule.create({
signer: wallet,
moduleAddress: DEFAULT_ECDSA_OWNERSHIP_MODULE,
});

let biconomyAccount = await BiconomySmartAccountV2.create({
chainId: ChainId.POLYGON_MUMBAI,
bundler: bundler,
paymaster: paymaster,
entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS,
defaultValidationModule: module,
activeValidationModule: module,
});
console.log("address", await biconomyAccount.getAccountAddress());
return biconomyAccount;
}


async function mintNFT() {
const smartAccount = await createAccount();
const address = await smartAccount.getAccountAddress();
const nftInterface = new ethers.utils.Interface([
"function safeMint(address _to)",
]);

const data = nftInterface.encodeFunctionData("safeMint", [address]);

const nftAddress = "0x1758f42Af7026fBbB559Dc60EcE0De3ef81f665e";

const transaction = {
to: nftAddress,
data: data,
};

let partialUserOp = await smartAccount.buildUserOp([transaction], {
paymasterServiceData: {
mode: PaymasterMode.SPONSORED,
},
});

const biconomyPaymaster =
smartAccount.paymaster as IHybridPaymaster<SponsorUserOperationDto>;

try {
const paymasterAndDataResponse =
await biconomyPaymaster.getPaymasterAndData(partialUserOp);
partialUserOp.paymasterAndData = paymasterAndDataResponse.paymasterAndData;
} catch (e) {
console.log("error received ", e);
}

try {
const userOpResponse = await smartAccount.sendUserOp(partialUserOp);
const transactionDetails = await userOpResponse.wait();
console.log(
`transactionDetails: https://mumbai.polygonscan.com/tx/${transactionDetails.receipt.transactionHash}`
);
console.log(
`view minted nfts for smart account: https://testnets.opensea.io/${address}`
);
} catch (e) {
console.log("error received ", e);
}
}

mintNFT();

Mint NFT starting point

In our mintNFT function (which you can view above) we are going to keep the initial set up of the contract connection but remove code specific to a gasless transaction. Here is a good starting point for the edit on this function:

async function mintNFT() {
await createAccount();
const nftInterface = new ethers.utils.Interface([
"function safeMint(address _to)",
]);

const data = nftInterface.encodeFunctionData("safeMint", [address]);

const nftAddress = "0x1758f42Af7026fBbB559Dc60EcE0De3ef81f665e";
0xda5289fcaaf71d52a80a254da614a192b693e977;

const transaction = {
to: nftAddress,
data: data,
};

console.log("creating nft mint userop");
let partialUserOp = await smartAccount.buildUserOp([transaction]);
}

Partial UserOP

We've got the connection to the contract as well as the partial userOp constructed. Our paymaster has two key features: helping sponsor transactions for the user, and allowing the user to pay for gas in ERC20 tokens. Let's get ourselves set up to have users pay for gas in USDC. Add the following lines:

let finalUserOp = partialUserOp;

const biconomyPaymaster =
smartAccount.paymaster as IHybridPaymaster<SponsorUserOperationDto>;

We will be manipulating our partialUserOp into finalUserOp before executing it.

ERC20 Fee Quotes

Now lets get fee quotes for our USDC token:

const feeQuotesResponse = await biconomyPaymaster.getPaymasterFeeQuotesOrData(
partialUserOp,
{
mode: PaymasterMode.ERC20,
tokenList: ["0xda5289fcaaf71d52a80a254da614a192b693e977"],
}
);

For the Paymaster Mode we are using ERC20 instead of SPONSORED and passing a token list with the USDC token address on mumbai. Note that if you pass an empty array you will recieve an array of all supported ERC20 tokens. Make sure to update your imports from the Paymaster to include PaymasterFeeQuote as well.

const feeQuotes = feeQuotesResponse.feeQuotes as PaymasterFeeQuote[];
const spender = feeQuotesResponse.tokenPaymasterAddress || "";
const usdcFeeQuotes = feeQuotes[0];

Now we get the response from the fee quotes and specify that the Paymaster will be the eventual spender for the transaction. The user will pay USDC and the Paymaster will pay in native tokens. Lastly since we specified the USDC address the only result in the array will be our USDC fee quote object.

Now let's update the userOp with our gathered information:

finalUserOp = await smartAccount.buildTokenPaymasterUserOp(partialUserOp, {
feeQuote: usdcFeeQuotes,
spender: spender,
maxApproval: false,
});

let paymasterServiceData = {
mode: PaymasterMode.ERC20,
feeTokenAddress: usdcFeeQuotes.tokenAddress,
calculateGasLimits: true, // Always recommended and especially when using token paymaster
};

try{
const paymasterAndDataWithLimits =
await biconomyPaymaster.getPaymasterAndData(
finalUserOp,
paymasterServiceData
);
finalUserOp.paymasterAndData = paymasterAndDataWithLimits.paymasterAndData;

// below code is only needed if you sent the flag calculateGasLimits = true
if (
paymasterAndDataWithLimits.callGasLimit &&
paymasterAndDataWithLimits.verificationGasLimit &&
paymasterAndDataWithLimits.preVerificationGas
) {

// Returned gas limits must be replaced in your op as you update paymasterAndData.
// Because these are the limits paymaster service signed on to generate paymasterAndData
// If you receive AA34 error check here..

finalUserOp.callGasLimit = paymasterAndDataWithLimits.callGasLimit;
finalUserOp.verificationGasLimit =
paymasterAndDataWithLimits.verificationGasLimit;
finalUserOp.preVerificationGas =
paymasterAndDataWithLimits.preVerificationGas;
}
} catch (e) {
console.log("error received ", e);
}

Paymaster Service Data response

Now similar to sponsored userOps we need to get the paymasterAndData field of the userOp:

try {
const paymasterAndDataWithLimits =
await biconomyPaymaster.getPaymasterAndData(
finalUserOp,
paymasterServiceData
);
finalUserOp.paymasterAndData = paymasterAndDataWithLimits.paymasterAndData;
} catch (e) {
console.log("error received ", e);
}

Execute UserOp

And finally we execute our userop!

try {
const userOpResponse = await smartAccount.sendUserOp(finalUserOp);
const transactionDetails = await userOpResponse.wait();
console.log(
`transactionDetails: https://mumbai.polygonscan.com/tx/${transactionDetails.logs[0].transactionHash}`
);
console.log(
`view minted nfts for smart account: https://testnets.opensea.io/${address}`
);
} catch (e) {
console.log("error received ", e);
}
Click to view final code

import { config } from "dotenv";
import { IBundler, Bundler } from "@biconomy/bundler";
import { ChainId } from "@biconomy/core-types";
import {
BiconomySmartAccountV2,
DEFAULT_ENTRYPOINT_ADDRESS,
} from "@biconomy/account";
import {
ECDSAOwnershipValidationModule,
DEFAULT_ECDSA_OWNERSHIP_MODULE,
} from "@biconomy/modules";
import { ethers } from "ethers";
import {
IPaymaster,
BiconomyPaymaster,
IHybridPaymaster,
PaymasterMode,
SponsorUserOperationDto,
PaymasterFeeQuote
} from "@biconomy/paymaster";

config();

const provider = new ethers.providers.JsonRpcProvider(
"https://rpc.ankr.com/polygon_mumbai"
);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY || "", provider);

const bundler: IBundler = new Bundler({
bundlerUrl: "https://bundler.biconomy.io/api/v2/80001/nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f44",
chainId: ChainId.POLYGON_MUMBAI,
entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS,
});

const paymaster: IPaymaster = new BiconomyPaymaster({
paymasterUrl: "https://paymaster.biconomy.io/api/v1/80001/Tpk8nuCUd.70bd3a7f-a368-4e5a-af14-80c7f1fcda1a",
});

async function createAccount() {
const module = await ECDSAOwnershipValidationModule.create({
signer: wallet,
moduleAddress: DEFAULT_ECDSA_OWNERSHIP_MODULE,
});

let biconomyAccount = await BiconomySmartAccountV2.create({
chainId: ChainId.POLYGON_MUMBAI,
bundler: bundler,
paymaster: paymaster,
entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS,
defaultValidationModule: module,
activeValidationModule: module,
});
console.log("address", await biconomyAccount.getAccountAddress());
return biconomyAccount;
}


async function mintNFT() {
const smartAccount = await createAccount();
const address = await smartAccount.getAccountAddress()
const nftInterface = new ethers.utils.Interface([
"function safeMint(address _to)",
]);

const data = nftInterface.encodeFunctionData("safeMint", [address]);

const nftAddress = "0x1758f42Af7026fBbB559Dc60EcE0De3ef81f665e";

const transaction = {
to: nftAddress,
data: data,
};

let partialUserOp = await smartAccount.buildUserOp([transaction]);

let finalUserOp = partialUserOp;

const biconomyPaymaster = smartAccount.paymaster as IHybridPaymaster<SponsorUserOperationDto>;

const feeQuotesResponse = await biconomyPaymaster.getPaymasterFeeQuotesOrData(
partialUserOp,
{
mode: PaymasterMode.ERC20,
tokenList: ["0xda5289fcaaf71d52a80a254da614a192b693e977"],
}
);

const feeQuotes = feeQuotesResponse.feeQuotes as PaymasterFeeQuote[];
const spender = feeQuotesResponse.tokenPaymasterAddress || "";
const usdcFeeQuotes = feeQuotes[0];

finalUserOp = await smartAccount.buildTokenPaymasterUserOp(partialUserOp, {
feeQuote: usdcFeeQuotes,
spender: spender,
maxApproval: false,
});

let paymasterServiceData = {
mode: PaymasterMode.ERC20,
feeTokenAddress: usdcFeeQuotes.tokenAddress,
calculateGasLimits: true, // Always recommended and especially when using token paymaster
};

try{
const paymasterAndDataWithLimits =
await biconomyPaymaster.getPaymasterAndData(
finalUserOp,
paymasterServiceData
);
finalUserOp.paymasterAndData = paymasterAndDataWithLimits.paymasterAndData;

// below code is only needed if you sent the flag calculateGasLimits = true
if (
paymasterAndDataWithLimits.callGasLimit &&
paymasterAndDataWithLimits.verificationGasLimit &&
paymasterAndDataWithLimits.preVerificationGas
) {

// Returned gas limits must be replaced in your op as you update paymasterAndData.
// Because these are the limits paymaster service signed on to generate paymasterAndData
// If you receive AA34 error check here..

finalUserOp.callGasLimit = paymasterAndDataWithLimits.callGasLimit;
finalUserOp.verificationGasLimit =
paymasterAndDataWithLimits.verificationGasLimit;
finalUserOp.preVerificationGas =
paymasterAndDataWithLimits.preVerificationGas;
}
} catch (e) {
console.log("error received ", e);
}

try {
const paymasterAndDataWithLimits =
await biconomyPaymaster.getPaymasterAndData(
finalUserOp,
paymasterServiceData
);
finalUserOp.paymasterAndData = paymasterAndDataWithLimits.paymasterAndData;
} catch (e) {
console.log("error received ", e);
}

try {
const userOpResponse = await smartAccount.sendUserOp(finalUserOp);
const transactionDetails = await userOpResponse.wait();
console.log(
`transactionDetails: https://mumbai.polygonscan.com/tx/${transactionDetails.logs[0].transactionHash}`
);
console.log(
`view minted nfts for smart account: https://testnets.opensea.io/${address}`
);
} catch (e) {
console.log("error received ", e);
}

}

mintNFT();

Congratulations you can now execute ERC20 gas payment transactions utilizing the Biconomy SDK! Now let's take things Multi chain!