From f3aebfb6b1b4c02c0b93e5fb1c8a8699d05e4354 Mon Sep 17 00:00:00 2001 From: Bijan Massoumi Date: Wed, 26 Nov 2025 16:12:54 -0500 Subject: [PATCH 1/4] Enhance gas station SDK with new reimbursable execution features, including session signature handling and updated contract interactions. Add example scripts for USDC transfers and swaps using the Universal Router. Update package dependencies for Uniswap SDKs and OpenZeppelin contracts. --- examples/tk-gas-station/package.json | 4 + examples/tk-gas-station/src/swapUSDCForETH.ts | 494 +++++++++++++++ .../src/transferUSDCReimbursable.ts | 262 ++++++++ .../src/abi/reimbursable-gas-station.ts | 588 ++++++++++++++++++ packages/gas-station/src/config.ts | 9 + packages/gas-station/src/gasStationClient.ts | 107 ++++ packages/gas-station/src/gasStationUtils.ts | 26 + packages/gas-station/src/index.ts | 7 + packages/gas-station/src/intentBuilder.ts | 57 +- pnpm-lock.yaml | 253 ++++++-- 10 files changed, 1761 insertions(+), 46 deletions(-) create mode 100644 examples/tk-gas-station/src/swapUSDCForETH.ts create mode 100644 examples/tk-gas-station/src/transferUSDCReimbursable.ts create mode 100644 packages/gas-station/src/abi/reimbursable-gas-station.ts diff --git a/examples/tk-gas-station/package.json b/examples/tk-gas-station/package.json index 55b7d1f65..b32507fa3 100644 --- a/examples/tk-gas-station/package.json +++ b/examples/tk-gas-station/package.json @@ -11,6 +11,7 @@ "eth-transfer": "tsx src/transferETHDelegated.ts", "usdc-transfer": "tsx src/transferUSDCDelegated.ts", "approve-then-execute": "tsx src/approveThenExecuteDemo.ts", + "swap-usdc-eth": "tsx src/swapUSDCForETH.ts", "test:policies": "jest policyEnforcement", "clean": "rimraf ./dist ./.cache", "typecheck": "tsc --noEmit" @@ -21,6 +22,9 @@ "@turnkey/gas-station": "workspace:*", "@turnkey/sdk-server": "workspace:*", "@turnkey/viem": "workspace:*", + "@uniswap/sdk-core": "^6.0.0", + "@uniswap/universal-router-sdk": "^3.0.0", + "@uniswap/v3-sdk": "^3.9.0", "dotenv": "^16.0.3", "ethers": "^6.10.0", "prompts": "^2.4.2", diff --git a/examples/tk-gas-station/src/swapUSDCForETH.ts b/examples/tk-gas-station/src/swapUSDCForETH.ts new file mode 100644 index 000000000..5d5d7fbec --- /dev/null +++ b/examples/tk-gas-station/src/swapUSDCForETH.ts @@ -0,0 +1,494 @@ +import { resolve } from "path"; +import * as dotenv from "dotenv"; +import { z } from "zod"; +import { parseArgs } from "node:util"; +import { + parseUnits, + createWalletClient, + createPublicClient, + http, + encodeAbiParameters, + encodePacked, + type Hex, + concat, + toHex, +} from "viem"; +import { Turnkey as TurnkeyServerSDK } from "@turnkey/sdk-server"; +import { createAccount } from "@turnkey/viem"; +import { GasStationClient, CHAIN_PRESETS, buildTokenApproval } from "@turnkey/gas-station"; +import { print } from "./utils"; + +// Uniswap SDK imports for token definitions and types +import { Token, Percent } from "@uniswap/sdk-core"; +import { FeeAmount } from "@uniswap/v3-sdk"; + +dotenv.config({ path: resolve(process.cwd(), ".env.local") }); + +// Swap configuration +const USDC_AMOUNT = "0.10"; // 10 cents USDC to swap +const MIN_ETH_OUT = 0n; // Accept any amount for demo (production should use a quote) +const SLIPPAGE_PERCENT = new Percent(50, 10000); // 0.5% slippage (for reference) + +// Chain ID for Base mainnet +const BASE_CHAIN_ID = 8453; + +// Contract addresses on Base mainnet +const UNIVERSAL_ROUTER_ADDRESS: Hex = + "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD"; // Universal Router on Base +const PERMIT2_ADDRESS: Hex = + "0x000000000022D473030F116dDEE9F6B43aC78BA3"; // Permit2 (same on all chains) +const WETH_ADDRESS: Hex = "0x4200000000000000000000000000000000000006"; +const USDC_ADDRESS: Hex = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; + +// Universal Router command codes +const V3_SWAP_EXACT_IN = 0x00; + +// Universal Router ABI for execute function +const UNIVERSAL_ROUTER_ABI = [ + { + name: "execute", + type: "function", + stateMutability: "payable", + inputs: [ + { name: "commands", type: "bytes" }, + { name: "inputs", type: "bytes[]" }, + { name: "deadline", type: "uint256" }, + ], + outputs: [], + }, +] as const; + +// Permit2 ABI for approve and allowance functions +const PERMIT2_ABI = [ + { + name: "approve", + type: "function", + stateMutability: "nonpayable", + inputs: [ + { name: "token", type: "address" }, + { name: "spender", type: "address" }, + { name: "amount", type: "uint160" }, + { name: "expiration", type: "uint48" }, + ], + outputs: [], + }, + { + name: "allowance", + type: "function", + stateMutability: "view", + inputs: [ + { name: "owner", type: "address" }, + { name: "token", type: "address" }, + { name: "spender", type: "address" }, + ], + outputs: [ + { name: "amount", type: "uint160" }, + { name: "expiration", type: "uint48" }, + { name: "nonce", type: "uint48" }, + ], + }, +] as const; + +// ERC20 ABI for allowance check +const ERC20_ALLOWANCE_ABI = [ + { + name: "allowance", + type: "function", + stateMutability: "view", + inputs: [ + { name: "owner", type: "address" }, + { name: "spender", type: "address" }, + ], + outputs: [{ name: "", type: "uint256" }], + }, +] as const; + +// Parse command line arguments +const { values } = parseArgs({ + args: process.argv.slice(2), + options: { + chain: { + type: "string", + short: "c", + default: "base", + }, + }, +}); + +// This example only supports Base mainnet +if (values.chain !== "base") { + console.error("This swap example only supports Base mainnet."); + process.exit(1); +} + +const preset = CHAIN_PRESETS.BASE_MAINNET; + +const envSchema = z.object({ + BASE_URL: z.string().url(), + API_PRIVATE_KEY: z.string().min(1), + API_PUBLIC_KEY: z.string().min(1), + ORGANIZATION_ID: z.string().min(1), + EOA_ADDRESS: z.string().min(1), + PAYMASTER_ADDRESS: z.string().min(1), + BASE_RPC_URL: z.string().url(), +}); + +const env = envSchema.parse(process.env); + +print( + `🌐 Using BASE network`, + `Swapping USDC for ETH via Uniswap Universal Router`, +); + +const turnkeyClient = new TurnkeyServerSDK({ + apiBaseUrl: env.BASE_URL, + apiPrivateKey: env.API_PRIVATE_KEY, + apiPublicKey: env.API_PUBLIC_KEY, + defaultOrganizationId: env.ORGANIZATION_ID, +}); + +// Define tokens using Uniswap SDK for type safety +const USDC_TOKEN = new Token(BASE_CHAIN_ID, USDC_ADDRESS, 6, "USDC", "USD Coin"); +const WETH_TOKEN = new Token(BASE_CHAIN_ID, WETH_ADDRESS, 18, "WETH", "Wrapped Ether"); + +/** + * Encodes the path for a V3 swap (tokenIn -> fee -> tokenOut). + * The path format is: [tokenIn, fee, tokenOut] packed together. + */ +function encodeV3Path(tokenIn: Hex, fee: FeeAmount, tokenOut: Hex): Hex { + return encodePacked( + ["address", "uint24", "address"], + [tokenIn, fee, tokenOut], + ); +} + +/** + * Encodes the input for V3_SWAP_EXACT_IN command. + * Format: (address recipient, uint256 amountIn, uint256 amountOutMin, bytes path, bool payerIsUser) + */ +function encodeV3SwapExactIn( + recipient: Hex, + amountIn: bigint, + amountOutMin: bigint, + path: Hex, + payerIsUser: boolean, +): Hex { + return encodeAbiParameters( + [ + { type: "address" }, + { type: "uint256" }, + { type: "uint256" }, + { type: "bytes" }, + { type: "bool" }, + ], + [recipient, amountIn, amountOutMin, path, payerIsUser], + ); +} + +/** + * Demonstrates USDC to ETH swap using the Gas Station pattern with EIP-7702 authorization. + * Uses Uniswap Universal Router on Base mainnet with separate approve and execute steps. + * + * This is ideal for gasless users who hold USDC but no ETH: + * 1. EOA signs intent to approve USDC to Permit2 β†’ Paymaster executes + * 2. EOA signs intent to approve Universal Router on Permit2 β†’ Paymaster executes + * 3. EOA signs intent to execute swap β†’ Paymaster executes + * 4. EOA receives WETH in exchange for USDC + */ +const main = async () => { + const rpcUrl = env.BASE_RPC_URL; + + // Create viem wallet clients with Turnkey accounts + const userAccount = await createAccount({ + client: turnkeyClient.apiClient(), + organizationId: env.ORGANIZATION_ID, + signWith: env.EOA_ADDRESS as Hex, + }); + + const paymasterAccount = await createAccount({ + client: turnkeyClient.apiClient(), + organizationId: env.ORGANIZATION_ID, + signWith: env.PAYMASTER_ADDRESS as Hex, + }); + + const userWalletClient = createWalletClient({ + account: userAccount, + chain: preset.chain, + transport: http(rpcUrl), + }); + + const paymasterWalletClient = createWalletClient({ + account: paymasterAccount, + chain: preset.chain, + transport: http(rpcUrl), + }); + + // Create Gas Station clients with the viem wallet clients + const userClient = new GasStationClient({ + walletClient: userWalletClient, + }); + + const paymasterClient = new GasStationClient({ + walletClient: paymasterWalletClient, + }); + + // Create public client for reading on-chain data + const publicClient = createPublicClient({ + chain: preset.chain, + transport: http(rpcUrl), + }); + + const explorerUrl = "https://basescan.org"; + + // Step 1: Check if EOA is already authorized, authorize if needed + const isAuthorized = await userClient.isAuthorized(); + + if (!isAuthorized) { + print("===== Starting EIP-7702 Authorization =====", ""); + print("EOA not yet authorized", "Starting authorization..."); + + print("User signing authorization...", ""); + const authorization = await userClient.signAuthorization(); + + const authResult = await paymasterClient.submitAuthorizations([ + authorization, + ]); + + print("Authorization transaction sent", authResult.txHash); + print("Waiting for confirmation...", ""); + print("βœ… Authorization SUCCEEDED", ""); + print( + "βœ… Authorization complete", + `${explorerUrl}/tx/${authResult.txHash}`, + ); + + // Delay to ensure proper sequencing of on-chain state after authorization + await new Promise((resolve) => setTimeout(resolve, 2000)); + } else { + print("βœ“ EOA already authorized", "Skipping authorization"); + } + + // Step 2: Execute USDC to ETH swap via Uniswap Universal Router + print("===== Starting USDC β†’ ETH Swap =====", ""); + + const swapAmount = parseUnits(USDC_AMOUNT, USDC_TOKEN.decimals); + const deadline = BigInt(Math.floor(Date.now() / 1000) + 60 * 20); // 20 minutes + + // Encode the V3 swap path: USDC -> 0.05% fee -> WETH + const swapPath = encodeV3Path(USDC_ADDRESS, FeeAmount.LOW, WETH_ADDRESS); + + // Encode the swap input for V3_SWAP_EXACT_IN + // recipient = EOA address, payerIsUser = true (USDC comes from msg.sender) + const swapInput = encodeV3SwapExactIn( + env.EOA_ADDRESS as Hex, + swapAmount, + MIN_ETH_OUT, + swapPath, + true, // payerIsUser - the EOA pays with their USDC + ); + + // Build the Universal Router execute calldata + const commands = toHex(new Uint8Array([V3_SWAP_EXACT_IN])); // Single command: V3_SWAP_EXACT_IN + const inputs = [swapInput]; // Corresponding input for the command + + // Encode the execute function call + const swapCallData = encodeAbiParameters( + [ + { type: "bytes" }, + { type: "bytes[]" }, + { type: "uint256" }, + ], + [commands, inputs, deadline], + ); + + // Prepend the function selector for execute(bytes,bytes[],uint256) + const executeSelector = "0x3593564c" as Hex; // keccak256("execute(bytes,bytes[],uint256)")[:4] + const fullSwapCallData = concat([executeSelector, swapCallData]); + + print( + `Swapping ${USDC_AMOUNT} USDC for ETH`, + `Recipient: ${env.EOA_ADDRESS}`, + ); + print( + "Swap parameters", + `Amount: ${swapAmount} (${USDC_AMOUNT} USDC), Fee tier: ${FeeAmount.LOW / 10000}%, Deadline: ${deadline}`, + ); + + // ===== Step 2a: Approve USDC to Permit2 (if needed) ===== + print("--- Step 2a: Check/Approve USDC to Permit2 ---", ""); + + // Check existing ERC20 allowance to Permit2 + const erc20Allowance = await publicClient.readContract({ + address: USDC_ADDRESS, + abi: ERC20_ALLOWANCE_ABI, + functionName: "allowance", + args: [env.EOA_ADDRESS as Hex, PERMIT2_ADDRESS], + }); + + let erc20ApproveResult: { gasUsed: bigint } | null = null; + + if (erc20Allowance >= swapAmount) { + print("βœ“ ERC20 allowance to Permit2 already sufficient", `${erc20Allowance} >= ${swapAmount}`); + } else { + print(`ERC20 allowance insufficient (${erc20Allowance} < ${swapAmount})`, "Approving..."); + + const approveNonce = await userClient.getNonce(); + print(`Current nonce: ${approveNonce}`, ""); + + // Build the ERC20 approval to Permit2 + const erc20ApprovalParams = buildTokenApproval( + USDC_ADDRESS, + PERMIT2_ADDRESS, + BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), // Max approval + ); + + // User signs the ERC20 approval intent + const erc20ApproveIntent = await userClient + .createIntent() + .setTarget(erc20ApprovalParams.outputContract) + .withValue(erc20ApprovalParams.value ?? 0n) + .withCallData(erc20ApprovalParams.callData) + .sign(approveNonce); + + print("βœ“ ERC20 approval intent signed by user", ""); + + // Paymaster executes the ERC20 approval + print("Executing USDC approval to Permit2...", ""); + erc20ApproveResult = await paymasterClient.execute(erc20ApproveIntent); + print("ERC20 Approval transaction sent", (erc20ApproveResult as any).txHash); + print("βœ… ERC20 Approval SUCCEEDED", ""); + print( + "Confirmed", + `Block: ${(erc20ApproveResult as any).blockNumber}, Gas: ${erc20ApproveResult.gasUsed}`, + ); + print("", `${explorerUrl}/tx/${(erc20ApproveResult as any).txHash}`); + + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + + // ===== Step 2b: Approve Universal Router on Permit2 (if needed) ===== + print("--- Step 2b: Check/Approve Universal Router on Permit2 ---", ""); + + // Check existing Permit2 allowance for Universal Router + const [permit2Amount, permit2Expiry] = await publicClient.readContract({ + address: PERMIT2_ADDRESS, + abi: PERMIT2_ABI, + functionName: "allowance", + args: [env.EOA_ADDRESS as Hex, USDC_ADDRESS, UNIVERSAL_ROUTER_ADDRESS], + }); + + const currentTime = Math.floor(Date.now() / 1000); + let permit2ApproveResult: { gasUsed: bigint } | null = null; + + if (permit2Amount >= swapAmount && permit2Expiry > currentTime) { + print( + "βœ“ Permit2 allowance for Universal Router already sufficient", + `Amount: ${permit2Amount} >= ${swapAmount}, Expires: ${new Date(Number(permit2Expiry) * 1000).toISOString()}`, + ); + } else { + const reason = permit2Amount < swapAmount + ? `Amount insufficient (${permit2Amount} < ${swapAmount})` + : `Expired (${permit2Expiry} <= ${currentTime})`; + print(`Permit2 allowance needs update: ${reason}`, "Approving..."); + + const permit2ApproveNonce = await userClient.getNonce(); + print(`Current nonce: ${permit2ApproveNonce}`, ""); + + // Permit2 expiration: 30 days from now + const permit2Expiration = Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30; + + // Build the Permit2 approve call with a larger amount for future swaps + const approveAmount = swapAmount * 1000n; // Approve 1000x the swap amount for future use + const permit2ApproveCallData = encodeAbiParameters( + [ + { type: "address" }, + { type: "address" }, + { type: "uint160" }, + { type: "uint48" }, + ], + [ + USDC_ADDRESS, + UNIVERSAL_ROUTER_ADDRESS, + approveAmount, + permit2Expiration, + ], + ); + + // Prepend the function selector for approve(address,address,uint160,uint48) + const permit2ApproveSelector = "0x87517c45" as Hex; // keccak256("approve(address,address,uint160,uint48)")[:4] + const fullPermit2ApproveCallData = concat([permit2ApproveSelector, permit2ApproveCallData]); + + // User signs the Permit2 approval intent + const permit2ApproveIntent = await userClient + .createIntent() + .setTarget(PERMIT2_ADDRESS) + .withValue(0n) + .withCallData(fullPermit2ApproveCallData) + .sign(permit2ApproveNonce); + + print("βœ“ Permit2 approval intent signed by user", ""); + + // Paymaster executes the Permit2 approval + print("Executing Permit2 approval for Universal Router...", ""); + permit2ApproveResult = await paymasterClient.execute(permit2ApproveIntent); + print("Permit2 Approval transaction sent", (permit2ApproveResult as any).txHash); + print("βœ… Permit2 Approval SUCCEEDED", ""); + print( + "Confirmed", + `Block: ${(permit2ApproveResult as any).blockNumber}, Gas: ${permit2ApproveResult.gasUsed}`, + ); + print("", `${explorerUrl}/tx/${(permit2ApproveResult as any).txHash}`); + + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + + // ===== Step 2c: Execute the Swap ===== + print("--- Step 2c: Execute Swap ---", ""); + + const swapNonce = await userClient.getNonce(); + print(`Current nonce: ${swapNonce}`, ""); + + // User signs the swap intent + const swapIntent = await userClient + .createIntent() + .setTarget(UNIVERSAL_ROUTER_ADDRESS) + .withValue(0n) // No ETH value needed for USDC β†’ WETH swap + .withCallData(fullSwapCallData) + .sign(swapNonce); + + print("βœ“ Swap intent signed by user", ""); + + // Paymaster executes the swap + print("Executing swap via gas station...", ""); + const swapResult = await paymasterClient.execute(swapIntent); + print("Swap transaction sent", swapResult.txHash); + print("βœ… Swap SUCCEEDED", ""); + print( + "Confirmed", + `Block: ${swapResult.blockNumber}, Gas: ${swapResult.gasUsed}`, + ); + + print("===== USDC β†’ ETH Swap Complete =====", ""); + print( + `βœ… Successfully swapped ${USDC_AMOUNT} USDC for WETH`, + `TX: ${explorerUrl}/tx/${swapResult.txHash}`, + ); + const erc20Gas = erc20ApproveResult?.gasUsed ?? 0n; + const permit2Gas = permit2ApproveResult?.gasUsed ?? 0n; + const totalGas = erc20Gas + permit2Gas + swapResult.gasUsed; + + print( + "Gas usage breakdown", + `ERC20 Approve: ${erc20Gas} + Permit2 Approve: ${permit2Gas} + Swap: ${swapResult.gasUsed} = ${totalGas} gas units`, + ); + + if (erc20Gas === 0n && permit2Gas === 0n) { + print("πŸ’‘ Tip", "Approvals were skipped because they already exist. Only the swap was executed!"); + } + print( + "Note", + "You received WETH. To get native ETH, an additional unwrap step would be needed.", + ); +}; + +main(); diff --git a/examples/tk-gas-station/src/transferUSDCReimbursable.ts b/examples/tk-gas-station/src/transferUSDCReimbursable.ts new file mode 100644 index 000000000..1238dcaaf --- /dev/null +++ b/examples/tk-gas-station/src/transferUSDCReimbursable.ts @@ -0,0 +1,262 @@ +import { resolve } from "path"; +import * as dotenv from "dotenv"; +import { z } from "zod"; +import { parseArgs } from "node:util"; +import { parseUnits, createWalletClient, http, type Hex } from "viem"; +import { Turnkey as TurnkeyServerSDK } from "@turnkey/sdk-server"; +import { createAccount } from "@turnkey/viem"; +import { + GasStationClient, + buildTokenTransfer, + CHAIN_PRESETS, + type ChainPreset, + type ReimbursableExecutionIntent, +} from "@turnkey/gas-station"; +import { print } from "./utils"; + +dotenv.config({ path: resolve(process.cwd(), ".env.local") }); + +// Transfer amount configuration +const USDC_AMOUNT = "0.01"; // 1 penny in USDC +const INITIAL_DEPOSIT_USDC = "1"; // 1 USDC initial deposit for gas (excess refunded) +const TRANSACTION_GAS_LIMIT = 200000n; // Gas limit in wei for the inner transaction + +// Parse command line arguments +const { values } = parseArgs({ + args: process.argv.slice(2), + options: { + chain: { + type: "string", + short: "c", + default: "base", + }, + }, +}); + +// Validate chain argument +const validChains = ["base", "mainnet"] as const; +type ValidChain = (typeof validChains)[number]; + +if (!validChains.includes(values.chain as ValidChain)) { + console.error( + `Invalid chain: ${values.chain}. Valid options: ${validChains.join(", ")}`, + ); + process.exit(1); +} + +const selectedChain = values.chain as ValidChain; + +// Map chain selection to chain presets +const chainPresetMap: Record = { + base: CHAIN_PRESETS.BASE_MAINNET, + mainnet: CHAIN_PRESETS.ETHEREUM_MAINNET, +}; + +const preset = chainPresetMap[selectedChain]; + +const envSchema = z.object({ + BASE_URL: z.string().url(), + API_PRIVATE_KEY: z.string().min(1), + API_PUBLIC_KEY: z.string().min(1), + ORGANIZATION_ID: z.string().min(1), + EOA_ADDRESS: z.string().min(1), + PAYMASTER_ADDRESS: z.string().min(1), + ETH_RPC_URL: z.string().url(), + BASE_RPC_URL: z.string().url(), +}); + +const env = envSchema.parse(process.env); + +print( + `🌐 Using ${selectedChain.toUpperCase()} network`, + `USDC: ${preset.tokens?.USDC}`, +); + +const turnkeyClient = new TurnkeyServerSDK({ + apiBaseUrl: env.BASE_URL, + apiPrivateKey: env.API_PRIVATE_KEY, + apiPublicKey: env.API_PUBLIC_KEY, + defaultOrganizationId: env.ORGANIZATION_ID, +}); + +/** + * Demonstrates USDC transfer using the Reimbursable Gas Station pattern. + * In this flow, the EOA pays for gas in USDC instead of the paymaster paying. + * + * Key differences from regular gas station: + * 1. EOA signs TWO signatures: + * - Execution signature (same as regular flow) - authorizes the transaction + * - Session signature (new!) - authorizes USDC transfers for gas payment + * 2. Contract pulls USDC from EOA, executes transaction, and refunds excess + * 3. Paymaster submits the transaction but EOA pays for gas in USDC + * 4. Session signature can be cached and reused across multiple transactions + */ +const main = async () => { + const rpcUrl = selectedChain === "base" ? env.BASE_RPC_URL : env.ETH_RPC_URL; + + // Create viem wallet clients with Turnkey accounts + const userAccount = await createAccount({ + client: turnkeyClient.apiClient(), + organizationId: env.ORGANIZATION_ID, + signWith: env.EOA_ADDRESS as Hex, + }); + + const paymasterAccount = await createAccount({ + client: turnkeyClient.apiClient(), + organizationId: env.ORGANIZATION_ID, + signWith: env.PAYMASTER_ADDRESS as Hex, + }); + + const userWalletClient = createWalletClient({ + account: userAccount, + chain: preset.chain, + transport: http(rpcUrl), + }); + + const paymasterWalletClient = createWalletClient({ + account: paymasterAccount, + chain: preset.chain, + transport: http(rpcUrl), + }); + + // Create Gas Station clients with the viem wallet clients + const userClient = new GasStationClient({ + walletClient: userWalletClient, + }); + + const paymasterClient = new GasStationClient({ + walletClient: paymasterWalletClient, + }); + + // Explorer URL for displaying transaction links + const explorerUrl = + selectedChain === "base" ? "https://basescan.org" : "https://etherscan.io"; + + // Step 1: Check if EOA is already authorized, authorize if needed + const isAuthorized = await userClient.isAuthorized(); + + if (!isAuthorized) { + print("===== Starting EIP-7702 Authorization =====", ""); + print("EOA not yet authorized", "Starting authorization..."); + + // Step 1: User signs the authorization + print("User signing authorization...", ""); + const authorization = await userClient.signAuthorization(); + + // Step 2: Paymaster submits the authorization transaction + const authResult = await paymasterClient.submitAuthorizations([ + authorization, + ]); + + print("Authorization transaction sent", authResult.txHash); + print("Waiting for confirmation...", ""); + print("βœ… Authorization SUCCEEDED", ""); + print( + "βœ… Authorization complete", + `${explorerUrl}/tx/${authResult.txHash}`, + ); + + // This delay helps ensure proper sequencing of on-chain state after authorization. + await new Promise((resolve) => setTimeout(resolve, 2000)); + } else { + print("βœ“ EOA already authorized", "Skipping authorization"); + } + + // Step 2: Sign session signature for USDC transfer authorization + print("===== Creating Session Signature =====", ""); + + const usdcAddress = preset.tokens?.USDC; + if (!usdcAddress) { + throw new Error(`USDC address not configured for ${selectedChain}`); + } + + const nonce = await userClient.getNonce(); + print(`Current nonce: ${nonce}`, ""); + + // Get the reimbursable contract address from the client + const reimbursableContract = (userClient as any).reimbursableContract as Hex; + + const initialDepositUSDC = parseUnits(INITIAL_DEPOSIT_USDC, 6); // 6 decimals for USDC + + // Sign session signature to authorize USDC transfer for gas payment + // Note: The session signature does NOT commit to a specific amount + // It's a general authorization for the reimbursable contract to interact with USDC + const sessionSignature = await userClient + .createIntent() + .signSessionForUSDCTransfer( + nonce, + usdcAddress, + reimbursableContract, + ); + + print( + "βœ“ Session signature created", + `Authorizes USDC transfers for gas payment`, + ); + + // Step 3: Create execution parameters for USDC transfer + print("===== Starting USDC Transfer with Reimbursement =====", ""); + + const transferAmount = parseUnits(USDC_AMOUNT, 6); // 6 decimals for USDC + + // Build the execution parameters using the helper + const executionParams = buildTokenTransfer( + usdcAddress, + env.PAYMASTER_ADDRESS as Hex, + transferAmount, + ); + + print( + `Executing USDC transfer`, + `${transferAmount} units (${USDC_AMOUNT} USDC) to ${env.PAYMASTER_ADDRESS}`, + ); + + // Step 3b: User creates and signs the execution intent + const executionIntent = await userClient + .createIntent() + .setTarget(executionParams.outputContract) + .withValue(executionParams.value ?? 0n) + .withCallData(executionParams.callData) + .sign(nonce); + + print("βœ“ Execution intent signed by user", ""); + + // Create the reimbursable execution intent (includes both signatures!) + const reimbursableIntent: ReimbursableExecutionIntent = { + ...executionIntent, + initialDepositUSDC, + transactionGasLimitWei: TRANSACTION_GAS_LIMIT, + sessionSignature, + }; + + print("βœ“ Reimbursable intent created", "Includes execution + session signatures"); + + // Step 4: Paymaster executes with reimbursement (EOA pays for gas in USDC) + print("Executing intent via reimbursable gas station...", ""); + const result = await paymasterClient.executeWithReimbursement( + reimbursableIntent, + ); + + print("Execution transaction sent", result.txHash); + print("Waiting for confirmation...", ""); + print("βœ… Execution SUCCEEDED", ""); + print("Confirmed", `Block: ${result.blockNumber}, Gas: ${result.gasUsed}`); + + print("===== USDC Transfer Complete =====", ""); + print( + "βœ… Successfully transferred 0.01 USDC from EOA to paymaster", + `TX: ${explorerUrl}/tx/${result.txHash}`, + ); + print("Gas payment", `EOA deposited ${INITIAL_DEPOSIT_USDC} USDC for gas (excess refunded)`); + print("Gas usage", `${result.gasUsed} gas units`); + + // Optional: Demonstrate cached session signature usage + print("\n===== Demonstrating Cached Session Signature =====", ""); + print( + "Note", + "On subsequent calls, you can pass '0x' as sessionSignature to use cached signature", + ); +}; + +main(); + diff --git a/packages/gas-station/src/abi/reimbursable-gas-station.ts b/packages/gas-station/src/abi/reimbursable-gas-station.ts new file mode 100644 index 000000000..eaac5c58c --- /dev/null +++ b/packages/gas-station/src/abi/reimbursable-gas-station.ts @@ -0,0 +1,588 @@ +export const reimbursableGasStationAbi = [ + { + inputs: [ + { internalType: "address", name: "_priceFeed", type: "address" }, + { internalType: "address", name: "_tkGasDelegate", type: "address" }, + { + internalType: "address", + name: "_reimbursementAddress", + type: "address", + }, + { internalType: "address", name: "_reimbursementErc20", type: "address" }, + { internalType: "uint16", name: "_gasFeeBasisPoints", type: "uint16" }, + { internalType: "uint256", name: "_minimumGasFee", type: "uint256" }, + { internalType: "uint256", name: "_maxDepositLimit", type: "uint256" }, + { + internalType: "uint256", + name: "_minimumTransactionGasLimitWei", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { inputs: [], name: "GasLimitExceeded", type: "error" }, + { inputs: [], name: "InsufficientBalance", type: "error" }, + { inputs: [], name: "InvalidPrice", type: "error" }, + { inputs: [], name: "InvalidSessionSignature", type: "error" }, + { inputs: [], name: "NotDelegated", type: "error" }, + { inputs: [], name: "TransactionGasLimitTooLow", type: "error" }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "_target", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "_to", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "_ethAmount", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "_erc20", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "_spender", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "_approveAmount", + type: "uint256", + }, + { + indexed: false, + internalType: "bytes", + name: "_data", + type: "bytes", + }, + ], + name: "ApproveThenExecuteFailed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "_target", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "_callsLength", + type: "uint256", + }, + ], + name: "BatchExecutionFailed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "_target", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "_to", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "_ethAmount", + type: "uint256", + }, + { + indexed: false, + internalType: "bytes", + name: "_data", + type: "bytes", + }, + ], + name: "ExecutionFailed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "_target", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "_gasChange", + type: "uint256", + }, + ], + name: "GasChangeReturned", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "_from", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "_destination", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "_gasUsed", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "_reimbursementAmount", + type: "uint256", + }, + ], + name: "GasReimbursed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "_from", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "_amount", + type: "uint256", + }, + ], + name: "GasUnpaid", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "_from", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "_initialDepositERC20", + type: "uint256", + }, + ], + name: "InitialDeposit", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "_to", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "_amount", + type: "uint256", + }, + ], + name: "TransferFailed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "_to", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "_amount", + type: "uint256", + }, + ], + name: "TransferFailedUnclaimedStored", + type: "event", + }, + { + inputs: [], + name: "BASE_GAS_FEE_ERC20", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "ERC20_TRANSFER_SUCCEEDED_RETURN_DATA_CHECK", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "GAS_FEE_BASIS_POINTS", + outputs: [{ internalType: "uint16", name: "", type: "uint16" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "MAX_DEPOSIT_LIMIT_ERC20", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "MINIMUM_TRANSACTION_GAS_LIMIT_WEI", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "PRICE_FEED_DECIMALS", + outputs: [{ internalType: "uint8", name: "", type: "uint8" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "REIMBURSEMENT_ADDRESS", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "REIMBURSEMENT_ERC20", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "REIMBURSEMENT_ERC20_TOKEN", + outputs: [{ internalType: "contract IERC20", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "REIMBURSEMENT_TOKEN_DECIMALS", + outputs: [{ internalType: "uint8", name: "", type: "uint8" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "TEN_TO_18_PLUS_PRICE_FEED_DECIMALS", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "TEN_TO_REIMBURSEMENT_TOKEN_DECIMALS", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "TK_GAS_DELEGATE", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "_initialDepositERC20", type: "uint256" }, + { + internalType: "uint256", + name: "_transactionGasLimitWei", + type: "uint256", + }, + { + internalType: "bytes", + name: "_packedSessionSignatureData", + type: "bytes", + }, + { internalType: "address", name: "_target", type: "address" }, + { internalType: "address", name: "_to", type: "address" }, + { internalType: "uint256", name: "_ethAmount", type: "uint256" }, + { internalType: "address", name: "_erc20", type: "address" }, + { internalType: "address", name: "_spender", type: "address" }, + { internalType: "uint256", name: "_approveAmount", type: "uint256" }, + { internalType: "bytes", name: "_data", type: "bytes" }, + ], + name: "approveThenExecute", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "_initialDepositERC20", type: "uint256" }, + { + internalType: "uint256", + name: "_transactionGasLimitWei", + type: "uint256", + }, + { + internalType: "bytes", + name: "_packedSessionSignatureData", + type: "bytes", + }, + { internalType: "address", name: "_target", type: "address" }, + { internalType: "address", name: "_to", type: "address" }, + { internalType: "uint256", name: "_ethAmount", type: "uint256" }, + { internalType: "address", name: "_erc20", type: "address" }, + { internalType: "address", name: "_spender", type: "address" }, + { internalType: "uint256", name: "_approveAmount", type: "uint256" }, + { internalType: "bytes", name: "_data", type: "bytes" }, + ], + name: "approveThenExecuteReturns", + outputs: [{ internalType: "bytes", name: "result", type: "bytes" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "_initialDepositERC20", type: "uint256" }, + { + internalType: "uint256", + name: "_transactionGasLimitWei", + type: "uint256", + }, + { + internalType: "bytes", + name: "_packedSessionSignatureData", + type: "bytes", + }, + { internalType: "address", name: "_targetEoA", type: "address" }, + { internalType: "bytes", name: "_signature", type: "bytes" }, + { internalType: "uint128", name: "_counter", type: "uint128" }, + ], + name: "burnCounter", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "_initialDepositERC20", type: "uint256" }, + { + internalType: "uint256", + name: "_transactionGasLimitWei", + type: "uint256", + }, + { + internalType: "bytes", + name: "_packedSessionSignatureData", + type: "bytes", + }, + { internalType: "address", name: "_targetEoA", type: "address" }, + { internalType: "bytes", name: "_signature", type: "bytes" }, + { internalType: "uint128", name: "_nonce", type: "uint128" }, + ], + name: "burnNonce", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "_targetEoA", type: "address" }, + { internalType: "uint128", name: "_counter", type: "uint128" }, + ], + name: "checkSessionCounterExpired", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "claimUnclaimedGasReimbursements", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "_initialDepositERC20", type: "uint256" }, + { + internalType: "uint256", + name: "_transactionGasLimitWei", + type: "uint256", + }, + { + internalType: "bytes", + name: "_packedSessionSignatureData", + type: "bytes", + }, + { internalType: "address", name: "_target", type: "address" }, + { internalType: "address", name: "_to", type: "address" }, + { internalType: "uint256", name: "_ethAmount", type: "uint256" }, + { internalType: "bytes", name: "_data", type: "bytes" }, + ], + name: "execute", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "_initialDepositERC20", type: "uint256" }, + { + internalType: "uint256", + name: "_transactionGasLimitWei", + type: "uint256", + }, + { + internalType: "bytes", + name: "_packedSessionSignatureData", + type: "bytes", + }, + { internalType: "address", name: "_target", type: "address" }, + { + components: [ + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "value", type: "uint256" }, + { internalType: "bytes", name: "data", type: "bytes" }, + ], + internalType: "struct IBatchExecution.Call[]", + name: "_calls", + type: "tuple[]", + }, + { internalType: "bytes", name: "_data", type: "bytes" }, + ], + name: "executeBatch", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "_initialDepositERC20", type: "uint256" }, + { + internalType: "uint256", + name: "_transactionGasLimitWei", + type: "uint256", + }, + { + internalType: "bytes", + name: "_packedSessionSignatureData", + type: "bytes", + }, + { internalType: "address", name: "_target", type: "address" }, + { + components: [ + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "value", type: "uint256" }, + { internalType: "bytes", name: "data", type: "bytes" }, + ], + internalType: "struct IBatchExecution.Call[]", + name: "_calls", + type: "tuple[]", + }, + { internalType: "bytes", name: "_data", type: "bytes" }, + ], + name: "executeBatchReturns", + outputs: [{ internalType: "bytes[]", name: "result", type: "bytes[]" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "_initialDepositERC20", type: "uint256" }, + { + internalType: "uint256", + name: "_transactionGasLimitWei", + type: "uint256", + }, + { + internalType: "bytes", + name: "_packedSessionSignatureData", + type: "bytes", + }, + { internalType: "address", name: "_target", type: "address" }, + { internalType: "address", name: "_to", type: "address" }, + { internalType: "uint256", name: "_ethAmount", type: "uint256" }, + { internalType: "bytes", name: "_data", type: "bytes" }, + ], + name: "executeReturns", + outputs: [{ internalType: "bytes", name: "result", type: "bytes" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "_targetEoA", type: "address" }], + name: "getNonce", + outputs: [{ internalType: "uint128", name: "", type: "uint128" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "_targetEoA", type: "address" }], + name: "isDelegated", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "", type: "address" }], + name: "unclaimedGasReimbursements", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, +] as const; diff --git a/packages/gas-station/src/config.ts b/packages/gas-station/src/config.ts index b470a383b..40ffa5e9e 100644 --- a/packages/gas-station/src/config.ts +++ b/packages/gas-station/src/config.ts @@ -7,12 +7,15 @@ export const DEFAULT_DELEGATE_CONTRACT: Hex = "0x000066a00056CD44008768E2aF00696e19A30084"; export const DEFAULT_EXECUTION_CONTRACT: Hex = "0x00000000008c57a1CE37836a5e9d36759D070d8c"; +export const DEFAULT_REIMBURSABLE_USDC_CONTRACT: Hex = + "0x4c0a2998B4Dc7BAF418109b80E5dde7395703dcb"; // Type definitions export interface GasStationConfig { walletClient: WalletClient; delegateContract?: Hex; executionContract?: Hex; + reimbursableContract?: Hex; defaultGasLimit?: bigint; } @@ -51,6 +54,12 @@ export interface ApprovalExecutionIntent extends ExecutionIntent { approveAmount: bigint; // The amount to approve } +export interface ReimbursableExecutionIntent extends ExecutionIntent { + initialDepositUSDC: bigint; // Initial USDC deposit for gas (6 decimals), excess refunded + transactionGasLimitWei: bigint; // Gas limit in wei for the inner transaction + sessionSignature: Hex; // 85 bytes session signature +} + // Chain preset configurations export const CHAIN_PRESETS: Record = { BASE_MAINNET: { diff --git a/packages/gas-station/src/gasStationClient.ts b/packages/gas-station/src/gasStationClient.ts index f79f9943f..3a2b73741 100644 --- a/packages/gas-station/src/gasStationClient.ts +++ b/packages/gas-station/src/gasStationClient.ts @@ -9,14 +9,17 @@ import { type Hex, } from "viem"; import { gasStationAbi } from "./abi/gas-station"; +import { reimbursableGasStationAbi } from "./abi/reimbursable-gas-station"; import type { GasStationConfig, ExecutionIntent, ApprovalExecutionIntent, + ReimbursableExecutionIntent, } from "./config"; import { DEFAULT_DELEGATE_CONTRACT, DEFAULT_EXECUTION_CONTRACT, + DEFAULT_REIMBURSABLE_USDC_CONTRACT, } from "./config"; import { IntentBuilder } from "./intentBuilder"; import { @@ -29,6 +32,7 @@ export class GasStationClient { private publicClient: PublicClient; private delegateContract: Hex; private executionContract: Hex; + private reimbursableContract: Hex; constructor(config: GasStationConfig) { this.walletClient = config.walletClient; @@ -40,6 +44,8 @@ export class GasStationClient { config.delegateContract ?? DEFAULT_DELEGATE_CONTRACT; this.executionContract = config.executionContract ?? DEFAULT_EXECUTION_CONTRACT; + this.reimbursableContract = + config.reimbursableContract ?? DEFAULT_REIMBURSABLE_USDC_CONTRACT; } /** @@ -404,4 +410,105 @@ export class GasStationClient { gasUsed: receipt.gasUsed, }; } + + /** + * Sign a reimbursable execution transaction (paymaster signs, doesn't send). + * This is useful for testing policies - the paymaster attempts to sign the execution + * but doesn't actually broadcast it to the network. + * Call this with a paymaster client to test if the paymaster can sign the execution. + */ + async signReimbursableExecution( + intent: ReimbursableExecutionIntent, + ): Promise { + // Pack the execution data (signature, nonce, deadline, args) + const packedExecutionData = packExecutionData({ + signature: intent.signature, + nonce: intent.nonce, + deadline: intent.deadline, + args: intent.callData, + }); + + const callData = encodeFunctionData({ + abi: reimbursableGasStationAbi, + functionName: "executeReturns", + args: [ + intent.initialDepositUSDC, + intent.transactionGasLimitWei, + intent.sessionSignature, + intent.eoaAddress, + intent.outputContract, + intent.ethAmount, + packedExecutionData, + ], + }); + + const signedTx = await this.walletClient.signTransaction({ + to: this.reimbursableContract, + data: callData, + gas: BigInt(300000), + type: "eip1559", + account: this.walletClient.account, + chain: this.walletClient.chain, + }); + + return signedTx; + } + + /** + * Execute with reimbursement through the reimbursable gas station contract. + * The EOA pays for gas in USDC rather than the paymaster covering the cost. + * The contract pulls initialDepositUSDC from the EOA, executes the transaction + * with a gas limit of transactionGasLimitWei, and refunds any unused USDC. + * Call this with a paymaster client to submit the transaction (paymaster only pays initial gas). + */ + async executeWithReimbursement( + intent: ReimbursableExecutionIntent, + ): Promise<{ txHash: Hex; blockNumber: bigint; gasUsed: bigint }> { + // Pack the execution data (signature, nonce, deadline, args) + const packedExecutionData = packExecutionData({ + signature: intent.signature, + nonce: intent.nonce, + deadline: intent.deadline, + args: intent.callData, + }); + + const txHash = await this.walletClient.sendTransaction({ + to: this.reimbursableContract, + data: encodeFunctionData({ + abi: reimbursableGasStationAbi, + functionName: "executeReturns", + args: [ + intent.initialDepositUSDC, + intent.transactionGasLimitWei, + intent.sessionSignature, + intent.eoaAddress, + intent.outputContract, + intent.ethAmount, + packedExecutionData, + ], + }), + gas: BigInt(300000), + account: this.walletClient.account, + }); + + const receipt = await this.publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + + if (receipt.status !== "success") { + // Try to get the revert reason if available + const revertReason = await this.getRevertReason(txHash); + throw new Error( + `Reimbursable execution failed: ${revertReason || "Transaction reverted"}. ` + + `Gas used: ${receipt.gasUsed}/${receipt.cumulativeGasUsed}. ` + + `Transaction hash: ${txHash}`, + ); + } + + return { + txHash, + blockNumber: receipt.blockNumber, + gasUsed: receipt.gasUsed, + }; + } } diff --git a/packages/gas-station/src/gasStationUtils.ts b/packages/gas-station/src/gasStationUtils.ts index 303bf9864..763f81b65 100644 --- a/packages/gas-station/src/gasStationUtils.ts +++ b/packages/gas-station/src/gasStationUtils.ts @@ -175,3 +175,29 @@ export function packExecutionData({ args, // variable length ]); } + +/** + * Packs session signature data for the reimbursable gas station. + * Used to authorize USDC transfers for gas payment. + * + * Packed data format: + * Layout: [signature(65)][nonce(16)][deadline(4)] + * - signature: bytes 0-64 (65 bytes) + * - nonce: bytes 65-80 (16 bytes, uint128) + * - deadline: bytes 81-84 (4 bytes, uint32) + */ +export function packSessionSignature({ + signature, + nonce, + deadline, +}: { + signature: Hex; + nonce: bigint; + deadline: number; +}): Hex { + return concat([ + signature, // 65 bytes + pad(toHex(nonce), { size: 16 }), // 16 bytes (uint128) + pad(toHex(deadline), { size: 4 }), // 4 bytes (uint32) + ]); +} diff --git a/packages/gas-station/src/index.ts b/packages/gas-station/src/index.ts index b32ce6759..58f3cfffa 100644 --- a/packages/gas-station/src/index.ts +++ b/packages/gas-station/src/index.ts @@ -10,6 +10,7 @@ export { buildETHTransferFromEther, buildContractCall, packExecutionData, + packSessionSignature, ERC20_ABI, } from "./gasStationUtils"; @@ -23,6 +24,7 @@ export { createCustomPreset, DEFAULT_EXECUTION_CONTRACT, DEFAULT_DELEGATE_CONTRACT, + DEFAULT_REIMBURSABLE_USDC_CONTRACT, } from "./config"; // Policy utilities @@ -41,4 +43,9 @@ export type { ContractCallParams, ExecutionIntent, ApprovalExecutionIntent, + ReimbursableExecutionIntent, } from "./config"; + +// ABI exports +export { gasStationAbi } from "./abi/gas-station"; +export { reimbursableGasStationAbi } from "./abi/reimbursable-gas-station"; diff --git a/packages/gas-station/src/intentBuilder.ts b/packages/gas-station/src/intentBuilder.ts index e5bc6fb33..bebfaf512 100644 --- a/packages/gas-station/src/intentBuilder.ts +++ b/packages/gas-station/src/intentBuilder.ts @@ -11,7 +11,7 @@ import type { ExecutionIntent, ApprovalExecutionIntent, } from "./config"; -import { ERC20_ABI } from "./gasStationUtils"; +import { ERC20_ABI, packSessionSignature } from "./gasStationUtils"; interface IntentBuilderConfig { eoaWalletClient: WalletClient; @@ -260,6 +260,61 @@ export class IntentBuilder { }; } + /** + * Signs a session signature for USDC transfer authorization in the reimbursable gas station. + * This authorizes the reimbursable contract to interact with the USDC contract on behalf of the EOA. + * The session signature does NOT commit to a specific amount - amounts are specified at execution time. + * This allows the same session signature to be cached and reused for multiple transactions. + * Returns an 85-byte packed signature that can be passed to executeWithReimbursement(). + */ + async signSessionForUSDCTransfer( + currentNonce: bigint, + usdcAddress: Hex, + reimbursableContract: Hex, + sessionDeadline?: number, + ): Promise { + const nonce = this.nonce ?? currentNonce; + // Default deadline: 1 hour from now + const deadline = + sessionDeadline ?? Math.floor(Date.now() / 1000) + 60 * 60; + + // EIP-712 domain and types for session execution + const domain = { + name: "TKGasDelegate", + version: "1", + chainId: this.config.chainId, + verifyingContract: this.config.eoaAddress, + }; + + // Based on hashSessionExecution from the delegate contract + // keccak256("SessionExecution(uint128 counter,uint32 deadline,address sender,address to)") + const types = { + SessionExecution: [ + { name: "counter", type: "uint128" }, + { name: "deadline", type: "uint32" }, + { name: "sender", type: "address" }, + { name: "to", type: "address" }, + ], + }; + + const message = { + counter: nonce, + deadline, + sender: reimbursableContract, + to: usdcAddress, + }; + + const signature = await this.config.eoaWalletClient.signTypedData({ + account: this.config.eoaWalletClient.account, + domain, + types, + primaryType: "SessionExecution", + message, + }); + + return packSessionSignature({ signature, nonce, deadline }); + } + // Static factory method for quick intent creation static create(config: IntentBuilderConfig): IntentBuilder { return new IntentBuilder(config); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b67a54d70..f1b05a1de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -848,6 +848,15 @@ importers: '@turnkey/viem': specifier: workspace:* version: link:../../packages/viem + '@uniswap/sdk-core': + specifier: ^6.0.0 + version: 6.1.1 + '@uniswap/universal-router-sdk': + specifier: ^3.0.0 + version: 3.4.0(bufferutil@4.0.9)(hardhat@2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@24.7.1)(typescript@5.4.3))(typescript@5.4.3)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10) + '@uniswap/v3-sdk': + specifier: ^3.9.0 + version: 3.25.2(hardhat@2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@24.7.1)(typescript@5.4.3))(typescript@5.4.3)(utf-8-validate@5.0.10)) dotenv: specifier: ^16.0.3 version: 16.0.3 @@ -6456,6 +6465,9 @@ packages: '@openzeppelin/contracts@3.4.2-solc-0.7': resolution: {integrity: sha512-W6QmqgkADuFcTLzHL8vVoNBtkwjvQRpYIAom7KiUNoLKghyx3FgH0GBjt8NRvigV1ZmMOBllvE1By1C+bi8WpA==} + '@openzeppelin/contracts@4.7.0': + resolution: {integrity: sha512-52Qb+A1DdOss8QvJrijYYPSf32GUg2pGaG/yCxtaA3cu4jduouTdg4XZSMLW9op54m1jH7J8hoajhHKOPsoJFw==} + '@openzeppelin/contracts@5.0.2': resolution: {integrity: sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA==} @@ -8874,24 +8886,54 @@ packages: resolution: {integrity: sha512-f6UIliwBbRsgVLxIaBANF6w09tYqc6Y/qXdsrbEmXHyFA7ILiKrIwRFXe1yOg8M3cksgVsO9N7yuL2DdCGQKBA==} engines: {node: '>=10'} + '@uniswap/permit2-sdk@1.4.0': + resolution: {integrity: sha512-l/aGhfhB93M76vXs4eB8QNwhELE6bs66kh7F1cyobaPtINaVpMmlJv+j3KmHeHwAZIsh7QXyYzhDxs07u0Pe4Q==} + + '@uniswap/router-sdk@1.23.0': + resolution: {integrity: sha512-KkHoMauTZh2N44sOU0ZuYseNNn9nAvaU57HwyCWjtwZdA7HaXtACfIRJbQvnkNNuALJfzHNkuv2aFyPSjNNmMw==} + '@uniswap/sdk-core@3.2.6': resolution: {integrity: sha512-MvH/3G0W0sM2g7XjaUy9qU7IabxL/KQp/ucU0AQGpVxiTaAhmVRtsjkkv9UDyzpIXVrmevl4kRgV7KKE29UuXA==} engines: {node: '>=10'} deprecated: breaking change required major version bump + '@uniswap/sdk-core@5.9.0': + resolution: {integrity: sha512-OME7WR6+5QwQs45A2079r+/FS0zU944+JCQwUX9GyIriCxqw2pGu4F9IEqmlwD+zSIMml0+MJnJJ47pFgSyWDw==} + engines: {node: '>=10'} + + '@uniswap/sdk-core@6.1.1': + resolution: {integrity: sha512-S9D5NTn7vV+wYwXbKOmYVjJidgmKY6zUsG5KGlQO4fNvcIde1TtVgtMXJl06qv1JeJKbGnzkIAZG4R82lSVZCg==} + engines: {node: '>=10'} + '@uniswap/sdk-core@7.7.2': resolution: {integrity: sha512-0KqXw+y0opBo6eoPAEoLHEkNpOu0NG9gEk7GAYIGok+SHX89WlykWsRYeJKTg9tOwhLpcG9oHg8xZgQ390iOrA==} engines: {node: '>=10'} + '@uniswap/sdk-core@7.9.0': + resolution: {integrity: sha512-HHUFNK3LMi4KMQCAiHkdUyL62g/nrZLvNT44CY8RN4p8kWO6XYWzqdQt6OcjCsIbhMZ/Ifhe6Py5oOoccg/jUQ==} + engines: {node: '>=10'} + '@uniswap/swap-router-contracts@1.3.1': resolution: {integrity: sha512-mh/YNbwKb7Mut96VuEtL+Z5bRe0xVIbjjiryn+iMMrK2sFKhR4duk/86mEz0UO5gSx4pQIw9G5276P5heY/7Rg==} engines: {node: '>=10'} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + '@uniswap/universal-router-sdk@3.4.0': + resolution: {integrity: sha512-EB/NLIkuT2BCdKnh2wcXT0cmINjRoiskjibFclpheALHL49XSrB08H4k7KV3BP6+JNKLeTHekvTDdsMd9rs5TA==} + engines: {node: '>=14'} + + '@uniswap/universal-router@2.0.0-beta.1': + resolution: {integrity: sha512-DdaMHaoDyJoCwpH+BiRKw/w2vjZtZS+ekpyrhmIeOBK1L2QEVFj977BNo6t24WzriZ9mSuIKF69RjHdXDUgHsQ==} + engines: {node: '>=14'} + '@uniswap/v2-core@1.0.1': resolution: {integrity: sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q==} engines: {node: '>=10'} + '@uniswap/v2-sdk@4.16.0': + resolution: {integrity: sha512-USMm2qz1xhEX8R0dhd0mHzf6pz5aCLjbtud1ZyUBk+gshhUCFp6NW9UovH0L5hqrH03rTvmqQdfhHMW5m+Sosg==} + engines: {node: '>=10'} + '@uniswap/v3-core@1.0.0': resolution: {integrity: sha512-kSC4djMGKMHj7sLMYVnn61k9nu+lHjMIxgg9CDQT+s2QYLoA56GbSK9Oxr+qJXzzygbkrmuY6cwgP6cW2JXPFA==} engines: {node: '>=10'} @@ -8908,11 +8950,19 @@ packages: resolution: {integrity: sha512-0oiyJNGjUVbc958uZmAr+m4XBCjV7PfMs/OUeBv+XDl33MEYF/eH86oBhvqGDM8S/cYaK55tCXzoWkmRUByrHg==} engines: {node: '>=10'} + '@uniswap/v3-sdk@3.26.0': + resolution: {integrity: sha512-bcoWNE7ntNNTHMOnDPscIqtIN67fUyrbBKr6eswI2gD2wm5b0YYFBDeh+Qc5Q3117o9i8S7QdftqrU8YSMQUfQ==} + engines: {node: '>=10'} + '@uniswap/v3-staker@1.0.0': resolution: {integrity: sha512-JV0Qc46Px5alvg6YWd+UIaGH9lDuYG/Js7ngxPit1SPaIP30AlVer1UYB7BRYeUVVxE+byUyIeN5jeQ7LLDjIw==} engines: {node: '>=10'} deprecated: Please upgrade to 1.0.1 + '@uniswap/v4-sdk@1.23.0': + resolution: {integrity: sha512-WpnkNacNTe/qL4kj3DVC2nHaivUeuzYsWIvon+olAWYZyy+Frsnzfon/ZlznDifMPoV+im+MqYFsNQke4Vz3LA==} + engines: {node: '>=14'} + '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} cpu: [arm] @@ -18534,15 +18584,15 @@ snapshots: '@ethersproject/abi@5.7.0': dependencies: - '@ethersproject/address': 5.7.0 + '@ethersproject/address': 5.8.0 '@ethersproject/bignumber': 5.7.0 - '@ethersproject/bytes': 5.7.0 + '@ethersproject/bytes': 5.8.0 '@ethersproject/constants': 5.7.0 '@ethersproject/hash': 5.7.0 - '@ethersproject/keccak256': 5.7.0 + '@ethersproject/keccak256': 5.8.0 '@ethersproject/logger': 5.7.0 '@ethersproject/properties': 5.7.0 - '@ethersproject/strings': 5.7.0 + '@ethersproject/strings': 5.8.0 '@ethersproject/abi@5.8.0': dependencies: @@ -18559,7 +18609,7 @@ snapshots: '@ethersproject/abstract-provider@5.7.0': dependencies: '@ethersproject/bignumber': 5.7.0 - '@ethersproject/bytes': 5.7.0 + '@ethersproject/bytes': 5.8.0 '@ethersproject/logger': 5.7.0 '@ethersproject/networks': 5.7.1 '@ethersproject/properties': 5.7.0 @@ -18580,7 +18630,7 @@ snapshots: dependencies: '@ethersproject/abstract-provider': 5.7.0 '@ethersproject/bignumber': 5.7.0 - '@ethersproject/bytes': 5.7.0 + '@ethersproject/bytes': 5.8.0 '@ethersproject/logger': 5.7.0 '@ethersproject/properties': 5.7.0 @@ -18595,8 +18645,8 @@ snapshots: '@ethersproject/address@5.7.0': dependencies: '@ethersproject/bignumber': 5.7.0 - '@ethersproject/bytes': 5.7.0 - '@ethersproject/keccak256': 5.7.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/keccak256': 5.8.0 '@ethersproject/logger': 5.7.0 '@ethersproject/rlp': 5.7.0 @@ -18610,7 +18660,7 @@ snapshots: '@ethersproject/base64@5.7.0': dependencies: - '@ethersproject/bytes': 5.7.0 + '@ethersproject/bytes': 5.8.0 '@ethersproject/base64@5.8.0': dependencies: @@ -18618,12 +18668,12 @@ snapshots: '@ethersproject/basex@5.7.0': dependencies: - '@ethersproject/bytes': 5.7.0 + '@ethersproject/bytes': 5.8.0 '@ethersproject/properties': 5.7.0 '@ethersproject/bignumber@5.7.0': dependencies: - '@ethersproject/bytes': 5.7.0 + '@ethersproject/bytes': 5.8.0 '@ethersproject/logger': 5.7.0 bn.js: 5.2.2 @@ -18651,12 +18701,12 @@ snapshots: '@ethersproject/contracts@5.7.0': dependencies: - '@ethersproject/abi': 5.7.0 + '@ethersproject/abi': 5.8.0 '@ethersproject/abstract-provider': 5.7.0 '@ethersproject/abstract-signer': 5.7.0 - '@ethersproject/address': 5.7.0 + '@ethersproject/address': 5.8.0 '@ethersproject/bignumber': 5.7.0 - '@ethersproject/bytes': 5.7.0 + '@ethersproject/bytes': 5.8.0 '@ethersproject/constants': 5.7.0 '@ethersproject/logger': 5.7.0 '@ethersproject/properties': 5.7.0 @@ -18678,14 +18728,14 @@ snapshots: '@ethersproject/hash@5.7.0': dependencies: '@ethersproject/abstract-signer': 5.7.0 - '@ethersproject/address': 5.7.0 + '@ethersproject/address': 5.8.0 '@ethersproject/base64': 5.7.0 '@ethersproject/bignumber': 5.7.0 - '@ethersproject/bytes': 5.7.0 - '@ethersproject/keccak256': 5.7.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/keccak256': 5.8.0 '@ethersproject/logger': 5.7.0 '@ethersproject/properties': 5.7.0 - '@ethersproject/strings': 5.7.0 + '@ethersproject/strings': 5.8.0 '@ethersproject/hash@5.8.0': dependencies: @@ -18704,28 +18754,28 @@ snapshots: '@ethersproject/abstract-signer': 5.7.0 '@ethersproject/basex': 5.7.0 '@ethersproject/bignumber': 5.7.0 - '@ethersproject/bytes': 5.7.0 + '@ethersproject/bytes': 5.8.0 '@ethersproject/logger': 5.7.0 '@ethersproject/pbkdf2': 5.7.0 '@ethersproject/properties': 5.7.0 '@ethersproject/sha2': 5.7.0 '@ethersproject/signing-key': 5.7.0 - '@ethersproject/strings': 5.7.0 + '@ethersproject/strings': 5.8.0 '@ethersproject/transactions': 5.7.0 '@ethersproject/wordlists': 5.7.0 '@ethersproject/json-wallets@5.7.0': dependencies: '@ethersproject/abstract-signer': 5.7.0 - '@ethersproject/address': 5.7.0 - '@ethersproject/bytes': 5.7.0 + '@ethersproject/address': 5.8.0 + '@ethersproject/bytes': 5.8.0 '@ethersproject/hdnode': 5.7.0 - '@ethersproject/keccak256': 5.7.0 + '@ethersproject/keccak256': 5.8.0 '@ethersproject/logger': 5.7.0 '@ethersproject/pbkdf2': 5.7.0 '@ethersproject/properties': 5.7.0 '@ethersproject/random': 5.7.0 - '@ethersproject/strings': 5.7.0 + '@ethersproject/strings': 5.8.0 '@ethersproject/transactions': 5.7.0 aes-js: 3.0.0 scrypt-js: 3.0.1 @@ -18754,7 +18804,7 @@ snapshots: '@ethersproject/pbkdf2@5.7.0': dependencies: - '@ethersproject/bytes': 5.7.0 + '@ethersproject/bytes': 5.8.0 '@ethersproject/sha2': 5.7.0 '@ethersproject/properties@5.7.0': @@ -18769,11 +18819,11 @@ snapshots: dependencies: '@ethersproject/abstract-provider': 5.7.0 '@ethersproject/abstract-signer': 5.7.0 - '@ethersproject/address': 5.7.0 + '@ethersproject/address': 5.8.0 '@ethersproject/base64': 5.7.0 '@ethersproject/basex': 5.7.0 '@ethersproject/bignumber': 5.7.0 - '@ethersproject/bytes': 5.7.0 + '@ethersproject/bytes': 5.8.0 '@ethersproject/constants': 5.7.0 '@ethersproject/hash': 5.7.0 '@ethersproject/logger': 5.7.0 @@ -18782,7 +18832,7 @@ snapshots: '@ethersproject/random': 5.7.0 '@ethersproject/rlp': 5.7.0 '@ethersproject/sha2': 5.7.0 - '@ethersproject/strings': 5.7.0 + '@ethersproject/strings': 5.8.0 '@ethersproject/transactions': 5.7.0 '@ethersproject/web': 5.7.1 bech32: 1.1.4 @@ -18793,12 +18843,12 @@ snapshots: '@ethersproject/random@5.7.0': dependencies: - '@ethersproject/bytes': 5.7.0 + '@ethersproject/bytes': 5.8.0 '@ethersproject/logger': 5.7.0 '@ethersproject/rlp@5.7.0': dependencies: - '@ethersproject/bytes': 5.7.0 + '@ethersproject/bytes': 5.8.0 '@ethersproject/logger': 5.7.0 '@ethersproject/rlp@5.8.0': @@ -18808,7 +18858,7 @@ snapshots: '@ethersproject/sha2@5.7.0': dependencies: - '@ethersproject/bytes': 5.7.0 + '@ethersproject/bytes': 5.8.0 '@ethersproject/logger': 5.7.0 hash.js: 1.1.7 @@ -18820,7 +18870,7 @@ snapshots: '@ethersproject/signing-key@5.7.0': dependencies: - '@ethersproject/bytes': 5.7.0 + '@ethersproject/bytes': 5.8.0 '@ethersproject/logger': 5.7.0 '@ethersproject/properties': 5.7.0 bn.js: 5.2.2 @@ -18839,11 +18889,11 @@ snapshots: '@ethersproject/solidity@5.7.0': dependencies: '@ethersproject/bignumber': 5.7.0 - '@ethersproject/bytes': 5.7.0 - '@ethersproject/keccak256': 5.7.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/keccak256': 5.8.0 '@ethersproject/logger': 5.7.0 '@ethersproject/sha2': 5.7.0 - '@ethersproject/strings': 5.7.0 + '@ethersproject/strings': 5.8.0 '@ethersproject/solidity@5.8.0': dependencies: @@ -18868,11 +18918,11 @@ snapshots: '@ethersproject/transactions@5.7.0': dependencies: - '@ethersproject/address': 5.7.0 + '@ethersproject/address': 5.8.0 '@ethersproject/bignumber': 5.7.0 - '@ethersproject/bytes': 5.7.0 + '@ethersproject/bytes': 5.8.0 '@ethersproject/constants': 5.7.0 - '@ethersproject/keccak256': 5.7.0 + '@ethersproject/keccak256': 5.8.0 '@ethersproject/logger': 5.7.0 '@ethersproject/properties': 5.7.0 '@ethersproject/rlp': 5.7.0 @@ -18900,13 +18950,13 @@ snapshots: dependencies: '@ethersproject/abstract-provider': 5.7.0 '@ethersproject/abstract-signer': 5.7.0 - '@ethersproject/address': 5.7.0 + '@ethersproject/address': 5.8.0 '@ethersproject/bignumber': 5.7.0 - '@ethersproject/bytes': 5.7.0 + '@ethersproject/bytes': 5.8.0 '@ethersproject/hash': 5.7.0 '@ethersproject/hdnode': 5.7.0 '@ethersproject/json-wallets': 5.7.0 - '@ethersproject/keccak256': 5.7.0 + '@ethersproject/keccak256': 5.8.0 '@ethersproject/logger': 5.7.0 '@ethersproject/properties': 5.7.0 '@ethersproject/random': 5.7.0 @@ -18917,10 +18967,10 @@ snapshots: '@ethersproject/web@5.7.1': dependencies: '@ethersproject/base64': 5.7.0 - '@ethersproject/bytes': 5.7.0 + '@ethersproject/bytes': 5.8.0 '@ethersproject/logger': 5.7.0 '@ethersproject/properties': 5.7.0 - '@ethersproject/strings': 5.7.0 + '@ethersproject/strings': 5.8.0 '@ethersproject/web@5.8.0': dependencies: @@ -18932,11 +18982,11 @@ snapshots: '@ethersproject/wordlists@5.7.0': dependencies: - '@ethersproject/bytes': 5.7.0 + '@ethersproject/bytes': 5.8.0 '@ethersproject/hash': 5.7.0 '@ethersproject/logger': 5.7.0 '@ethersproject/properties': 5.7.0 - '@ethersproject/strings': 5.7.0 + '@ethersproject/strings': 5.8.0 '@farcaster/frame-core@0.0.26(typescript@5.4.3)': dependencies: @@ -20492,6 +20542,8 @@ snapshots: '@openzeppelin/contracts@3.4.2-solc-0.7': {} + '@openzeppelin/contracts@4.7.0': {} + '@openzeppelin/contracts@5.0.2': {} '@particle-network/analytics@1.0.2': @@ -25154,6 +25206,25 @@ snapshots: '@uniswap/lib@4.0.1-alpha': {} + '@uniswap/permit2-sdk@1.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + ethers: 5.7.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) + tiny-invariant: 1.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@uniswap/router-sdk@1.23.0(hardhat@2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@24.7.1)(typescript@5.4.3))(typescript@5.4.3)(utf-8-validate@5.0.10))': + dependencies: + '@ethersproject/abi': 5.8.0 + '@uniswap/sdk-core': 7.7.2 + '@uniswap/swap-router-contracts': 1.3.1(hardhat@2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@24.7.1)(typescript@5.4.3))(typescript@5.4.3)(utf-8-validate@5.0.10)) + '@uniswap/v2-sdk': 4.16.0 + '@uniswap/v3-sdk': 3.25.2(hardhat@2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@24.7.1)(typescript@5.4.3))(typescript@5.4.3)(utf-8-validate@5.0.10)) + '@uniswap/v4-sdk': 1.23.0(hardhat@2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@24.7.1)(typescript@5.4.3))(typescript@5.4.3)(utf-8-validate@5.0.10)) + transitivePeerDependencies: + - hardhat + '@uniswap/sdk-core@3.2.6': dependencies: '@ethersproject/address': 5.8.0 @@ -25163,6 +25234,30 @@ snapshots: tiny-invariant: 1.3.3 toformat: 2.0.0 + '@uniswap/sdk-core@5.9.0': + dependencies: + '@ethersproject/address': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/strings': 5.7.0 + big.js: 5.2.2 + decimal.js-light: 2.5.1 + jsbi: 3.2.5 + tiny-invariant: 1.3.3 + toformat: 2.0.0 + + '@uniswap/sdk-core@6.1.1': + dependencies: + '@ethersproject/address': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/strings': 5.7.0 + big.js: 5.2.2 + decimal.js-light: 2.5.1 + jsbi: 3.2.5 + tiny-invariant: 1.3.3 + toformat: 2.0.0 + '@uniswap/sdk-core@7.7.2': dependencies: '@ethersproject/address': 5.8.0 @@ -25175,6 +25270,18 @@ snapshots: tiny-invariant: 1.3.3 toformat: 2.0.0 + '@uniswap/sdk-core@7.9.0': + dependencies: + '@ethersproject/address': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/strings': 5.7.0 + big.js: 5.2.2 + decimal.js-light: 2.5.1 + jsbi: 3.2.5 + tiny-invariant: 1.3.3 + toformat: 2.0.0 + '@uniswap/swap-router-contracts@1.3.1(hardhat@2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@24.7.1)(typescript@5.4.3))(typescript@5.4.3)(utf-8-validate@5.0.10))': dependencies: '@openzeppelin/contracts': 3.4.2-solc-0.7 @@ -25186,8 +25293,41 @@ snapshots: transitivePeerDependencies: - hardhat + '@uniswap/universal-router-sdk@3.4.0(bufferutil@4.0.9)(hardhat@2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@24.7.1)(typescript@5.4.3))(typescript@5.4.3)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10)': + dependencies: + '@openzeppelin/contracts': 4.7.0 + '@uniswap/permit2-sdk': 1.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@uniswap/router-sdk': 1.23.0(hardhat@2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@24.7.1)(typescript@5.4.3))(typescript@5.4.3)(utf-8-validate@5.0.10)) + '@uniswap/sdk-core': 5.9.0 + '@uniswap/universal-router': 2.0.0-beta.1 + '@uniswap/v2-core': 1.0.1 + '@uniswap/v2-sdk': 4.16.0 + '@uniswap/v3-core': 1.0.0 + '@uniswap/v3-sdk': 3.25.2(hardhat@2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@24.7.1)(typescript@5.4.3))(typescript@5.4.3)(utf-8-validate@5.0.10)) + '@uniswap/v4-sdk': 1.23.0(hardhat@2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@24.7.1)(typescript@5.4.3))(typescript@5.4.3)(utf-8-validate@5.0.10)) + bignumber.js: 9.3.1 + ethers: 5.7.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - hardhat + - utf-8-validate + + '@uniswap/universal-router@2.0.0-beta.1': + dependencies: + '@openzeppelin/contracts': 5.0.2 + '@uniswap/v2-core': 1.0.1 + '@uniswap/v3-core': 1.0.0 + '@uniswap/v2-core@1.0.1': {} + '@uniswap/v2-sdk@4.16.0': + dependencies: + '@ethersproject/address': 5.8.0 + '@ethersproject/solidity': 5.8.0 + '@uniswap/sdk-core': 7.9.0 + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + '@uniswap/v3-core@1.0.0': {} '@uniswap/v3-core@1.0.1': {} @@ -25213,12 +25353,35 @@ snapshots: transitivePeerDependencies: - hardhat + '@uniswap/v3-sdk@3.26.0(hardhat@2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@24.7.1)(typescript@5.4.3))(typescript@5.4.3)(utf-8-validate@5.0.10))': + dependencies: + '@ethersproject/abi': 5.8.0 + '@ethersproject/solidity': 5.8.0 + '@uniswap/sdk-core': 7.9.0 + '@uniswap/swap-router-contracts': 1.3.1(hardhat@2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@24.7.1)(typescript@5.4.3))(typescript@5.4.3)(utf-8-validate@5.0.10)) + '@uniswap/v3-periphery': 1.4.4 + '@uniswap/v3-staker': 1.0.0 + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + transitivePeerDependencies: + - hardhat + '@uniswap/v3-staker@1.0.0': dependencies: '@openzeppelin/contracts': 3.4.1-solc-0.7-2 '@uniswap/v3-core': 1.0.0 '@uniswap/v3-periphery': 1.4.4 + '@uniswap/v4-sdk@1.23.0(hardhat@2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@24.7.1)(typescript@5.4.3))(typescript@5.4.3)(utf-8-validate@5.0.10))': + dependencies: + '@ethersproject/solidity': 5.8.0 + '@uniswap/sdk-core': 7.9.0 + '@uniswap/v3-sdk': 3.26.0(hardhat@2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@24.7.1)(typescript@5.4.3))(typescript@5.4.3)(utf-8-validate@5.0.10)) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + transitivePeerDependencies: + - hardhat + '@unrs/resolver-binding-android-arm-eabi@1.11.1': optional: true From e86a169851ecd31113fa266ad24e9e1c78190615 Mon Sep 17 00:00:00 2001 From: Bijan Massoumi Date: Wed, 26 Nov 2025 16:23:50 -0500 Subject: [PATCH 2/4] Refactor USDC to WETH swap example to utilize combined approval via Permit2 and Uniswap Universal Router. Enhance documentation with detailed usage instructions and environment variable requirements. Streamline code for clarity and maintainability. --- examples/tk-gas-station/src/swapUSDCForETH.ts | 609 +++++++----------- 1 file changed, 235 insertions(+), 374 deletions(-) diff --git a/examples/tk-gas-station/src/swapUSDCForETH.ts b/examples/tk-gas-station/src/swapUSDCForETH.ts index 5d5d7fbec..bffb717d0 100644 --- a/examples/tk-gas-station/src/swapUSDCForETH.ts +++ b/examples/tk-gas-station/src/swapUSDCForETH.ts @@ -1,11 +1,42 @@ +/** + * USDC β†’ WETH Swap Example using Turnkey Gas Station + * + * This example demonstrates a gasless token swap using the Gas Station pattern + * with EIP-7702 authorization. It's ideal for users who hold USDC but no ETH. + * + * Flow (2 transactions total): + * 1. approveThenExecute: Atomically approves USDC to Permit2 AND sets up + * Permit2 allowance for Universal Router (combines 2 approvals into 1 tx) + * 2. execute: Performs the actual swap via Uniswap Universal Router + * + * The paymaster pays for all gas - the user only signs intents. + * + * Prerequisites: + * - EOA wallet with USDC balance on Base mainnet + * - Paymaster wallet with ETH for gas + * - Both wallets accessible via Turnkey + * + * Usage: + * pnpm tsx src/swapUSDCForETH.ts [--amount ] + * + * Environment variables required (in .env.local): + * - BASE_URL: Turnkey API base URL + * - API_PRIVATE_KEY: Turnkey API private key + * - API_PUBLIC_KEY: Turnkey API public key + * - ORGANIZATION_ID: Turnkey organization ID + * - EOA_ADDRESS: User wallet address (holds USDC) + * - PAYMASTER_ADDRESS: Paymaster wallet address (pays gas) + * - BASE_RPC_URL: Base mainnet RPC endpoint + */ + import { resolve } from "path"; import * as dotenv from "dotenv"; import { z } from "zod"; import { parseArgs } from "node:util"; import { parseUnits, + formatUnits, createWalletClient, - createPublicClient, http, encodeAbiParameters, encodePacked, @@ -15,158 +46,81 @@ import { } from "viem"; import { Turnkey as TurnkeyServerSDK } from "@turnkey/sdk-server"; import { createAccount } from "@turnkey/viem"; -import { GasStationClient, CHAIN_PRESETS, buildTokenApproval } from "@turnkey/gas-station"; +import { GasStationClient, CHAIN_PRESETS } from "@turnkey/gas-station"; +import { Token } from "@uniswap/sdk-core"; +import { FeeAmount } from "@uniswap/v3-sdk"; import { print } from "./utils"; -// Uniswap SDK imports for token definitions and types -import { Token, Percent } from "@uniswap/sdk-core"; -import { FeeAmount } from "@uniswap/v3-sdk"; +// ============================================================================ +// Configuration +// ============================================================================ dotenv.config({ path: resolve(process.cwd(), ".env.local") }); -// Swap configuration -const USDC_AMOUNT = "0.10"; // 10 cents USDC to swap -const MIN_ETH_OUT = 0n; // Accept any amount for demo (production should use a quote) -const SLIPPAGE_PERCENT = new Percent(50, 10000); // 0.5% slippage (for reference) - -// Chain ID for Base mainnet -const BASE_CHAIN_ID = 8453; - -// Contract addresses on Base mainnet -const UNIVERSAL_ROUTER_ADDRESS: Hex = - "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD"; // Universal Router on Base -const PERMIT2_ADDRESS: Hex = - "0x000000000022D473030F116dDEE9F6B43aC78BA3"; // Permit2 (same on all chains) -const WETH_ADDRESS: Hex = "0x4200000000000000000000000000000000000006"; -const USDC_ADDRESS: Hex = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; - -// Universal Router command codes -const V3_SWAP_EXACT_IN = 0x00; - -// Universal Router ABI for execute function -const UNIVERSAL_ROUTER_ABI = [ - { - name: "execute", - type: "function", - stateMutability: "payable", - inputs: [ - { name: "commands", type: "bytes" }, - { name: "inputs", type: "bytes[]" }, - { name: "deadline", type: "uint256" }, - ], - outputs: [], - }, -] as const; - -// Permit2 ABI for approve and allowance functions -const PERMIT2_ABI = [ - { - name: "approve", - type: "function", - stateMutability: "nonpayable", - inputs: [ - { name: "token", type: "address" }, - { name: "spender", type: "address" }, - { name: "amount", type: "uint160" }, - { name: "expiration", type: "uint48" }, - ], - outputs: [], - }, - { - name: "allowance", - type: "function", - stateMutability: "view", - inputs: [ - { name: "owner", type: "address" }, - { name: "token", type: "address" }, - { name: "spender", type: "address" }, - ], - outputs: [ - { name: "amount", type: "uint160" }, - { name: "expiration", type: "uint48" }, - { name: "nonce", type: "uint48" }, - ], - }, -] as const; - -// ERC20 ABI for allowance check -const ERC20_ALLOWANCE_ABI = [ - { - name: "allowance", - type: "function", - stateMutability: "view", - inputs: [ - { name: "owner", type: "address" }, - { name: "spender", type: "address" }, - ], - outputs: [{ name: "", type: "uint256" }], - }, -] as const; - -// Parse command line arguments -const { values } = parseArgs({ +// Parse CLI arguments +const { values: args } = parseArgs({ args: process.argv.slice(2), options: { - chain: { + amount: { type: "string", - short: "c", - default: "base", + short: "a", + default: "0.10", }, }, }); -// This example only supports Base mainnet -if (values.chain !== "base") { - console.error("This swap example only supports Base mainnet."); - process.exit(1); -} - -const preset = CHAIN_PRESETS.BASE_MAINNET; +const SWAP_AMOUNT_USDC = args.amount!; +// Environment validation const envSchema = z.object({ - BASE_URL: z.string().url(), - API_PRIVATE_KEY: z.string().min(1), - API_PUBLIC_KEY: z.string().min(1), - ORGANIZATION_ID: z.string().min(1), - EOA_ADDRESS: z.string().min(1), - PAYMASTER_ADDRESS: z.string().min(1), - BASE_RPC_URL: z.string().url(), + BASE_URL: z.string().url("BASE_URL must be a valid Turnkey API URL"), + API_PRIVATE_KEY: z.string().min(1, "API_PRIVATE_KEY is required"), + API_PUBLIC_KEY: z.string().min(1, "API_PUBLIC_KEY is required"), + ORGANIZATION_ID: z.string().min(1, "ORGANIZATION_ID is required"), + EOA_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, "EOA_ADDRESS must be a valid Ethereum address"), + PAYMASTER_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, "PAYMASTER_ADDRESS must be a valid Ethereum address"), + BASE_RPC_URL: z.string().url("BASE_RPC_URL must be a valid URL"), }); const env = envSchema.parse(process.env); -print( - `🌐 Using BASE network`, - `Swapping USDC for ETH via Uniswap Universal Router`, -); +// ============================================================================ +// Contract Addresses (Base Mainnet) +// ============================================================================ -const turnkeyClient = new TurnkeyServerSDK({ - apiBaseUrl: env.BASE_URL, - apiPrivateKey: env.API_PRIVATE_KEY, - apiPublicKey: env.API_PUBLIC_KEY, - defaultOrganizationId: env.ORGANIZATION_ID, -}); +const BASE_CHAIN_ID = 8453; +const EXPLORER_URL = "https://basescan.org"; + +const USDC_ADDRESS: Hex = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; +const WETH_ADDRESS: Hex = "0x4200000000000000000000000000000000000006"; +const PERMIT2_ADDRESS: Hex = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; +const UNIVERSAL_ROUTER_ADDRESS: Hex = "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD"; -// Define tokens using Uniswap SDK for type safety +// Token definitions for amount parsing const USDC_TOKEN = new Token(BASE_CHAIN_ID, USDC_ADDRESS, 6, "USDC", "USD Coin"); -const WETH_TOKEN = new Token(BASE_CHAIN_ID, WETH_ADDRESS, 18, "WETH", "Wrapped Ether"); + +// ============================================================================ +// Uniswap Encoding Helpers +// ============================================================================ + +// Universal Router command for V3 exact input swap +const V3_SWAP_EXACT_IN = 0x00; + +// Function selectors +const UNIVERSAL_ROUTER_EXECUTE_SELECTOR: Hex = "0x3593564c"; +const PERMIT2_APPROVE_SELECTOR: Hex = "0x87517c45"; /** - * Encodes the path for a V3 swap (tokenIn -> fee -> tokenOut). - * The path format is: [tokenIn, fee, tokenOut] packed together. + * Encodes the swap path for Uniswap V3 (tokenIn -> fee -> tokenOut) */ -function encodeV3Path(tokenIn: Hex, fee: FeeAmount, tokenOut: Hex): Hex { - return encodePacked( - ["address", "uint24", "address"], - [tokenIn, fee, tokenOut], - ); +function encodeV3SwapPath(tokenIn: Hex, fee: FeeAmount, tokenOut: Hex): Hex { + return encodePacked(["address", "uint24", "address"], [tokenIn, fee, tokenOut]); } /** - * Encodes the input for V3_SWAP_EXACT_IN command. - * Format: (address recipient, uint256 amountIn, uint256 amountOutMin, bytes path, bool payerIsUser) + * Encodes parameters for V3_SWAP_EXACT_IN command */ -function encodeV3SwapExactIn( +function encodeV3SwapExactInParams( recipient: Hex, amountIn: bigint, amountOutMin: bigint, @@ -186,31 +140,73 @@ function encodeV3SwapExactIn( } /** - * Demonstrates USDC to ETH swap using the Gas Station pattern with EIP-7702 authorization. - * Uses Uniswap Universal Router on Base mainnet with separate approve and execute steps. - * - * This is ideal for gasless users who hold USDC but no ETH: - * 1. EOA signs intent to approve USDC to Permit2 β†’ Paymaster executes - * 2. EOA signs intent to approve Universal Router on Permit2 β†’ Paymaster executes - * 3. EOA signs intent to execute swap β†’ Paymaster executes - * 4. EOA receives WETH in exchange for USDC + * Builds complete calldata for Universal Router execute */ -const main = async () => { - const rpcUrl = env.BASE_RPC_URL; +function buildUniversalRouterExecuteCalldata( + commands: Hex, + inputs: Hex[], + deadline: bigint, +): Hex { + const params = encodeAbiParameters( + [{ type: "bytes" }, { type: "bytes[]" }, { type: "uint256" }], + [commands, inputs, deadline], + ); + return concat([UNIVERSAL_ROUTER_EXECUTE_SELECTOR, params]); +} - // Create viem wallet clients with Turnkey accounts - const userAccount = await createAccount({ - client: turnkeyClient.apiClient(), - organizationId: env.ORGANIZATION_ID, - signWith: env.EOA_ADDRESS as Hex, - }); +/** + * Builds calldata for Permit2.approve(token, spender, amount, expiration) + */ +function buildPermit2ApproveCalldata( + token: Hex, + spender: Hex, + amount: bigint, + expiration: number, +): Hex { + const params = encodeAbiParameters( + [{ type: "address" }, { type: "address" }, { type: "uint160" }, { type: "uint48" }], + [token, spender, amount, expiration], + ); + return concat([PERMIT2_APPROVE_SELECTOR, params]); +} - const paymasterAccount = await createAccount({ - client: turnkeyClient.apiClient(), - organizationId: env.ORGANIZATION_ID, - signWith: env.PAYMASTER_ADDRESS as Hex, +// ============================================================================ +// Main Execution +// ============================================================================ + +async function main(): Promise { + print("πŸ”„ Turnkey Gas Station: USDC β†’ WETH Swap", ""); + print("Network", "Base Mainnet"); + print("Swap Amount", `${SWAP_AMOUNT_USDC} USDC`); + print("User Wallet", env.EOA_ADDRESS); + print("Paymaster", env.PAYMASTER_ADDRESS); + print("", ""); + + // Initialize Turnkey SDK + const turnkeyClient = new TurnkeyServerSDK({ + apiBaseUrl: env.BASE_URL, + apiPrivateKey: env.API_PRIVATE_KEY, + apiPublicKey: env.API_PUBLIC_KEY, + defaultOrganizationId: env.ORGANIZATION_ID, }); + // Create wallet clients + const [userAccount, paymasterAccount] = await Promise.all([ + createAccount({ + client: turnkeyClient.apiClient(), + organizationId: env.ORGANIZATION_ID, + signWith: env.EOA_ADDRESS as Hex, + }), + createAccount({ + client: turnkeyClient.apiClient(), + organizationId: env.ORGANIZATION_ID, + signWith: env.PAYMASTER_ADDRESS as Hex, + }), + ]); + + const preset = CHAIN_PRESETS.BASE_MAINNET; + const rpcUrl = env.BASE_RPC_URL; + const userWalletClient = createWalletClient({ account: userAccount, chain: preset.chain, @@ -223,272 +219,137 @@ const main = async () => { transport: http(rpcUrl), }); - // Create Gas Station clients with the viem wallet clients - const userClient = new GasStationClient({ - walletClient: userWalletClient, - }); + // Initialize Gas Station clients + const userClient = new GasStationClient({ walletClient: userWalletClient }); + const paymasterClient = new GasStationClient({ walletClient: paymasterWalletClient }); - const paymasterClient = new GasStationClient({ - walletClient: paymasterWalletClient, - }); + // ────────────────────────────────────────────────────────────────────────── + // Step 1: EIP-7702 Authorization (if needed) + // ────────────────────────────────────────────────────────────────────────── - // Create public client for reading on-chain data - const publicClient = createPublicClient({ - chain: preset.chain, - transport: http(rpcUrl), - }); + print("Step 1: EIP-7702 Authorization", ""); - const explorerUrl = "https://basescan.org"; - - // Step 1: Check if EOA is already authorized, authorize if needed const isAuthorized = await userClient.isAuthorized(); if (!isAuthorized) { - print("===== Starting EIP-7702 Authorization =====", ""); - print("EOA not yet authorized", "Starting authorization..."); + print("Status", "EOA not authorized, signing authorization..."); - print("User signing authorization...", ""); const authorization = await userClient.signAuthorization(); + const authResult = await paymasterClient.submitAuthorizations([authorization]); - const authResult = await paymasterClient.submitAuthorizations([ - authorization, - ]); - - print("Authorization transaction sent", authResult.txHash); - print("Waiting for confirmation...", ""); - print("βœ… Authorization SUCCEEDED", ""); - print( - "βœ… Authorization complete", - `${explorerUrl}/tx/${authResult.txHash}`, - ); - - // Delay to ensure proper sequencing of on-chain state after authorization - await new Promise((resolve) => setTimeout(resolve, 2000)); + print("βœ… Authorized", `${EXPLORER_URL}/tx/${authResult.txHash}`); + await delay(2000); } else { - print("βœ“ EOA already authorized", "Skipping authorization"); + print("βœ… Already authorized", "Skipping"); } - // Step 2: Execute USDC to ETH swap via Uniswap Universal Router - print("===== Starting USDC β†’ ETH Swap =====", ""); + print("", ""); - const swapAmount = parseUnits(USDC_AMOUNT, USDC_TOKEN.decimals); - const deadline = BigInt(Math.floor(Date.now() / 1000) + 60 * 20); // 20 minutes + // ────────────────────────────────────────────────────────────────────────── + // Step 2: Combined Approval (approveThenExecute) + // ────────────────────────────────────────────────────────────────────────── - // Encode the V3 swap path: USDC -> 0.05% fee -> WETH - const swapPath = encodeV3Path(USDC_ADDRESS, FeeAmount.LOW, WETH_ADDRESS); + print("Step 2: Combined Approval", ""); + print("Action", "ERC20 approve USDCβ†’Permit2 + Permit2 approve for Universal Router"); - // Encode the swap input for V3_SWAP_EXACT_IN - // recipient = EOA address, payerIsUser = true (USDC comes from msg.sender) - const swapInput = encodeV3SwapExactIn( - env.EOA_ADDRESS as Hex, - swapAmount, - MIN_ETH_OUT, - swapPath, - true, // payerIsUser - the EOA pays with their USDC - ); + const swapAmount = parseUnits(SWAP_AMOUNT_USDC, USDC_TOKEN.decimals); + const approveNonce = await userClient.getNonce(); - // Build the Universal Router execute calldata - const commands = toHex(new Uint8Array([V3_SWAP_EXACT_IN])); // Single command: V3_SWAP_EXACT_IN - const inputs = [swapInput]; // Corresponding input for the command + // Permit2 allowance: 1000x swap amount, expires in 30 days + const permit2AllowanceAmount = swapAmount * 1000n; + const permit2Expiration = Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30; - // Encode the execute function call - const swapCallData = encodeAbiParameters( - [ - { type: "bytes" }, - { type: "bytes[]" }, - { type: "uint256" }, - ], - [commands, inputs, deadline], + // Build Permit2.approve calldata (the "execute" part of approveThenExecute) + const permit2ApproveCalldata = buildPermit2ApproveCalldata( + USDC_ADDRESS, + UNIVERSAL_ROUTER_ADDRESS, + permit2AllowanceAmount, + permit2Expiration, ); - // Prepend the function selector for execute(bytes,bytes[],uint256) - const executeSelector = "0x3593564c" as Hex; // keccak256("execute(bytes,bytes[],uint256)")[:4] - const fullSwapCallData = concat([executeSelector, swapCallData]); + // Max ERC20 approval for USDCβ†’Permit2 (the "approve" part of approveThenExecute) + const maxErc20Approval = BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); - print( - `Swapping ${USDC_AMOUNT} USDC for ETH`, - `Recipient: ${env.EOA_ADDRESS}`, - ); - print( - "Swap parameters", - `Amount: ${swapAmount} (${USDC_AMOUNT} USDC), Fee tier: ${FeeAmount.LOW / 10000}%, Deadline: ${deadline}`, - ); + // Sign combined approval intent + const approvalIntent = await userClient + .createIntent() + .setTarget(PERMIT2_ADDRESS) + .withValue(0n) + .withCallData(permit2ApproveCalldata) + .signApprovalExecution(approveNonce, USDC_ADDRESS, PERMIT2_ADDRESS, maxErc20Approval); - // ===== Step 2a: Approve USDC to Permit2 (if needed) ===== - print("--- Step 2a: Check/Approve USDC to Permit2 ---", ""); + // Paymaster executes both approvals atomically + const approvalResult = await paymasterClient.approveThenExecute(approvalIntent); - // Check existing ERC20 allowance to Permit2 - const erc20Allowance = await publicClient.readContract({ - address: USDC_ADDRESS, - abi: ERC20_ALLOWANCE_ABI, - functionName: "allowance", - args: [env.EOA_ADDRESS as Hex, PERMIT2_ADDRESS], - }); + print("βœ… Approved", `${EXPLORER_URL}/tx/${approvalResult.txHash}`); + print("Gas Used", approvalResult.gasUsed.toString()); + await delay(2000); - let erc20ApproveResult: { gasUsed: bigint } | null = null; + print("", ""); - if (erc20Allowance >= swapAmount) { - print("βœ“ ERC20 allowance to Permit2 already sufficient", `${erc20Allowance} >= ${swapAmount}`); - } else { - print(`ERC20 allowance insufficient (${erc20Allowance} < ${swapAmount})`, "Approving..."); - - const approveNonce = await userClient.getNonce(); - print(`Current nonce: ${approveNonce}`, ""); - - // Build the ERC20 approval to Permit2 - const erc20ApprovalParams = buildTokenApproval( - USDC_ADDRESS, - PERMIT2_ADDRESS, - BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), // Max approval - ); - - // User signs the ERC20 approval intent - const erc20ApproveIntent = await userClient - .createIntent() - .setTarget(erc20ApprovalParams.outputContract) - .withValue(erc20ApprovalParams.value ?? 0n) - .withCallData(erc20ApprovalParams.callData) - .sign(approveNonce); - - print("βœ“ ERC20 approval intent signed by user", ""); - - // Paymaster executes the ERC20 approval - print("Executing USDC approval to Permit2...", ""); - erc20ApproveResult = await paymasterClient.execute(erc20ApproveIntent); - print("ERC20 Approval transaction sent", (erc20ApproveResult as any).txHash); - print("βœ… ERC20 Approval SUCCEEDED", ""); - print( - "Confirmed", - `Block: ${(erc20ApproveResult as any).blockNumber}, Gas: ${erc20ApproveResult.gasUsed}`, - ); - print("", `${explorerUrl}/tx/${(erc20ApproveResult as any).txHash}`); - - await new Promise((resolve) => setTimeout(resolve, 2000)); - } + // ────────────────────────────────────────────────────────────────────────── + // Step 3: Execute Swap + // ────────────────────────────────────────────────────────────────────────── - // ===== Step 2b: Approve Universal Router on Permit2 (if needed) ===== - print("--- Step 2b: Check/Approve Universal Router on Permit2 ---", ""); - - // Check existing Permit2 allowance for Universal Router - const [permit2Amount, permit2Expiry] = await publicClient.readContract({ - address: PERMIT2_ADDRESS, - abi: PERMIT2_ABI, - functionName: "allowance", - args: [env.EOA_ADDRESS as Hex, USDC_ADDRESS, UNIVERSAL_ROUTER_ADDRESS], - }); + print("Step 3: Execute Swap", ""); - const currentTime = Math.floor(Date.now() / 1000); - let permit2ApproveResult: { gasUsed: bigint } | null = null; + const swapNonce = await userClient.getNonce(); + const deadline = BigInt(Math.floor(Date.now() / 1000) + 60 * 20); // 20 min - if (permit2Amount >= swapAmount && permit2Expiry > currentTime) { - print( - "βœ“ Permit2 allowance for Universal Router already sufficient", - `Amount: ${permit2Amount} >= ${swapAmount}, Expires: ${new Date(Number(permit2Expiry) * 1000).toISOString()}`, - ); - } else { - const reason = permit2Amount < swapAmount - ? `Amount insufficient (${permit2Amount} < ${swapAmount})` - : `Expired (${permit2Expiry} <= ${currentTime})`; - print(`Permit2 allowance needs update: ${reason}`, "Approving..."); - - const permit2ApproveNonce = await userClient.getNonce(); - print(`Current nonce: ${permit2ApproveNonce}`, ""); - - // Permit2 expiration: 30 days from now - const permit2Expiration = Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30; - - // Build the Permit2 approve call with a larger amount for future swaps - const approveAmount = swapAmount * 1000n; // Approve 1000x the swap amount for future use - const permit2ApproveCallData = encodeAbiParameters( - [ - { type: "address" }, - { type: "address" }, - { type: "uint160" }, - { type: "uint48" }, - ], - [ - USDC_ADDRESS, - UNIVERSAL_ROUTER_ADDRESS, - approveAmount, - permit2Expiration, - ], - ); - - // Prepend the function selector for approve(address,address,uint160,uint48) - const permit2ApproveSelector = "0x87517c45" as Hex; // keccak256("approve(address,address,uint160,uint48)")[:4] - const fullPermit2ApproveCallData = concat([permit2ApproveSelector, permit2ApproveCallData]); - - // User signs the Permit2 approval intent - const permit2ApproveIntent = await userClient - .createIntent() - .setTarget(PERMIT2_ADDRESS) - .withValue(0n) - .withCallData(fullPermit2ApproveCallData) - .sign(permit2ApproveNonce); - - print("βœ“ Permit2 approval intent signed by user", ""); - - // Paymaster executes the Permit2 approval - print("Executing Permit2 approval for Universal Router...", ""); - permit2ApproveResult = await paymasterClient.execute(permit2ApproveIntent); - print("Permit2 Approval transaction sent", (permit2ApproveResult as any).txHash); - print("βœ… Permit2 Approval SUCCEEDED", ""); - print( - "Confirmed", - `Block: ${(permit2ApproveResult as any).blockNumber}, Gas: ${permit2ApproveResult.gasUsed}`, - ); - print("", `${explorerUrl}/tx/${(permit2ApproveResult as any).txHash}`); - - await new Promise((resolve) => setTimeout(resolve, 2000)); - } + // Build swap path: USDC β†’ (0.05% fee) β†’ WETH + const swapPath = encodeV3SwapPath(USDC_ADDRESS, FeeAmount.LOW, WETH_ADDRESS); - // ===== Step 2c: Execute the Swap ===== - print("--- Step 2c: Execute Swap ---", ""); + // Encode swap parameters (recipient receives WETH, user pays with USDC) + const swapParams = encodeV3SwapExactInParams( + env.EOA_ADDRESS as Hex, + swapAmount, + 0n, // Accept any amount out (production should use a quote service) + swapPath, + true, + ); - const swapNonce = await userClient.getNonce(); - print(`Current nonce: ${swapNonce}`, ""); + // Build Universal Router execute calldata + const commands = toHex(new Uint8Array([V3_SWAP_EXACT_IN])); + const swapCalldata = buildUniversalRouterExecuteCalldata(commands, [swapParams], deadline); - // User signs the swap intent + // Sign and execute swap intent const swapIntent = await userClient .createIntent() .setTarget(UNIVERSAL_ROUTER_ADDRESS) - .withValue(0n) // No ETH value needed for USDC β†’ WETH swap - .withCallData(fullSwapCallData) + .withValue(0n) + .withCallData(swapCalldata) .sign(swapNonce); - print("βœ“ Swap intent signed by user", ""); - - // Paymaster executes the swap - print("Executing swap via gas station...", ""); const swapResult = await paymasterClient.execute(swapIntent); - print("Swap transaction sent", swapResult.txHash); - print("βœ… Swap SUCCEEDED", ""); - print( - "Confirmed", - `Block: ${swapResult.blockNumber}, Gas: ${swapResult.gasUsed}`, - ); - print("===== USDC β†’ ETH Swap Complete =====", ""); - print( - `βœ… Successfully swapped ${USDC_AMOUNT} USDC for WETH`, - `TX: ${explorerUrl}/tx/${swapResult.txHash}`, - ); - const erc20Gas = erc20ApproveResult?.gasUsed ?? 0n; - const permit2Gas = permit2ApproveResult?.gasUsed ?? 0n; - const totalGas = erc20Gas + permit2Gas + swapResult.gasUsed; + print("βœ… Swapped", `${EXPLORER_URL}/tx/${swapResult.txHash}`); + print("Gas Used", swapResult.gasUsed.toString()); - print( - "Gas usage breakdown", - `ERC20 Approve: ${erc20Gas} + Permit2 Approve: ${permit2Gas} + Swap: ${swapResult.gasUsed} = ${totalGas} gas units`, - ); + print("", ""); - if (erc20Gas === 0n && permit2Gas === 0n) { - print("πŸ’‘ Tip", "Approvals were skipped because they already exist. Only the swap was executed!"); - } - print( - "Note", - "You received WETH. To get native ETH, an additional unwrap step would be needed.", - ); -}; + // ────────────────────────────────────────────────────────────────────────── + // Summary + // ────────────────────────────────────────────────────────────────────────── + + const totalGas = approvalResult.gasUsed + swapResult.gasUsed; -main(); + print("═══════════════════════════════════════════", ""); + print("βœ… Swap Complete!", ""); + print("Amount", `${SWAP_AMOUNT_USDC} USDC β†’ WETH`); + print("Total Gas", `${totalGas} (Approval: ${approvalResult.gasUsed} + Swap: ${swapResult.gasUsed})`); + print("Transactions", "2 (combined approvals into 1 tx using approveThenExecute)"); + print("Swap TX", `${EXPLORER_URL}/tx/${swapResult.txHash}`); + print("═══════════════════════════════════════════", ""); + print("", ""); + print("Note", "You received WETH. To get native ETH, an additional unwrap step is needed."); +} + +function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +// Run with error handling +main().catch((error) => { + console.error("\n❌ Error:", error.message || error); + process.exit(1); +}); From 46e398e42c5eba8e38a56a41d34bf2358b20e383 Mon Sep 17 00:00:00 2001 From: Bijan Massoumi Date: Wed, 26 Nov 2025 16:26:55 -0500 Subject: [PATCH 3/4] Add new changeset for USDC swap example with approveThenExecute and reimbursable gas station support --- .changeset/polite-horses-accept.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/polite-horses-accept.md diff --git a/.changeset/polite-horses-accept.md b/.changeset/polite-horses-accept.md new file mode 100644 index 000000000..7ec561dd9 --- /dev/null +++ b/.changeset/polite-horses-accept.md @@ -0,0 +1,5 @@ +--- +"@turnkey/gas-station": minor +--- + +Add USDC swap example with approveThenExecute and reimbursable gas station support From d755808b5de3333bd0d182bf02a4d46495930a16 Mon Sep 17 00:00:00 2001 From: Bijan Massoumi Date: Wed, 26 Nov 2025 16:28:12 -0500 Subject: [PATCH 4/4] Update gas station SDK to align with audited contract changes, including new contract addresses and EIP-712 field name updates. Refactor code for improved readability and maintainability across various files, and enhance documentation to reflect these changes. --- .changeset/chubby-peas-sip.md | 3 + examples/tk-gas-station/src/swapUSDCForETH.ts | 85 +++++++++++++++---- .../src/transferUSDCReimbursable.ts | 22 ++--- .../src/abi/reimbursable-gas-station.ts | 48 +++++++++-- packages/gas-station/src/intentBuilder.ts | 3 +- packages/gas-station/src/policyUtils.ts | 4 +- 6 files changed, 125 insertions(+), 40 deletions(-) diff --git a/.changeset/chubby-peas-sip.md b/.changeset/chubby-peas-sip.md index 445677032..96dd51184 100644 --- a/.changeset/chubby-peas-sip.md +++ b/.changeset/chubby-peas-sip.md @@ -5,10 +5,12 @@ Updated the `@turnkey/gas-station` SDK to align with the audited smart contract changes. The audit resulted in several interface updates: **Contract Changes:** + - **New contract addresses**: Updated both delegate and execution contract addresses to the newly deployed versions - **EIP-712 field name changes**: The canonical delegate contract interface uses simplified field names (`to`, `value`, `data`) instead of the previous descriptive names (`outputContract`, `ethAmount`, `arguments`) **SDK Updates:** + - Updated `DEFAULT_EXECUTION_CONTRACT` address from `0x4ece92b06C7d2d99d87f052E0Fca47Fb180c3348` to `0x00000000008c57a1CE37836a5e9d36759D070d8c` - Updated `DEFAULT_DELEGATE_CONTRACT` address from `0xC2a37Ee08cAc3778d9d05FF0a93FD5B553C77E3a` to `0x000066a00056CD44008768E2aF00696e19A30084` - Updated EIP-712 Execution typehash field names to match the contract's canonical interface @@ -17,6 +19,7 @@ Updated the `@turnkey/gas-station` SDK to align with the audited smart contract - Updated documentation and examples to reflect the new field names **Files Modified:** + - `packages/gas-station/src/config.ts` - Updated contract addresses - `packages/gas-station/src/intentBuilder.ts` - Updated EIP-712 type definitions and message objects - `packages/gas-station/src/policyUtils.ts` - Updated policy condition field references and documentation diff --git a/examples/tk-gas-station/src/swapUSDCForETH.ts b/examples/tk-gas-station/src/swapUSDCForETH.ts index bffb717d0..e96e94b7a 100644 --- a/examples/tk-gas-station/src/swapUSDCForETH.ts +++ b/examples/tk-gas-station/src/swapUSDCForETH.ts @@ -77,8 +77,18 @@ const envSchema = z.object({ API_PRIVATE_KEY: z.string().min(1, "API_PRIVATE_KEY is required"), API_PUBLIC_KEY: z.string().min(1, "API_PUBLIC_KEY is required"), ORGANIZATION_ID: z.string().min(1, "ORGANIZATION_ID is required"), - EOA_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, "EOA_ADDRESS must be a valid Ethereum address"), - PAYMASTER_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, "PAYMASTER_ADDRESS must be a valid Ethereum address"), + EOA_ADDRESS: z + .string() + .regex( + /^0x[a-fA-F0-9]{40}$/, + "EOA_ADDRESS must be a valid Ethereum address", + ), + PAYMASTER_ADDRESS: z + .string() + .regex( + /^0x[a-fA-F0-9]{40}$/, + "PAYMASTER_ADDRESS must be a valid Ethereum address", + ), BASE_RPC_URL: z.string().url("BASE_RPC_URL must be a valid URL"), }); @@ -94,10 +104,17 @@ const EXPLORER_URL = "https://basescan.org"; const USDC_ADDRESS: Hex = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; const WETH_ADDRESS: Hex = "0x4200000000000000000000000000000000000006"; const PERMIT2_ADDRESS: Hex = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; -const UNIVERSAL_ROUTER_ADDRESS: Hex = "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD"; +const UNIVERSAL_ROUTER_ADDRESS: Hex = + "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD"; // Token definitions for amount parsing -const USDC_TOKEN = new Token(BASE_CHAIN_ID, USDC_ADDRESS, 6, "USDC", "USD Coin"); +const USDC_TOKEN = new Token( + BASE_CHAIN_ID, + USDC_ADDRESS, + 6, + "USDC", + "USD Coin", +); // ============================================================================ // Uniswap Encoding Helpers @@ -114,7 +131,10 @@ const PERMIT2_APPROVE_SELECTOR: Hex = "0x87517c45"; * Encodes the swap path for Uniswap V3 (tokenIn -> fee -> tokenOut) */ function encodeV3SwapPath(tokenIn: Hex, fee: FeeAmount, tokenOut: Hex): Hex { - return encodePacked(["address", "uint24", "address"], [tokenIn, fee, tokenOut]); + return encodePacked( + ["address", "uint24", "address"], + [tokenIn, fee, tokenOut], + ); } /** @@ -164,7 +184,12 @@ function buildPermit2ApproveCalldata( expiration: number, ): Hex { const params = encodeAbiParameters( - [{ type: "address" }, { type: "address" }, { type: "uint160" }, { type: "uint48" }], + [ + { type: "address" }, + { type: "address" }, + { type: "uint160" }, + { type: "uint48" }, + ], [token, spender, amount, expiration], ); return concat([PERMIT2_APPROVE_SELECTOR, params]); @@ -221,7 +246,9 @@ async function main(): Promise { // Initialize Gas Station clients const userClient = new GasStationClient({ walletClient: userWalletClient }); - const paymasterClient = new GasStationClient({ walletClient: paymasterWalletClient }); + const paymasterClient = new GasStationClient({ + walletClient: paymasterWalletClient, + }); // ────────────────────────────────────────────────────────────────────────── // Step 1: EIP-7702 Authorization (if needed) @@ -235,7 +262,9 @@ async function main(): Promise { print("Status", "EOA not authorized, signing authorization..."); const authorization = await userClient.signAuthorization(); - const authResult = await paymasterClient.submitAuthorizations([authorization]); + const authResult = await paymasterClient.submitAuthorizations([ + authorization, + ]); print("βœ… Authorized", `${EXPLORER_URL}/tx/${authResult.txHash}`); await delay(2000); @@ -250,7 +279,10 @@ async function main(): Promise { // ────────────────────────────────────────────────────────────────────────── print("Step 2: Combined Approval", ""); - print("Action", "ERC20 approve USDCβ†’Permit2 + Permit2 approve for Universal Router"); + print( + "Action", + "ERC20 approve USDCβ†’Permit2 + Permit2 approve for Universal Router", + ); const swapAmount = parseUnits(SWAP_AMOUNT_USDC, USDC_TOKEN.decimals); const approveNonce = await userClient.getNonce(); @@ -268,7 +300,9 @@ async function main(): Promise { ); // Max ERC20 approval for USDCβ†’Permit2 (the "approve" part of approveThenExecute) - const maxErc20Approval = BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + const maxErc20Approval = BigInt( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + ); // Sign combined approval intent const approvalIntent = await userClient @@ -276,10 +310,16 @@ async function main(): Promise { .setTarget(PERMIT2_ADDRESS) .withValue(0n) .withCallData(permit2ApproveCalldata) - .signApprovalExecution(approveNonce, USDC_ADDRESS, PERMIT2_ADDRESS, maxErc20Approval); + .signApprovalExecution( + approveNonce, + USDC_ADDRESS, + PERMIT2_ADDRESS, + maxErc20Approval, + ); // Paymaster executes both approvals atomically - const approvalResult = await paymasterClient.approveThenExecute(approvalIntent); + const approvalResult = + await paymasterClient.approveThenExecute(approvalIntent); print("βœ… Approved", `${EXPLORER_URL}/tx/${approvalResult.txHash}`); print("Gas Used", approvalResult.gasUsed.toString()); @@ -310,7 +350,11 @@ async function main(): Promise { // Build Universal Router execute calldata const commands = toHex(new Uint8Array([V3_SWAP_EXACT_IN])); - const swapCalldata = buildUniversalRouterExecuteCalldata(commands, [swapParams], deadline); + const swapCalldata = buildUniversalRouterExecuteCalldata( + commands, + [swapParams], + deadline, + ); // Sign and execute swap intent const swapIntent = await userClient @@ -336,12 +380,21 @@ async function main(): Promise { print("═══════════════════════════════════════════", ""); print("βœ… Swap Complete!", ""); print("Amount", `${SWAP_AMOUNT_USDC} USDC β†’ WETH`); - print("Total Gas", `${totalGas} (Approval: ${approvalResult.gasUsed} + Swap: ${swapResult.gasUsed})`); - print("Transactions", "2 (combined approvals into 1 tx using approveThenExecute)"); + print( + "Total Gas", + `${totalGas} (Approval: ${approvalResult.gasUsed} + Swap: ${swapResult.gasUsed})`, + ); + print( + "Transactions", + "2 (combined approvals into 1 tx using approveThenExecute)", + ); print("Swap TX", `${EXPLORER_URL}/tx/${swapResult.txHash}`); print("═══════════════════════════════════════════", ""); print("", ""); - print("Note", "You received WETH. To get native ETH, an additional unwrap step is needed."); + print( + "Note", + "You received WETH. To get native ETH, an additional unwrap step is needed.", + ); } function delay(ms: number): Promise { diff --git a/examples/tk-gas-station/src/transferUSDCReimbursable.ts b/examples/tk-gas-station/src/transferUSDCReimbursable.ts index 1238dcaaf..02dfaa648 100644 --- a/examples/tk-gas-station/src/transferUSDCReimbursable.ts +++ b/examples/tk-gas-station/src/transferUSDCReimbursable.ts @@ -183,11 +183,7 @@ const main = async () => { // It's a general authorization for the reimbursable contract to interact with USDC const sessionSignature = await userClient .createIntent() - .signSessionForUSDCTransfer( - nonce, - usdcAddress, - reimbursableContract, - ); + .signSessionForUSDCTransfer(nonce, usdcAddress, reimbursableContract); print( "βœ“ Session signature created", @@ -229,13 +225,15 @@ const main = async () => { sessionSignature, }; - print("βœ“ Reimbursable intent created", "Includes execution + session signatures"); + print( + "βœ“ Reimbursable intent created", + "Includes execution + session signatures", + ); // Step 4: Paymaster executes with reimbursement (EOA pays for gas in USDC) print("Executing intent via reimbursable gas station...", ""); - const result = await paymasterClient.executeWithReimbursement( - reimbursableIntent, - ); + const result = + await paymasterClient.executeWithReimbursement(reimbursableIntent); print("Execution transaction sent", result.txHash); print("Waiting for confirmation...", ""); @@ -247,7 +245,10 @@ const main = async () => { "βœ… Successfully transferred 0.01 USDC from EOA to paymaster", `TX: ${explorerUrl}/tx/${result.txHash}`, ); - print("Gas payment", `EOA deposited ${INITIAL_DEPOSIT_USDC} USDC for gas (excess refunded)`); + print( + "Gas payment", + `EOA deposited ${INITIAL_DEPOSIT_USDC} USDC for gas (excess refunded)`, + ); print("Gas usage", `${result.gasUsed} gas units`); // Optional: Demonstrate cached session signature usage @@ -259,4 +260,3 @@ const main = async () => { }; main(); - diff --git a/packages/gas-station/src/abi/reimbursable-gas-station.ts b/packages/gas-station/src/abi/reimbursable-gas-station.ts index eaac5c58c..57929427a 100644 --- a/packages/gas-station/src/abi/reimbursable-gas-station.ts +++ b/packages/gas-station/src/abi/reimbursable-gas-station.ts @@ -345,7 +345,11 @@ export const reimbursableGasStationAbi = [ }, { inputs: [ - { internalType: "uint256", name: "_initialDepositERC20", type: "uint256" }, + { + internalType: "uint256", + name: "_initialDepositERC20", + type: "uint256", + }, { internalType: "uint256", name: "_transactionGasLimitWei", @@ -371,7 +375,11 @@ export const reimbursableGasStationAbi = [ }, { inputs: [ - { internalType: "uint256", name: "_initialDepositERC20", type: "uint256" }, + { + internalType: "uint256", + name: "_initialDepositERC20", + type: "uint256", + }, { internalType: "uint256", name: "_transactionGasLimitWei", @@ -397,7 +405,11 @@ export const reimbursableGasStationAbi = [ }, { inputs: [ - { internalType: "uint256", name: "_initialDepositERC20", type: "uint256" }, + { + internalType: "uint256", + name: "_initialDepositERC20", + type: "uint256", + }, { internalType: "uint256", name: "_transactionGasLimitWei", @@ -419,7 +431,11 @@ export const reimbursableGasStationAbi = [ }, { inputs: [ - { internalType: "uint256", name: "_initialDepositERC20", type: "uint256" }, + { + internalType: "uint256", + name: "_initialDepositERC20", + type: "uint256", + }, { internalType: "uint256", name: "_transactionGasLimitWei", @@ -458,7 +474,11 @@ export const reimbursableGasStationAbi = [ }, { inputs: [ - { internalType: "uint256", name: "_initialDepositERC20", type: "uint256" }, + { + internalType: "uint256", + name: "_initialDepositERC20", + type: "uint256", + }, { internalType: "uint256", name: "_transactionGasLimitWei", @@ -481,7 +501,11 @@ export const reimbursableGasStationAbi = [ }, { inputs: [ - { internalType: "uint256", name: "_initialDepositERC20", type: "uint256" }, + { + internalType: "uint256", + name: "_initialDepositERC20", + type: "uint256", + }, { internalType: "uint256", name: "_transactionGasLimitWei", @@ -512,7 +536,11 @@ export const reimbursableGasStationAbi = [ }, { inputs: [ - { internalType: "uint256", name: "_initialDepositERC20", type: "uint256" }, + { + internalType: "uint256", + name: "_initialDepositERC20", + type: "uint256", + }, { internalType: "uint256", name: "_transactionGasLimitWei", @@ -543,7 +571,11 @@ export const reimbursableGasStationAbi = [ }, { inputs: [ - { internalType: "uint256", name: "_initialDepositERC20", type: "uint256" }, + { + internalType: "uint256", + name: "_initialDepositERC20", + type: "uint256", + }, { internalType: "uint256", name: "_transactionGasLimitWei", diff --git a/packages/gas-station/src/intentBuilder.ts b/packages/gas-station/src/intentBuilder.ts index bebfaf512..d2273dc7f 100644 --- a/packages/gas-station/src/intentBuilder.ts +++ b/packages/gas-station/src/intentBuilder.ts @@ -275,8 +275,7 @@ export class IntentBuilder { ): Promise { const nonce = this.nonce ?? currentNonce; // Default deadline: 1 hour from now - const deadline = - sessionDeadline ?? Math.floor(Date.now() / 1000) + 60 * 60; + const deadline = sessionDeadline ?? Math.floor(Date.now() / 1000) + 60 * 60; // EIP-712 domain and types for session execution const domain = { diff --git a/packages/gas-station/src/policyUtils.ts b/packages/gas-station/src/policyUtils.ts index b9805358d..fab7efb7b 100644 --- a/packages/gas-station/src/policyUtils.ts +++ b/packages/gas-station/src/policyUtils.ts @@ -108,9 +108,7 @@ export function buildIntentSigningPolicy(config: { // Build OR conditions for each allowed contract // Convert to lowercase for case-insensitive comparison const contractConditions = config.restrictions.allowedContracts - .map( - (c) => `eth.eip_712.message['to'] == '${c.toLowerCase()}'`, - ) + .map((c) => `eth.eip_712.message['to'] == '${c.toLowerCase()}'`) .join(" || "); conditions.push(`(${contractConditions})`); }