Note: The USDC token has 6 decimals. To approve $100 USDC, set APPROVAL_AMOUNT
to 100000000 (100 × 10^6).
For Transactions V2, there is a dependency on the Permit2 contract to enable
allowance management. To get the benefits of Transactions V2, you must grant a
USDC token allowance to the Permit2 contract. This guide shows two examples of
how to grant a USDC token allowance to the Permit2 contract. The
Permit2 documentation
provides additional examples of how to grant this allowance.
The examples on this page show how to grant a USDC token allowance to the
Permit2 contract using a
Circle Wallets developer-controlled wallet or a
generic EIP-1193 Ethereum wallet. Before you begin, ensure you have:
If you are following the Circle Wallets example:
Node.js and npm installed on your development machine
A project set up as described in the below section
Initialize a new Node.js project and install dependencies:
npm init -y
npm pkg set type=module
npm install viem dotenv
In the project root, create a .env file and add the following variables:
USDC_CONTRACT_ADDRESS=<USDC_CONTRACT_ADDRESS>
PERMIT2_CONTRACT_ADDRESS=<PERMIT2_CONTRACT_ADDRESS>
APPROVAL_AMOUNT=<APPROVAL_AMOUNT>
WALLET_ADDRESS=<WALLET_ADDRESS>
Note: The USDC token has 6 decimals. To approve $100 USDC, set APPROVAL_AMOUNT
to 100000000 (100 × 10^6).
If you are following the Circle Wallets example, you will also need to add the following variables:
CIRCLE_WALLET_ID=<CIRCLE_WALLET_ID>
CIRCLE_WALLETS_API_KEY=<CIRCLE_WALLETS_API_KEY>
ENTITY_SECRET=<ENTITY_SECRET>
If you are following the EIP-1193 Ethereum wallet example, or your Circle Wallet
is on the generic EVM / EVM-TESTNET chain, you will also need to add the
following variable:
RPC_URL=<RPC_URL>
Create an index.js file. You'll add code step by step in the following
sections.
The following steps show how to grant a USDC token allowance to the Permit2
contract using a Circle Wallets developer-controlled wallet or an EIP-1193
Ethereum wallet.
Permit2 contractThe following example code shows the process for granting a USDC token allowance
to the Permit2 contract using a Circle Wallets developer-controlled wallet or
an EIP-1193 Ethereum wallet.
Note: This example is for a Circle Wallets developer-controlled wallet on
specific EVM blockchains (e.g. ETH, ETH-SEPOLIA, MATIC, MATIC-AMOY,
etc.). If your Circle Wallet is on the generic EVM / EVM-TESTNET chain,
which is likely the case if you are migrating from Transactions V1 to V2,
you can use the example in the "Circle Wallets (generic EVM)" tab.
import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets";
import { randomUUID } from "crypto";
import dotenv from "dotenv";
dotenv.config();
/**
* Approves a specified amount of USDC for the Permit2 contract using
* a developer-controlled wallet managed by Circle.
*
* This function sends an on-chain transaction to call the ERC-20 `approve`
* method on the USDC contract, allowing the Permit2 contract to spend
* the specified amount on behalf of the wallet.
*
* @async
* @function approveUSDCWithCircleWallets
* @returns {Promise<object>} The transaction response data returned by Circle's API.
*/
export async function approveUSDCWithCircleWallets() {
const client = initiateDeveloperControlledWalletsClient({
apiKey: process.env.CIRCLE_WALLETS_API_KEY,
entitySecret: process.env.ENTITY_SECRET,
});
const response = await client.createContractExecutionTransaction({
walletId: process.env.CIRCLE_WALLET_ID,
contractAddress: process.env.USDC_CONTRACT_ADDRESS,
abiFunctionSignature: "approve(address,uint256)",
abiParameters: [
process.env.PERMIT2_CONTRACT_ADDRESS,
process.env.APPROVAL_AMOUNT,
],
idempotencyKey: randomUUID(),
fee: { type: "level", config: { feeLevel: "MEDIUM" } },
});
return response.data;
}
/* -------- Example usage with Circle ---------
// For auth and wallet creation, see: https://developers.circle.com/interactive-quickstarts/dev-controlled-wallets
const response = await approveUSDCWithCircleWallets();
console.log('Response:', response);
---------------------------------- */
Note: This example is for a Circle Wallets developer-controlled wallet on
generic EVM blockchains (i.e. EVM, EVM-TESTNET). If you are getting started
with Transactions V2 instead of migrating from Transactions V1, we recommend
that you use create chain-specific Circle Wallets (i.e. ETH, ETH-SEPOLIA,
MATIC, MATIC-AMOY, etc.) instead of a generic EVM wallet. And follow the
example in the "Circle Wallets" tab.
import {
createPublicClient,
http,
encodeFunctionData,
erc20Abi,
} from "viem";
import { sepolia } from "viem/chains";
import dotenv from "dotenv";
dotenv.config();
/**
* Signs a transaction using a developer-controlled wallet managed by Circle.
*
* For EVM chains, accepts a transaction object in JSON format.
* The transaction object will be automatically stringified if needed.
*
* @async
* @function signTransaction
* @param {object} transaction - Transaction object for EVM chains
* @returns {Promise<object>} The signature response data returned by Circle's API
*
* @example
* const signature = await signTransaction({
* to: '0x...',
* value: '0x0',
* data: '0x...',
* nonce: 1,
* gasLimit: '0x5208',
* maxFeePerGas: '0x...',
* maxPriorityFeePerGas: '0x...',
* chainId: 1
* });
*/
export async function signTransaction(transaction) {
const client = initiateDeveloperControlledWalletsClient({
apiKey: process.env.CIRCLE_WALLETS_API_KEY,
entitySecret: process.env.ENTITY_SECRET,
});
const transactionJson = JSON.stringify(transaction,
(_, value) => typeof value === 'bigint' ? value.toString() : value, 2);
const response = await client.signTransaction({
walletId: process.env.CIRCLE_WALLET_ID,
transaction: transactionJson,
});
return response.data;
}
/**
* Composes a raw unsigned EIP-1559 transaction for approving USDC for the Permit2 contract.
* This transaction can then be signed using Circle Wallets' raw transaction signing API.
*
* @async
* @function composeUSDCApprovalTransaction
* @returns {Promise<object>} The unsigned EIP-1559 transaction object.
*/
export async function composeUSDCApprovalTransaction() {
const publicClient = createPublicClient({
chain: sepolia,
transport: http(process.env.RPC_URL),
});
// Step 1: Encode the function call data
const data = encodeFunctionData({
abi: erc20Abi,
functionName: "approve",
args: [
process.env.PERMIT2_CONTRACT_ADDRESS,
BigInt(process.env.APPROVAL_AMOUNT),
],
});
// Step 2: Estimate gas fees (viem handles the calculation)
const fees = await publicClient.estimateFeesPerGas();
// Step 3: Prepare the transaction request (viem auto-fills nonce, gas, etc.)
const transaction = await publicClient.prepareTransactionRequest({
account: process.env.SENDER_WALLET_ADDRESS,
to: process.env.USDC_CONTRACT_ADDRESS,
value: 0n,
data,
...fees,
});
return transaction;
}
/**
* Broadcasts an signed transaction to the network.
*
* @async
* @function broadcastSignedTransaction
* @param {string} signedTransaction - The signed transaction as a hex string (with 0x prefix)
* @returns {Promise<string>} The transaction hash
*/
export async function broadcastSignedTransaction(signedTransaction) {
const publicClient = createPublicClient({
chain: sepolia, // use the correct chain for your wallet
transport: http(process.env.RPC_URL),
});
// Send the raw signed transaction
const txHash = await publicClient.sendRawTransaction({
serializedTransaction: signedTransaction,
});
return txHash;
}
/* -------- Example usage with Circle ---------
// For auth and wallet creation, see: https://developers.circle.com/interactive-quickstarts/dev-controlled-wallets
const tx = await composeUSDCApprovalTransaction();
const signature = await signTransaction(tx);
const txHash = await broadcastSignedTransaction(signature.signedTransaction);
---------------------------------- */
import {
createWalletClient,
createPublicClient,
http,
custom,
erc20Abi,
} from "viem";
import { sepolia } from "viem/chains";
import dotenv from "dotenv";
dotenv.config();
/**
* Approves a specified amount of USDC for the Permit2 contract using
* a custom wallet with an EIP-1193 provider (like MetaMask).
*
* This function sends an on-chain transaction to call the ERC-20 `approve`
* method on the USDC contract, allowing the Permit2 contract to spend
* the specified amount on behalf of the wallet.
*
* @async
* @function approveUSDCWithEIP1193Wallet
* @param {any} [provider] - EIP-1193 provider.
* @returns {Promise<object>} The transaction hash and receipt.
*/
export async function approveUSDCWithEIP1193Wallet(provider) {
const publicClient = createPublicClient({
chain: sepolia,
transport: http(process.env.RPC_URL),
});
const walletClient = createWalletClient({
account: process.env.WALLET_ADDRESS,
chain: sepolia,
transport: custom(provider),
});
const hash = await walletClient.writeContract({
address: process.env.USDC_CONTRACT_ADDRESS,
abi: erc20Abi,
functionName: "approve",
args: [
process.env.PERMIT2_CONTRACT_ADDRESS,
BigInt(process.env.APPROVAL_AMOUNT),
],
});
const receipt = await publicClient.waitForTransactionReceipt({ hash });
return { hash, receipt };
}
/* -------- Example usage with EIP-1193 wallet ---------
// Refer to https://viem.sh/docs/clients/transports/custom and your wallet provider's documentation for the provider object.
const {hash, receipt} = await approveUSDCWithEIP1193Wallet({provider: window.ethereum});
console.log('Hash:', hash);
console.log('Receipt:', receipt);
---------------------------------- */