Circle Internet Financial
Circle Internet Financial Logo

Jan 30, 2025

January 30, 2025

How to Integrate Circle Paymaster to Enable Users to Pay Gas Fees with Their USDC Balance

what you’ll learn

Simplify blockchain transactions with Circle Paymaster: enable gas fee payments in USDC, enhance user experience, and eliminate the need for native tokens.

How to Integrate Circle Paymaster to Enable Users to Pay Gas Fees with Their USDC Balance

Gas fees are often a barrier to entry for users interacting with apps that process transactions over blockchain networks. Typically, these fees are paid in the blockchain’s native token, like ETH, which adds complexity for users who primarily transact with USDC. Circle Paymaster simplifies this process by allowing users to pay for gas fees directly from their USDC balance. 

What is Circle Paymaster?

Circle Paymaster is a smart contract within the Account Abstraction (ERC-4337) framework that sponsors gas fees on behalf of users. It is currently deployed on Arbitrum and Base, with plans to expand to other blockchains in the near future.

How Circle Paymaster Handles Transactions with a Bundler

Paymaster is operated by Circle and integrates seamlessly with bundlers such as Pimlico and Alchemy, which facilitate transaction bundling. A bundler is a service that collects user operations, combines them into a single transaction, and submits it to the blockchain network for execution, optimizing gas usage and efficiency. Below are the sequential steps outlining how Paymaster processes a transaction, with references to relevant contract functions:

  1. USDC Balance Verification: Paymaster uses the balanceOf(address) function of the USDC token contract to check whether the user’s balance is sufficient to cover the transaction and associated gas fees.
  2. USDC to ETH Conversion for Gas Calculation: The fetchPrice() function retrieves the real-time USDC to ETH conversion rate, ensuring accurate calculation of the gas fee equivalent in USDC and preventing overcharging.
  3. Transaction Authorization with Bundlers: The _validatePaymasterUserOp() function processes an EIP-2612 permit, which authorizes Paymaster to deduct the required USDC amount from the user. The permit is signed off-chain by the user and submitted with the transaction.
  4. Gas Payment Processing: The _postOp() function debits the required USDC from the user’s balance and handles the exchange of USDC for ETH using the swapForNative(uint256 amountIn, uint256 slippageBips, uint24 poolFee) function. The bundler then processes the transaction on the blockchain.
  5. Transaction Finalization: The bundler confirms the transaction completion and Paymaster ensures all balances and gas payment records are updated accordingly.

Key functions involved in this process include:

  • balanceOf(address) – Retrieves the USDC balance of the user.
  • getPrice(address token1, address token2) – Fetches the exchange rate between USDC and ETH for accurate gas fee calculations.
  • processTransaction(bytes calldata userOperation) – Manages the transaction execution and gas fee deduction.

The paymaster contract addresses for Circle Paymaster are as follows:

These addresses are essential for configuring and interacting with Paymaster in your application. The paymaster interacts with the blockchain network to cover gas fees by leveraging off-chain signatures that authorize the paymaster to spend a user's USDC balance. It calculates the required gas, converts it into an equivalent USDC value, and then deducts the amount from the user's balance while ensuring the transaction is processed seamlessly on-chain without needing the native token.

Why use Circle Paymaster?

  1. Improved User Experience:  Users can interact with your app using only USDC, eliminating the need to acquire ETH for gas payments.
  2. EIP-2612 Permit Support:  Paymaster supports EIP-2612, allowing users to authorize gas payments through off-chain signatures, reducing gas costs.
  3. Reliability from Circle: Backed by Circle, the issuer of USDC, Paymaster offers trust and operational reliability.
  4. Deep Liquidity: Paymaster is funded with deep native gas token liquidity, ensuring consistent transaction reliability.

Here’s a step-by-step guide to building an app that uses Paymaster. You can fork and run the code directly from our Replit link—feel free to check it out!

Step 1: Setup Your Development Environment

Step 2: Create a new Next.js Project

npx create-next-app@latest circle-paymaster-wallet --typescript --tailwind --eslint

