UNPKG

@account-kit/infra

Version:

adapters for @aa-sdk/core for interacting with alchemy services

319 lines 14.6 kB
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