diff --git a/account-kit/infra/src/actions/types.ts b/account-kit/infra/src/actions/types.ts index 4a52dbb229..a1c50e6b7c 100644 --- a/account-kit/infra/src/actions/types.ts +++ b/account-kit/infra/src/actions/types.ts @@ -57,6 +57,11 @@ export type RequestGasAndPaymasterAndDataRequest = [ { policyId: string | string[]; entryPoint: Address; + erc20Context?: { + tokenAddress: Address; + permit?: Hex; + maxTokenAmount?: BigInt; + }; dummySignature: Hex; userOperation: UserOperationRequest; overrides?: UserOperationOverrides; diff --git a/account-kit/infra/src/client/smartAccountClient.ts b/account-kit/infra/src/client/smartAccountClient.ts index 6c6791550a..b5f634fc8a 100644 --- a/account-kit/infra/src/client/smartAccountClient.ts +++ b/account-kit/infra/src/client/smartAccountClient.ts @@ -12,7 +12,7 @@ import { type SmartContractAccountWithSigner, type UserOperationContext, } from "@aa-sdk/core"; -import { type Chain } from "viem"; +import { type Address, type Chain } from "viem"; import { alchemy, convertHeadersToObject, @@ -48,6 +48,13 @@ export type AlchemySmartAccountClientConfig< account?: account; useSimulation?: boolean; policyId?: string | string[]; + policyToken?: { + address: Address; + approvalMode?: "NONE" | "PERMIT"; + maxTokenAmount?: bigint; + erc20Name?: string; + version?: string; + }; } & Pick< SmartAccountClientConfig, | "customMiddleware" @@ -163,6 +170,7 @@ export function createAlchemySmartAccountClient( ...(config.policyId ? alchemyGasAndPaymasterAndDataMiddleware({ policyId: config.policyId, + policyToken: config.policyToken, transport: config.transport, gasEstimatorOverride: config.gasEstimator, feeEstimatorOverride: config.feeEstimator, diff --git a/account-kit/infra/src/gas-manager.ts b/account-kit/infra/src/gas-manager.ts index 9041f15612..e4173c900b 100644 --- a/account-kit/infra/src/gas-manager.ts +++ b/account-kit/infra/src/gas-manager.ts @@ -1,4 +1,4 @@ -import type { Address, Chain } from "viem"; +import type { Address, Chain, Hex } from "viem"; import { arbitrum, arbitrumSepolia, @@ -66,3 +66,38 @@ export const getAlchemyPaymasterAddress = (chain: Chain): Address => { throw new Error(`Unsupported chain: ${chain}`); } }; + +export const PermitTypes = { + EIP712Domain: [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" }, + ], + Permit: [ + { name: "owner", type: "address" }, + { name: "spender", type: "address" }, + { name: "value", type: "uint256" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + ], +} as const; + +export const EIP712NoncesAbi = [ + "function nonces(address owner) external view returns (uint)", +] as const; + +export type PermitMessage = { + owner: Hex; + spender: Hex; + value: bigint; + nonce: bigint; + deadline: bigint; +}; + +export type PermitDomain = { + name: string; + version: string; + chainId: bigint; + verifyingContract: Hex; +}; diff --git a/account-kit/infra/src/middleware/gasManager.ts b/account-kit/infra/src/middleware/gasManager.ts index 362088c336..b992178a0d 100644 --- a/account-kit/infra/src/middleware/gasManager.ts +++ b/account-kit/infra/src/middleware/gasManager.ts @@ -1,4 +1,5 @@ import type { + Address, ClientMiddlewareConfig, ClientMiddlewareFn, EntryPointVersion, @@ -20,10 +21,23 @@ import { noopMiddleware, resolveProperties, } from "@aa-sdk/core"; -import { fromHex, isHex, type Hex } from "viem"; +import { + fromHex, + isHex, + toHex, + type Hex, + encodeAbiParameters, + encodeFunctionData, + parseAbi, + maxUint256, + sliceHex, +} from "viem"; import type { AlchemySmartAccountClient } from "../client/smartAccountClient.js"; import type { AlchemyTransport } from "../alchemyTransport.js"; import { alchemyFeeEstimator } from "./feeEstimator.js"; +import type { RequestGasAndPaymasterAndDataRequest } from "../actions/types.js"; +import { PermitTypes, EIP712NoncesAbi } from "../gas-manager.js"; +import type { PermitMessage, PermitDomain } from "../gas-manager.js"; /** * Paymaster middleware factory that uses Alchemy's Gas Manager for sponsoring @@ -54,11 +68,20 @@ export function alchemyGasManagerMiddleware( interface AlchemyGasAndPaymasterAndDataMiddlewareParams { policyId: string | string[]; + policyToken?: PolicyToken; transport: AlchemyTransport; gasEstimatorOverride?: ClientMiddlewareFn; feeEstimatorOverride?: ClientMiddlewareFn; } +export type PolicyToken = { + address: Address; + maxTokenAmount?: bigint; + approvalMode?: "NONE" | "PERMIT"; + erc20Name?: string; + version?: string; +}; + /** * Paymaster middleware factory that uses Alchemy's Gas Manager for sponsoring * transactions. Uses Alchemy's custom `alchemy_requestGasAndPaymasterAndData` @@ -86,7 +109,7 @@ interface AlchemyGasAndPaymasterAndDataMiddlewareParams { * @param {AlchemyGasAndPaymasterAndDataMiddlewareParams.transport} params.transport fallback transport to use for fee estimation when not using the paymaster * @param {AlchemyGasAndPaymasterAndDataMiddlewareParams.gasEstimatorOverride} params.gasEstimatorOverride custom gas estimator middleware * @param {AlchemyGasAndPaymasterAndDataMiddlewareParams.feeEstimatorOverride} params.feeEstimatorOverride custom fee estimator middleware - * @returns {Pick} partial client middleware configuration containing `dummyPaymasterAndData` and `paymasterAndData` + * @returns {Pick} partial client middleware configuration containing `dummyPaymasterAndData`, `feeEstimator`, `gasEstimator`, and `paymasterAndData` */ export function alchemyGasAndPaymasterAndDataMiddleware( params: AlchemyGasAndPaymasterAndDataMiddlewareParams @@ -94,8 +117,13 @@ export function alchemyGasAndPaymasterAndDataMiddleware( ClientMiddlewareConfig, "dummyPaymasterAndData" | "feeEstimator" | "gasEstimator" | "paymasterAndData" > { - const { policyId, transport, gasEstimatorOverride, feeEstimatorOverride } = - params; + const { + policyId, + policyToken, + transport, + gasEstimatorOverride, + feeEstimatorOverride, + } = params; return { dummyPaymasterAndData: async (uo, args) => { if ( @@ -188,6 +216,82 @@ export function alchemyGasAndPaymasterAndDataMiddleware( : {}), }); + let erc20Context: RequestGasAndPaymasterAndDataRequest[0]["erc20Context"] = + undefined; + if (policyToken !== undefined) { + const maxAmountToken = policyToken.maxTokenAmount || maxUint256; + + erc20Context = { + tokenAddress: policyToken.address, + ...(policyToken.maxTokenAmount + ? { maxTokenAmount: policyToken.maxTokenAmount } + : {}), + }; + if (policyToken.approvalMode === "PERMIT") { + // get a paymaster address + let paymasterAddress: Address | undefined = undefined; + const paymasterData = await ( + client as AlchemySmartAccountClient + ).request({ + method: "pm_getPaymasterStubData", + params: [ + userOp, + account.getEntryPoint().address, + toHex(client.chain.id), + { + policyId: Array.isArray(policyId) ? policyId[0] : policyId, + }, + ], + }); + + paymasterAddress = paymasterData.paymaster + ? paymasterData.paymaster + : paymasterData.paymasterAndData + ? sliceHex(paymasterData.paymasterAndData, 0, 20) + : undefined; + + if (paymasterAddress === undefined || paymasterAddress === "0x") { + throw new Error("no paymaster contract address available"); + } + const deadline = maxUint256; + const { data } = await client.call({ + to: policyToken.address, + data: encodeFunctionData({ + abi: parseAbi(EIP712NoncesAbi), + functionName: "nonces", + args: [account.address], + }), + }); + if (!data) { + throw new Error("No nonces returned from erc20 contract call"); + } + + const typedPermitData = { + types: PermitTypes, + primaryType: "Permit" as const, + domain: { + name: policyToken.erc20Name ?? "", + version: policyToken.version ?? "", + chainId: BigInt(client.chain.id), + verifyingContract: policyToken.address, + } satisfies PermitDomain, + message: { + owner: account.address, + spender: paymasterAddress, + value: maxAmountToken, + nonce: BigInt(data), + deadline, + } satisfies PermitMessage, + } as const; + + const signedPermit = await account.signTypedData(typedPermitData); + erc20Context.permit = encodeAbiParameters( + [{ type: "uint256" }, { type: "uint256" }, { type: "bytes" }], + [maxAmountToken, deadline, signedPermit] + ); + } + } + const result = await (client as AlchemySmartAccountClient).request({ method: "alchemy_requestGasAndPaymasterAndData", params: [ @@ -197,6 +301,11 @@ export function alchemyGasAndPaymasterAndDataMiddleware( userOperation: userOp, dummySignature: await account.getDummySignature(), overrides, + ...(erc20Context + ? { + erc20Context, + } + : {}), }, ], });