cd circle-paymaster-wallet

npx shadcn@latest init -d

Step 3: Install Required Dependencies

npm install @radix-ui/react-label @radix-ui/react-slot @radix-ui/react-tabs @tanstack/query-core @tanstack/react-query class-variance-authority clsx lucide-react next permissionless react react-dom tailwind-merge tailwindcss-animate viem

Step 4: Configure Blockchain Interaction

  • Setup a Smart Contract Interaction Service: File: lib/transfer-service.ts
 import { createPublicClient, http, getContract, encodeFunctionData,   encodePacked, parseAbi, parseErc6492Signature, formatUnits, hexToBigInt } from 'viem'
   import { createBundlerClient } from 'viem/account-abstraction'
   import { arbitrumSepolia } from 'viem/chains'
   import { toEcdsaKernelSmartAccount } from 'permissionless/accounts'
   import { privateKeyToAccount } from 'viem/accounts'
   import { eip2612Permit, tokenAbi } from './permit-helpers'

   const ARBITRUM_SEPOLIA_USDC = '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d'
   const ARBITRUM_SEPOLIA_PAYMASTER = '0x31BE08D380A21fc740883c0BC434FcFc88740b58'
   const ARBITRUM_SEPOLIA_BUNDLER = `https://public.pimlico.io/v2/${arbitrumSepolia.id}/rpc`

   const MAX_GAS_USDC = 1000000n // 1 USDC

   export async function transferUSDC(
     privateKey: `0x${string}`,
     recipientAddress: string,
     amount: bigint
   ) {
     // Create clients
     const client = createPublicClient({
       chain: arbitrumSepolia,
       transport: http()
     })
     const bundlerClient = createBundlerClient({
       client,
       transport: http(ARBITRUM_SEPOLIA_BUNDLER)
     })
     // Create accounts
     const owner = privateKeyToAccount(privateKey)
     const account = await toEcdsaKernelSmartAccount({
       client,
       owners: [owner],
       version: '0.3.1'
     })
     // Setup USDC contract
     const usdc = getContract({
       client,
       address: ARBITRUM_SEPOLIA_USDC,
       abi: tokenAbi,
     })
     // Verify USDC balance first
     const balance = await usdc.read.balanceOf([account.address])
     if (balance < amount) {
       throw new Error(`Insufficient USDC balance. Have: ${formatUnits(balance, 6)}, Need: ${formatUnits(amount, 6)}`)
     }
     // Construct and sign permit
     const permitData = await eip2612Permit({
       token: usdc,
       chain: arbitrumSepolia,
       ownerAddress: account.address,
       spenderAddress: ARBITRUM_SEPOLIA_PAYMASTER,
       value: MAX_GAS_USDC
     })
     const signData = { ...permitData, primaryType: 'Permit' as const }
     const wrappedPermitSignature = await account.signTypedData(signData)
     const { signature: permitSignature } = parseErc6492Signature(wrappedPermitSignature)
     // Prepare transfer call
     const calls = [{
       to: usdc.address,
       abi: usdc.abi,
       functionName: 'transfer',
       args: [recipientAddress, amount]
     }]
     // Specify the USDC Token Paymaster
     const paymaster = ARBITRUM_SEPOLIA_PAYMASTER
     const paymasterData = encodePacked(
       ['uint8', 'address', 'uint256', 'bytes'],
       [
         0, // Reserved for future use
         usdc.address, // Token address
         MAX_GAS_USDC, // Max spendable gas in USDC
         permitSignature // EIP-2612 permit signature
       ]
     )
     // Get additional gas charge from paymaster
     const additionalGasCharge = hexToBigInt(
       (
         await client.call({
           to: paymaster,
           data: encodeFunctionData({
             abi: parseAbi(['function additionalGasCharge() returns (uint256)']),
             functionName: 'additionalGasCharge'
           })
         }) ?? { data: '0x0' }
       ).data
     )
     // Get current gas prices
     const { standard: fees } = await bundlerClient.request({
       method: 'pimlico_getUserOperationGasPrice' as any
     }) as { standard: { maxFeePerGas: `0x${string}`, maxPriorityFeePerGas: `0x${string}` } }
     const maxFeePerGas = hexToBigInt(fees.maxFeePerGas)
     const maxPriorityFeePerGas = hexToBigInt(fees.maxPriorityFeePerGas)
     // Estimate gas limits
     const {
       callGasLimit,
       preVerificationGas,
       verificationGasLimit,
       paymasterPostOpGasLimit,
       paymasterVerificationGasLimit
     } = await bundlerClient.estimateUserOperationGas({
       account,
       calls,
       paymaster,
       paymasterData,
       paymasterPostOpGasLimit: additionalGasCharge,
       maxFeePerGas: 1n,
       maxPriorityFeePerGas: 1n
     })
     // Send user operation
     const userOpHash = await bundlerClient.sendUserOperation({
       account,
       calls,
       callGasLimit,
       preVerificationGas,
       verificationGasLimit,
       paymaster,
       paymasterData,
       paymasterVerificationGasLimit,
       paymasterPostOpGasLimit: BigInt(Math.max(
         Number(paymasterPostOpGasLimit),
         Number(additionalGasCharge)
       )),
       maxFeePerGas,
       maxPriorityFeePerGas
     })
     // Wait for receipt
     const userOpReceipt = await bundlerClient.waitForUserOperationReceipt({
       hash: userOpHash
     })
     return userOpReceipt
   }
  • Setup Permit Helper for EIP-2612 Integration:
    File: lib/permit-helpers.ts
