Simplify blockchain transactions with Circle Paymaster: enable gas fee payments in USDC, enhance user experience, and eliminate the need for native tokens.
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:
- 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.
- 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.
- 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.
- 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.
- 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:
- Arbitrum Mainnet: 0x6C973eBe80dCD8660841D4356bf15c32460271C9
- Arbitrum Testnet: 0x31BE08D380A21fc740883c0BC434FcFc88740b58
- Base Mainnet: 0x6C973eBe80dCD8660841D4356bf15c32460271C9
- Base Testnet: 0x31BE08D380A21fc740883c0BC434FcFc88740b58
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?
- Improved User Experience: Users can interact with your app using only USDC, eliminating the need to acquire ETH for gas payments.
- EIP-2612 Permit Support: Paymaster supports EIP-2612, allowing users to authorize gas payments through off-chain signatures, reducing gas costs.
- Reliability from Circle: Backed by Circle, the issuer of USDC, Paymaster offers trust and operational reliability.
- 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
- Install Node.js and npm: Download from nodejs.org and npmjs.com.
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.