import { randomBytes } from "node:crypto";
import {
createPublicClient,
createWalletClient,
erc20Abi,
http,
pad,
parseAbi,
parseUnits,
toHex,
type Account,
type Address,
} from "viem";
import { base } from "viem/chains";
const permit3 = "0xEc00030C0000245E27d1521Cc2EE88F071c2Ae34";
const usdc = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const permit3Abi = parseAbi([
"struct AllowanceOrTransfer { uint48 modeOrExpiration; bytes32 tokenKey; address account; uint160 amountDelta; }",
"struct ChainPermits { uint64 chainId; AllowanceOrTransfer[] permits; }",
"function hashChainPermits(ChainPermits chainPermits) pure returns (bytes32)",
"function permit(address owner, bytes32 salt, uint48 deadline, uint48 timestamp, AllowanceOrTransfer[] permits, bytes signature)",
]);
export async function transferUsdc(
account: Account,
recipient: Address,
rpcUrl: string,
) {
const publicClient = createPublicClient({ chain: base, transport: http(rpcUrl) });
const walletClient = createWalletClient({ account, chain: base, transport: http(rpcUrl) });
const amount = parseUnits("1", 6);
try {
if ((await publicClient.getChainId()) !== base.id) {
throw new Error("The RPC must connect to Base mainnet.");
}
const allowance = await publicClient.readContract({
address: usdc,
abi: erc20Abi,
functionName: "allowance",
args: [account.address, permit3],
});
if (allowance < amount) {
throw new Error("Approve at least 1 USDC to Permit3 before continuing.");
}
const permits = [{
modeOrExpiration: 0,
tokenKey: pad(usdc, { size: 32 }),
account: recipient,
amountDelta: amount,
}];
const merkleRoot = await publicClient.readContract({
address: permit3,
abi: permit3Abi,
functionName: "hashChainPermits",
args: [{ chainId: BigInt(base.id), permits }],
});
const timestamp = Math.floor(Date.now() / 1000);
const deadline = timestamp + 300;
const salt = toHex(randomBytes(32));
const signature = await walletClient.signTypedData({
domain: { name: "Permit3", version: "1", chainId: 1, verifyingContract: permit3 },
primaryType: "Permit3",
types: {
Permit3: [
{ name: "owner", type: "address" },
{ name: "salt", type: "bytes32" },
{ name: "deadline", type: "uint48" },
{ name: "timestamp", type: "uint48" },
{ name: "merkleRoot", type: "bytes32" },
],
},
message: { owner: account.address, salt, deadline, timestamp, merkleRoot },
});
const { request } = await publicClient.simulateContract({
account,
address: permit3,
abi: permit3Abi,
functionName: "permit",
args: [account.address, salt, deadline, timestamp, permits, signature],
});
const gas = await publicClient.estimateContractGas(request);
const hash = await walletClient.writeContract({ ...request, gas });
const receipt = await publicClient.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") {
throw new Error(`Permit3 transfer reverted: ${hash}`);
}
return { hash, receipt };
} catch (error) {
throw new Error("Could not complete the Permit3 transfer.", { cause: error });
}
}