Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions account-kit/infra/src/actions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 9 additions & 1 deletion account-kit/infra/src/client/smartAccountClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<AlchemyTransport, chain, account, context>,
| "customMiddleware"
Expand Down Expand Up @@ -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,
Expand Down
37 changes: 36 additions & 1 deletion account-kit/infra/src/gas-manager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Address, Chain } from "viem";
import type { Address, Chain, Hex } from "viem";
import {
arbitrum,
arbitrumSepolia,
Expand Down Expand Up @@ -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;
};
117 changes: 113 additions & 4 deletions account-kit/infra/src/middleware/gasManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
Address,
ClientMiddlewareConfig,
ClientMiddlewareFn,
EntryPointVersion,
Expand All @@ -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
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -86,16 +109,21 @@ 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<ClientMiddlewareConfig, "dummyPaymasterAndData" | "paymasterAndData">} partial client middleware configuration containing `dummyPaymasterAndData` and `paymasterAndData`
* @returns {Pick<ClientMiddlewareConfig, "dummyPaymasterAndData" | "feeEstimator" | "gasEstimator" | "paymasterAndData">} partial client middleware configuration containing `dummyPaymasterAndData`, `feeEstimator`, `gasEstimator`, and `paymasterAndData`
*/
export function alchemyGasAndPaymasterAndDataMiddleware(
params: AlchemyGasAndPaymasterAndDataMiddlewareParams
): Pick<
ClientMiddlewareConfig,
"dummyPaymasterAndData" | "feeEstimator" | "gasEstimator" | "paymasterAndData"
> {
const { policyId, transport, gasEstimatorOverride, feeEstimatorOverride } =
params;
const {
policyId,
policyToken,
transport,
gasEstimatorOverride,
feeEstimatorOverride,
} = params;
return {
dummyPaymasterAndData: async (uo, args) => {
if (
Expand Down Expand Up @@ -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: [
Expand All @@ -197,6 +301,11 @@ export function alchemyGasAndPaymasterAndDataMiddleware(
userOperation: userOp,
dummySignature: await account.getDummySignature(),
overrides,
...(erc20Context
? {
erc20Context,
}
: {}),
},
],
});
Expand Down
Loading