@account-kit/infra
Version:
adapters for @aa-sdk/core for interacting with alchemy services
319 lines • 14.6 kB
JavaScript
import { bypassPaymasterAndData, ChainNotFoundError, clientHeaderTrack, deepHexlify, defaultGasEstimator, erc7677Middleware, filterUndefined, isBigNumberish, isMultiplier, noopMiddleware, resolveProperties, } from "@aa-sdk/core";
import { fromHex, isHex, encodeAbiParameters, encodeFunctionData, parseAbi, } from "viem";
import { alchemyFeeEstimator } from "./feeEstimator.js";
import { PermitTypes, EIP7597Abis, ERC20Abis, getAlchemyPaymasterAddress, } from "../gas-manager.js";
import { InvalidSignedPermit } from "../errors/invalidSignedPermit.js";
/**
* Paymaster middleware factory that uses Alchemy's Gas Manager for sponsoring
* transactions. Adheres to the ERC-7677 standardized communication protocol.
*
* @example
* ```ts
* import { sepolia, alchemyGasManagerMiddleware } from "@account-kit/infra";
* import { http } from "viem";
*
* const client = createSmartAccountClient({
* transport: http("rpc-url"),
* chain: sepolia,
* ...alchemyGasManagerMiddleware("policyId")
* });
* ```
*
* @param {string | string[]} policyId - The policyId (or list of policyIds) for Alchemy's gas manager
* @param {PolicyToken | undefined} policyToken - The policy token configuration
* @param {string | undefined} webhookData - The webhook data to include in the request
* @returns {Pick<ClientMiddlewareConfig, "dummyPaymasterAndData" | "paymasterAndData">} Partial client middleware configuration containing `dummyPaymasterAndData` and `paymasterAndData`
*/
export function alchemyGasManagerMiddleware(policyId, policyToken, webhookData) {
const buildContext = async (args) => {
const context = { policyId };
const { account, client } = args;
if (!client.chain) {
throw new ChainNotFoundError();
}
if (policyToken !== undefined) {
context.erc20Context = {
tokenAddress: policyToken.address,
maxTokenAmount: policyToken.maxTokenAmount,
};
if (policyToken.permit !== undefined) {
const permit = await generateSignedPermit(client, account, policyToken);
if (permit !== undefined) {
context.erc20Context.permit = permit;
}
}
else if (args.context !== undefined) {
context.erc20Context.permit = extractSignedPermitFromContext(args.context);
}
}
if (webhookData !== undefined) {
context.webhookData = webhookData;
}
return context;
};
return {
dummyPaymasterAndData: async (uo, args) => {
const context = await buildContext(args);
const baseMiddleware = erc7677Middleware({ context });
return baseMiddleware.dummyPaymasterAndData(uo, args);
},
paymasterAndData: async (uo, args) => {
const context = await buildContext(args);
const baseMiddleware = erc7677Middleware({ context });
return baseMiddleware.paymasterAndData(uo, args);
},
};
}
/**
* Paymaster middleware factory that uses Alchemy's Gas Manager for sponsoring
* transactions. Uses Alchemy's custom `alchemy_requestGasAndPaymasterAndData`
* method instead of conforming to the standard ERC-7677 interface. Note that
* if you use `createAlchemySmartAccountClient`, this middleware is already
* used by default and you do not need to manually include it.
*
* @example
* ```ts twoslash
* import { sepolia, alchemy, alchemyGasAndPaymasterAndDataMiddleware } from "@account-kit/infra";
* import { createSmartAccountClient } from "@aa-sdk/core";
*
* const client = createSmartAccountClient({
* transport: alchemy({ apiKey: "your-api-key" }),
* chain: sepolia,
* ...alchemyGasAndPaymasterAndDataMiddleware({
* policyId: "policyId",
* transport: alchemy({ apiKey: "your-api-key" }),
* })
* });
* ```
*
* @param {AlchemyGasAndPaymasterAndDataMiddlewareParams} params configuration params
* @param {AlchemyGasAndPaymasterAndDataMiddlewareParams.policyId} params.policyId the policyId for Alchemy's gas manager
* @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" | "feeEstimator" | "gasEstimator" | "paymasterAndData">} partial client middleware configuration containing `dummyPaymasterAndData`, `feeEstimator`, `gasEstimator`, and `paymasterAndData`
*/
export function alchemyGasAndPaymasterAndDataMiddleware(params) {
const { policyId, policyToken, transport, webhookData, gasEstimatorOverride, feeEstimatorOverride, } = params;
return {
dummyPaymasterAndData: async (uo, args) => {
if (
// No reason to generate dummy data if we are bypassing the paymaster.
bypassPaymasterAndData(args.overrides) ||
// When using alchemy_requestGasAndPaymasterAndData, there is generally no reason to generate dummy
// data. However, if the gas/feeEstimator is overriden, then this option should be enabled.
!(gasEstimatorOverride || feeEstimatorOverride)) {
return noopMiddleware(uo, args);
}
// Fall back to the default 7677 dummyPaymasterAndData middleware.
return alchemyGasManagerMiddleware(policyId, policyToken, webhookData).dummyPaymasterAndData(uo, args);
},
feeEstimator: (uo, args) => {
return feeEstimatorOverride
? feeEstimatorOverride(uo, args)
: bypassPaymasterAndData(args.overrides)
? alchemyFeeEstimator(transport)(uo, args)
: noopMiddleware(uo, args);
},
gasEstimator: (uo, args) => {
return gasEstimatorOverride
? gasEstimatorOverride(uo, args)
: bypassPaymasterAndData(args.overrides)
? defaultGasEstimator(args.client)(uo, args)
: noopMiddleware(uo, args);
},
paymasterAndData: async (uo, { account, client: client_, feeOptions, overrides: overrides_, context: uoContext, }) => {
const client = clientHeaderTrack(client_, "alchemyFeeEstimator");
if (!client.chain) {
throw new ChainNotFoundError();
}
const userOp = deepHexlify(await resolveProperties(uo));
const overrides = filterUndefined({
maxFeePerGas: overrideField("maxFeePerGas", overrides_, feeOptions, userOp),
maxPriorityFeePerGas: overrideField("maxPriorityFeePerGas", overrides_, feeOptions, userOp),
callGasLimit: overrideField("callGasLimit", overrides_, feeOptions, userOp),
verificationGasLimit: overrideField("verificationGasLimit", overrides_, feeOptions, userOp),
preVerificationGas: overrideField("preVerificationGas", overrides_, feeOptions, userOp),
...(account.getEntryPoint().version === "0.7.0"
? {
paymasterVerificationGasLimit: overrideField("paymasterVerificationGasLimit", overrides_, feeOptions, userOp),
paymasterPostOpGasLimit: overrideField("paymasterPostOpGasLimit", overrides_, feeOptions, userOp),
}
: {}),
});
let erc20Context = undefined;
if (policyToken !== undefined) {
erc20Context = {
tokenAddress: policyToken.address,
maxTokenAmount: policyToken.maxTokenAmount,
};
if (policyToken.permit !== undefined) {
const permit = await generateSignedPermit(client, account, policyToken);
if (permit !== undefined) {
erc20Context.permit = permit;
}
}
else if (uoContext !== undefined) {
erc20Context.permit = extractSignedPermitFromContext(uoContext);
}
}
const result = await client.request({
method: "alchemy_requestGasAndPaymasterAndData",
params: [
{
policyId,
entryPoint: account.getEntryPoint().address,
userOperation: userOp,
dummySignature: await account.getDummySignature(),
overrides,
webhookData,
...(erc20Context
? {
erc20Context,
}
: {}),
},
],
});
return {
...uo,
...result,
};
},
};
}
/**
* Utility function to override a field in the user operation request with the overrides or fee options
*
* @template {EntryPointVersion} TEntryPointVersion
* @param {keyof UserOperationFeeOptions<TEntryPointVersion>} field the field to override
* @param {UserOperationOverrides<TEntryPointVersion> | undefined} overrides the overrides object
* @param {UserOperationFeeOptions<TEntryPointVersion> | undefined} feeOptions the fee options object from the client
* @param {UserOperationRequest<TEntryPointVersion>} userOperation the user operation request
* @returns {Hex | Multiplier | undefined} the overridden field value
*/
const overrideField = (field, overrides, feeOptions, userOperation) => {
let _field = field;
if (overrides?.[_field] != null) {
// one-off absolute override
if (isBigNumberish(overrides[_field])) {
return deepHexlify(overrides[_field]);
}
// one-off multiplier overrides
else {
return {
multiplier: Number(overrides[_field].multiplier),
};
}
}
// provider level fee options with multiplier
if (isMultiplier(feeOptions?.[field])) {
return {
multiplier: Number(feeOptions[field].multiplier),
};
}
const userOpField = userOperation[field];
if (isHex(userOpField) && fromHex(userOpField, "bigint") > 0n) {
return userOpField;
}
return undefined;
};
/**
* Utility function to generate a signed Permit for erc20 transaction
*
* @param {MiddlewareClient} client - The Alchemy smart account client
* @param {TAccount} account - The smart account instance
* @param {PolicyToken} policyToken - The policy token configuration
* @returns {Promise<Hex>} Returns a Promise containing the signed EIP2612 permit
*/
const generateSignedPermit = async (client, account, policyToken) => {
if (!client.chain) {
throw new ChainNotFoundError();
}
if (!policyToken.permit) {
throw new Error("permit is missing");
}
if (!policyToken.permit?.erc20Name || !policyToken.permit?.version) {
throw new Error("erc20Name or version is missing");
}
const paymasterAddress = policyToken.permit.paymasterAddress ??
getAlchemyPaymasterAddress(client.chain, account.getEntryPoint().version);
if (paymasterAddress === undefined || paymasterAddress === "0x") {
throw new Error("no paymaster contract address available");
}
let allowanceFuture = client.call({
to: policyToken.address,
data: encodeFunctionData({
abi: parseAbi(ERC20Abis),
functionName: "allowance",
args: [account.address, paymasterAddress],
}),
});
let nonceFuture = client.call({
to: policyToken.address,
data: encodeFunctionData({
abi: parseAbi(EIP7597Abis),
functionName: "nonces",
args: [account.address],
}),
});
const [allowanceResponse, nonceResponse] = await Promise.all([
allowanceFuture,
nonceFuture,
]);
if (!allowanceResponse.data) {
throw new Error("No allowance returned from erc20 contract call");
}
if (!nonceResponse.data) {
throw new Error("No nonces returned from erc20 contract call");
}
const permitLimit = policyToken.permit.autoPermitApproveTo;
const currentAllowance = BigInt(allowanceResponse.data);
if (currentAllowance > policyToken.permit.autoPermitBelow) {
// no need to generate permit
return undefined;
}
const nonce = BigInt(nonceResponse.data);
const deadline = BigInt(Math.floor(Date.now() / 1000) + 60 * 10);
const typedPermitData = {
types: PermitTypes,
primaryType: "Permit",
domain: {
name: policyToken.permit.erc20Name ?? "",
version: policyToken.permit.version ?? "",
chainId: BigInt(client.chain.id),
verifyingContract: policyToken.address,
},
message: {
owner: account.address,
spender: paymasterAddress,
value: permitLimit,
nonce: nonce,
deadline,
},
};
const signedPermit = await account.signTypedData(typedPermitData);
return encodeSignedPermit(permitLimit, deadline, signedPermit);
};
function extractSignedPermitFromContext(context) {
if (context.signedPermit === undefined) {
return undefined;
}
if (typeof context.signedPermit !== "object") {
throw new InvalidSignedPermit("signedPermit is not an object");
}
if (typeof context.signedPermit.value !== "bigint") {
throw new InvalidSignedPermit("signedPermit.value is not a bigint");
}
if (typeof context.signedPermit.deadline !== "bigint") {
throw new InvalidSignedPermit("signedPermit.deadline is not a bigint");
}
if (!isHex(context.signedPermit.signature)) {
throw new InvalidSignedPermit("signedPermit.signature is not a hex string");
}
return encodeSignedPermit(context.signedPermit.value, context.signedPermit.deadline, context.signedPermit.signature);
}
function encodeSignedPermit(value, deadline, signedPermit) {
return encodeAbiParameters([{ type: "uint256" }, { type: "uint256" }, { type: "bytes" }], [value, deadline, signedPermit]);
}
//# sourceMappingURL=gasManager.js.map