import { Address, Chain, TypedDataDomain, getContract } from 'viem'
export const eip2612Abi = [
  {
    constant: false,
    inputs: [
      { name: 'owner', type: 'address' },
      { name: 'spender', type: 'address' },
      { name: 'value', type: 'uint256' },
      { name: 'deadline', type: 'uint256' },
      { name: 'v', type: 'uint8' },
      { name: 'r', type: 'bytes32' },
      { name: 's', type: 'bytes32' },
    ],
    name: 'permit',
    outputs: [],
    payable: false,
    stateMutability: 'nonpayable',
    type: 'function',
  },
] as const
export const tokenAbi = [
  ...eip2612Abi,
  {
    inputs: [{ name: 'owner', type: 'address' }],
    name: 'nonces',
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [],
    name: 'name',
    outputs: [{ name: '', type: 'string' }],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [],
    name: 'version',
    outputs: [{ name: '', type: 'string' }],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      { name: 'recipient', type: 'address' },
      { name: 'amount', type: 'uint256' }
    ],
    name: 'transfer',
    outputs: [{ name: '', type: 'bool' }],
    stateMutability: 'nonpayable',
    type: 'function',
  },
  {
    inputs: [{ name: 'account', type: 'address' }],
    name: 'balanceOf',
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
    type: 'function',
  }
] as const
export async function eip2612Permit({
  token,
  chain,
  ownerAddress,
  spenderAddress,
  value,
}: {
  token: ReturnType<typeof getContract>
  chain: Chain
  ownerAddress: Address
  spenderAddress: Address
  value: bigint
}) {
  const [nonce, name, version] = await Promise.all([
    token.read.nonces([ownerAddress]),
    token.read.name(),
    token.read.version(),
  ])
  const domain: TypedDataDomain = {
    name,
    version,
    chainId: chain.id,
    verifyingContract: token.address,
  }
  const types = {
    Permit: [
      { name: 'owner', type: 'address' },
      { name: 'spender', type: 'address' },
      { name: 'value', type: 'uint256' },
      { name: 'nonce', type: 'uint256' },
      { name: 'deadline', type: 'uint256' },
    ],
  }
  const message = {
    owner: ownerAddress,
    spender: spenderAddress,
    value,
    nonce,
    deadline: BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'),
  }
  return {
    domain,
    types,
    message,
  }
}

Step 5: Build the Frontend

  • Update Main Component:
    File: app/page.tsx
