Gas Sponsorship for Smart Account
I have implemented the Coinbase Gas Sponsorship feature for Smart Accounts, which tested successfully with Coinbase Smart Wallets (https://keys.coinbase.com). However, for standard EOA wallets (like Metamask or Coinbase Wallet), users will still have to pay their own gas to claim the box.
Register Paymaster
Go to Coinbase paymaster service
https://portal.cdp.coinbase.com/products/paymaster/
Register your project to get
-
Paymaster & Bundler endpoint: looks like
https://api.developer.coinbase.com/rpc/v1/base/${YOUR_API_KEY} -
Enable Paymaster: register your contract and function that will be sponsored, in my case:
Name
PixelGift
Contract Address
0x83514843b0A11398e98e99873908c1d6f1C1CaeA
Functions
claimBox(uint16,uint256,bytes)
Claim Box with normal EOA Account
export function useClaimBoxEOA() {
const { writeContractAsync, data, error } = useWriteContract()
const claimBoxEOA = useCallback(async (pos: number, deadline: number, sig: `0x${string}`) => {
await writeContractAsync({
address: GiftContractAddress,
abi: claimBoxAbi,
functionName: 'claimBox',
args: [pos, BigInt(deadline), sig],
})
}, [writeContractAsync])
// After call, get status and receipts (with logs)
const { data: receipt } = useWaitForTransactionReceipt({
hash: data,
query: { enabled: !!data }
})
// Parse the logs to get result (how many tokens claimed)
const result = useMemo(() => getBoxClaimedEventLog(receipt), [receipt])
return { claimBoxEOA, result, error }
}
Claim Box with Smart Account
- First, get the capabilities of the connected account on the current chain.
- If paymaster supported, we set the Paymaster Url got above, then call contract using useWriteContracts (plural)
Check Capabilities
import { useMemo } from 'react'
import { useAccount, useCapabilities, useChainId } from 'wagmi'
interface AccountCapabilities {
paymasterService?: {
url: string
}
}
export function useAccountCapabilities(): AccountCapabilities {
const { address: account } = useAccount()
const chainId = useChainId()
// Check for paymaster capabilities with `useCapabilities`
const { data: availableCapabilities } = useCapabilities({
account,
});
const capabilities = useMemo(() => {
if (!availableCapabilities) return {};
const capabilitiesForChain = availableCapabilities[chainId];
if (
capabilitiesForChain['paymasterService'] &&
capabilitiesForChain['paymasterService'].supported
) {
return {
paymasterService: {
url: `https://api.developer.coinbase.com/rpc/v1/base/${YOUR_API_KEY}`, //For production use proxy
},
};
}
return {};
}, [availableCapabilities, chainId]);
return capabilities
}
Claimbox with Paymaster
import { useCallback, useEffect, useMemo } from 'react'
import { useCallsStatus } from 'wagmi'
import { useWriteContracts } from 'wagmi/experimental'
import { GiftContractAddress } from '../constants'
import { AccountCapabilities, claimBoxAbi } from './types'
import { getBoxClaimedEventLog } from './utils'
export function useClaimBoxPayMaster(capabilities: AccountCapabilities) {
const { writeContractsAsync, data, error } = useWriteContracts();
const claimBoxPayMaster = useCallback(async (pos: number, deadline: number, sig: `0x${string}`) => {
await writeContractsAsync({
contracts: [
{
address: GiftContractAddress,
abi: claimBoxAbi,
functionName: 'claimBox',
args: [pos, BigInt(deadline), sig],
}
],
capabilities // pass the capabilities with PaymasterUrl here
})
}, [capabilities])
// After call, get status and receipts (with logs)
const { data: statusData } = useCallsStatus({
id: data?.id ? data.id : '',
query: { enabled: !!data?.id}
})
useEffect(() => {
console.log('claimBoxPayMaster receipts', statusData)
}, [statusData?.receipts])
const result = useMemo(() => getBoxClaimedEventLog(statusData?.receipts?.[0] as any), [statusData?.receipts])
return { claimBoxPayMaster, result, error }
}
After calling contract successful, we get the bundle id, use it to call useCallsStatus get the receipt from the call. The receipt then can be used to get logs emitted by the transaction to see result how many tokens are claimed.
Support both EOA and Smart Account
I tried the Paymaster way with normal EOA account and it did not work because normal EOA account doesn't have code attached with it like a Smart Account, to be called by other contract. To support both we have to check the account and process the contract call differently.
Create a wrapper for claimBox function:
- Check the capabilities of the account to decide which way to go.
- Call claimBoxPayMaster or claimBoxEOA accordingly.
The hook function will look like
export function useClaimBox() {
// claimBox for EOA (Normal Account)
const { claimBoxEOA, result: rs1, error: err1 } = useClaimBoxEOA()
// claimBox for SA (Smart Account)
const capabilities = useAccountCapabilities()
const { claimBoxPayMaster, result: rs2, error: err2 } = useClaimBoxPayMaster(capabilities)
const claimBoxUniversal = useCallback(async (pos: number, deadline: number, signature: `0x${string}`) => {
if (capabilities?.paymasterService) {
await claimBoxPayMaster(pos, deadline, signature)
} else {
await claimBoxEOA(pos, deadline, signature)
}
}, [capabilities, claimBoxEOA, claimBoxPayMaster])
// Handle error (err1 || err2)
// Handle result (rs1 || rs2)
return claimBoxUniversal
}
Get Result & Parse log
After calling the claimBox function successfully, we get the result log in 2 different ways for EOA and Smart Account.
Get result
For EOA
// data is the 'hash' after calling
const { ..., data } = useWriteContract()
// After call, get status and receipts (with logs)
const { data: receipt } = useWaitForTransactionReceipt({
hash: data,
query: { enabled: !!data }
})
// Parse the logs to get result (how many tokens claimed)
const result = useMemo(() => getBoxClaimedEventLog(receipt), [receipt])
For Smart Account
// data is `{id: '...'}` after calling
const { ..., data } = useWriteContracts();
// After call, get status and receipts (with logs)
const { data: statusData } = useCallsStatus({
id: data?.id ? data.id : '',
query: { enabled: !!data?.id}
})
const result = useMemo(() => getBoxClaimedEventLog(statusData?.receipts?.[0] as any), [statusData?.receipts])
The EOA account method work quite stable with the receipt and logs, but for Smart Account, sometimes:
- I received
1recepit with logs, and status iscompletewhich is good and expected. - I received
0receipt, and status ispending, this case we can not see how many tokens claimed.
Parse the log
If things go as expected, we will get the same logs in both cases and can parse it to get information about how many tokens claimed.
import { Log, parseEventLogs } from 'viem'
// event ABI for parsing logs
export const boxClaimedEventAbi = [
{
type: "event",
name: "BoxClaimed",
inputs: [
{ name: "user", type: "address", indexed: false, internalType: "address" },
{ name: "position", type: "uint16", indexed: false, internalType: "uint16" },
{ name: "token", type: "uint16", indexed: false, internalType: "uint16" }
],
anonymous: false
},
] as const
// Note: the type of log here is not quite clear for me
export function getBoxClaimedEventLog(receipt?: { logs: Log<bigint, number, false>[]}) {
if (!receipt || !receipt.logs) return null
const parsedLogs = parseEventLogs({
abi: boxClaimedEventAbi,
eventName: 'BoxClaimed',
logs: receipt.logs,
})
if (parsedLogs.length === 0) {
console.log('No BoxClaimed event found in logs')
return null
}
return parsedLogs[0].args
}