'use client'
import { useState, useEffect } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { Loader2 } from 'lucide-react'
import { createPublicClient, http, formatUnits } from 'viem'
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
import { arbitrumSepolia } from 'viem/chains'
import { toEcdsaKernelSmartAccount } from 'permissionless/accounts'
import { tokenAbi } from '@/lib/permit-helpers'
import { transferUSDC } from '@/lib/transfer-service'

const ARBITRUM_SEPOLIA_USDC = '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d'

export default function SmartWallet() {
  const [loading, setLoading] = useState(false)
  const [account, setAccount] = useState<any>(null)
  const [recipientAddress, setRecipientAddress] = useState('')
  const [amount, setAmount] = useState('')
  const [status, setStatus] = useState('')
  const [usdcBalance, setUsdcBalance] = useState<string>('0.00')
  useEffect(() => {
    const fetchBalance = async () => {
      if (!account?.address) return
      
      const client = createPublicClient({
        chain: arbitrumSepolia,
        transport: http()
      })
      const balance = await client.readContract({
        address: ARBITRUM_SEPOLIA_USDC,
        abi: [{
          inputs: [{ name: 'account', type: 'address' }],
          name: 'balanceOf',
          outputs: [{ name: '', type: 'uint256' }],
          stateMutability: 'view',
          type: 'function'
        }],
        functionName: 'balanceOf',
        args: [account.address]
      })
      const formattedBalance = Number(formatUnits(balance as bigint, 6)).toFixed(2)
      setUsdcBalance(formattedBalance)
    }
    fetchBalance()
    // Set up polling interval
    const interval = setInterval(fetchBalance, 10000) // Poll every 10 seconds
    return () => clearInterval(interval)
  }, [account?.address])
  const createAccount = async () => {
    try {
      setLoading(true)
      setStatus('Creating smart account...')
      // Create RPC client
      const client = createPublicClient({
        chain: arbitrumSepolia,
        transport: http()
      })
      // Generate private key and create owner account
      const privateKey = generatePrivateKey()
      const owner = privateKeyToAccount(privateKey)
      // Create smart account
      const smartAccount = await toEcdsaKernelSmartAccount({
        client,
        owners: [owner],
        version: '0.3.1'
      })
      setAccount({
        address: smartAccount.address,
        owner: owner.address,
        privateKey: `0x${privateKey.slice(2)}`
      })
      setStatus('Smart account created successfully!')
    } catch (error) {
      setStatus('Error creating smart account: ' + (error as Error).message)
    } finally {
      setLoading(false)
    }
  }
  const transfer = async () => {
    try {
      setLoading(true)
      setStatus('Checking balance...')
      // Create client for balance check
      const client = createPublicClient({
        chain: arbitrumSepolia,
        transport: http()
      })
      // Check balance before transfer
      const balance = await client.readContract({
        address: ARBITRUM_SEPOLIA_USDC,
        abi: tokenAbi,
        functionName: 'balanceOf',
        args: [account.address]
      }) as bigint
      // Convert input amount to USDC decimals (6 decimals)
      const amountInWei = BigInt(Math.floor(parseFloat(amount) * 1_000_000))
      // Required gas buffer (2 USDC to be safe)
      const gasBuffer = BigInt(2_000_000) // 2 USDC in wei
      const totalNeeded = amountInWei + gasBuffer
      // Check if balance is sufficient including gas buffer
      if (balance < totalNeeded) {
        const currentBalance = Number(formatUnits(balance, 6))
        const requestedAmount = Number(amount)
        const availableForTransfer = Math.max(0, currentBalance - 2) // Leave 2 USDC for gas
        throw new Error(
          `Insufficient balance for this transfer. ` +
          `\nCurrent balance: ${currentBalance} USDC` +
          `\nRequested transfer: ${requestedAmount} USDC` +
          `\nGas buffer needed: 2 USDC` +
          `\nMaximum you can transfer: ${availableForTransfer.toFixed(2)} USDC` +
          `\n\nPlease reduce your transfer amount or get more USDC from the faucet.`
        )
      }
      setStatus('Initiating transfer...')
      const receipt = await transferUSDC(
        account.privateKey,
        recipientAddress,
        amountInWei
      )
      if (receipt.success) {
        setStatus('Transfer completed successfully!')
        setRecipientAddress('')
        setAmount('')
      } else {
        setStatus('Transfer failed. Please try again.')
      }
    } catch (error: any) {
      // Check for specific error signatures
      if (error.message.includes('0x65c8fd4d')) {
        setStatus('Error: Insufficient USDC balance for transfer and gas fees (need ~2 USDC for gas)')
      } else {
        setStatus(error.message)
      }
    } finally {
      setLoading(false)
    }
  }
  return (
    <>
      {account && (
        <div className="fixed top-4 right-4 bg-card rounded-lg border shadow p-3">
          <span className="text-sm font-medium">USDC Balance: </span>
          <span className="font-mono">${usdcBalance}</span>
        </div>
      )}
      <div className="container max-w-2xl mx-auto p-4">
        <Card>
          <CardHeader>
            <CardTitle>Smart Wallet Interface</CardTitle>
            <CardDescription>Create and manage your smart account with Circle's USDC Paymaster</CardDescription>
          </CardHeader>
          <CardContent>
            <Tabs defaultValue="create" className="space-y-4">
              <TabsList>
                <TabsTrigger value="create">Create Account</TabsTrigger>
                <TabsTrigger value="transfer" disabled={!account}>Transfer</TabsTrigger>
              </TabsList>
              <TabsContent value="create" className="space-y-4">
                {!account ? (
                  <Button 
                    onClick={createAccount} 
                    disabled={loading}
                    className="w-full"
                  >
                    {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
                    Create Smart Account
                  </Button>
                ) : (
                  <div className="space-y-4">
                    <div className="space-y-2">
                      <Label>Smart Wallet Address</Label>
                      <Alert>
                        <AlertDescription className="font-mono break-all">
                          {account.address}
                        </AlertDescription>
                      </Alert>
                    </div>
                    <div className="space-y-2">
                      <Label>Owner Address</Label>
                      <Alert>
                        <AlertDescription className="font-mono break-all">
                          {account.owner}
                        </AlertDescription>
                      </Alert>
                    </div>
                  </div>
                )}
              </TabsContent>
              <TabsContent value="transfer" className="space-y-4">
                <div className="space-y-4">
                  <div className="space-y-2">
                    <Label htmlFor="recipient">Recipient Address</Label>
                    <Input
                      id="recipient"
                      placeholder="0x..."
                      value={recipientAddress}
                      onChange={(e) => setRecipientAddress(e.target.value)}
                    />
                  </div>
                  <div className="space-y-2">
                    <Label htmlFor="amount">Amount (USDC)</Label>
                    <Input
                      id="amount"
                      type="number"
                      placeholder="0.00"
                      value={amount}
                      onChange={(e) => setAmount(e.target.value)}
                    />
                  </div>
                  <Button 
                    onClick={transfer} 
                    disabled={loading || !recipientAddress || !amount}
                    className="w-full"
                  >
                    {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
                    Transfer USDC
                  </Button>
                </div>
              </TabsContent>
            </Tabs>
            {status && (
              <Alert className="mt-4">
                <AlertDescription>{status}</AlertDescription>
              </Alert>
            )}
          </CardContent>
        </Card>
      </div>
    </>
  )
}
  • Update Root Layout for Application: File: app/layout.tsx
'use client'

import './globals.css'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        {children}
      </body>
    </html>
  )
}
  • Setup UI Components:
    Create reusable components in components/ui/ (e.g., button.tsx, card.tsx, input.tsx).
mkdir -p components/ui

components/ui/button.tsx

import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"

const buttonVariants = cva(
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
  {
    variants: {
      variant: {
        default:
          "bg-primary text-primary-foreground shadow hover:bg-primary/90",
        destructive:
          "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
        outline:
          "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
        secondary:
          "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-9 px-4 py-2",
        sm: "h-8 rounded-md px-3 text-xs",
        lg: "h-10 rounded-md px-8",
        icon: "h-9 w-9",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)
export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button"
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    )
  }
)
Button.displayName = "Button"
export { Button, buttonVariants }

components/ui/card.tsx

import * as React from "react"
import { cn } from "@/lib/utils"

const Card = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "rounded-xl border bg-card text-card-foreground shadow",
      className
    )}
    {...props}
  />
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("flex flex-col space-y-1.5 p-6", className)}
    {...props}
  />
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("font-semibold leading-none tracking-tight", className)}
    {...props}
  />
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("text-sm text-muted-foreground", className)}
    {...props}
  />
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("flex items-center p-6 pt-0", className)}
    {...props}
  />
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

components/ui/input.tsx

import * as React from "react"
import { cn } from "@/lib/utils"

const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
  ({ className, type, ...props }, ref) => {
    return (
      <input
        type={type}
        className={cn(
          "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
          className
        )}
        ref={ref}
        {...props}
      />
    )
  }
)
Input.displayName = "Input"
export { Input }

components/ui/label.tsx

"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"

const labelVariants = cva(
  "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
  React.ElementRef<typeof LabelPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
    VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
  <LabelPrimitive.Root
    ref={ref}
    className={cn(labelVariants(), className)}
    {...props}
  />
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

components/ui/tabs.tsx

"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"

const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
  React.ElementRef<typeof TabsPrimitive.List>,
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
  <TabsPrimitive.List
    ref={ref}
    className={cn(
      "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
      className
    )}
    {...props}
  />
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
  React.ElementRef<typeof TabsPrimitive.Trigger>,
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
  <TabsPrimitive.Trigger
    ref={ref}
    className={cn(
      "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
      className
    )}
    {...props}
  />
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
  React.ElementRef<typeof TabsPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
  <TabsPrimitive.Content
    ref={ref}
    className={cn(
      "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
      className
    )}
    {...props}
  />
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

components/ui/alert.tsx

import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"

const alertVariants = cva(
  "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
  {
    variants: {
      variant: {
        default: "bg-background text-foreground",
        destructive:
          "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)
const Alert = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
  <div
    ref={ref}
    role="alert"
    className={cn(alertVariants({ variant }), className)}
    {...props}
  />
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
  <h5
    ref={ref}
    className={cn("mb-1 font-medium leading-none tracking-tight", className)}
    {...props}
  />
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("text-sm [&_p]:leading-relaxed", className)}
    {...props}
  />
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

Step 6: Start Development Server

npm run dev

Once the development server is running, visit http://localhost:3000 in your browser. Follow these steps to test the application:

1. Click on the Create Smart Account button to generate a smart wallet address.

2. Deposit testnet USDC into the smart wallet address. You can source testnet USDC from https://faucet.circle.com.

3. Navigate to the Transfer tab.

4. Input the recipient address and the amount of USDC to transfer.

5. Click on the Transfer USDC button to initiate the transfer.

This process demonstrates the functionality of the smart wallet and gas fee management using Circle Paymaster.

Related posts

Now Available: Native USDC on Aptos

Now Available: Native USDC on Aptos

January 30, 2025
Migration Guide: Bridged to Native USDC on Aptos

Migration Guide: Bridged to Native USDC on Aptos

January 30, 2025
Recibo: Encrypted Messages for ERC-20 Transactions

Recibo: Encrypted Messages for ERC-20 Transactions

January 29, 2025
Blog
How to Integrate Circle Paymaster to Enable Users to Pay Gas Fees with Their USDC Balance
how-to-integrate-circle-paymaster-to-enable-users-to-pay-gas-fees-with-their-usdc-balance
January 30, 2025
Simplify blockchain transactions with Circle Paymaster: enable gas fee payments in USDC, enhance user experience, and eliminate the need for native tokens.
Developer
USDC
Build on Circle
Payments
Tutorial