diff --git a/packages/permissionless-test/mock-aa-infra/mock-paymaster/helpers/schema.ts b/packages/permissionless-test/mock-aa-infra/mock-paymaster/helpers/schema.ts index 5c294bc6..6d728920 100644 --- a/packages/permissionless-test/mock-aa-infra/mock-paymaster/helpers/schema.ts +++ b/packages/permissionless-test/mock-aa-infra/mock-paymaster/helpers/schema.ts @@ -259,6 +259,14 @@ export const pmGetPaymasterStubDataParamsSchema = z return [val[0], val[1], val[2], val[3] ?? null] as const }) +export const pimlicoGetTokenQuotesSchema = z.tuple([ + z.object({ + tokens: z.array(addressSchema) + }), + addressSchema, // entryPoint + hexNumberSchema +]) + export type UserOperationV7 = zodInfer export type UserOperationV6 = zodInfer export type JsonRpcSchema = zodInfer diff --git a/packages/permissionless-test/mock-aa-infra/mock-paymaster/relay.ts b/packages/permissionless-test/mock-aa-infra/mock-paymaster/relay.ts index fc20a018..7fa46e95 100644 --- a/packages/permissionless-test/mock-aa-infra/mock-paymaster/relay.ts +++ b/packages/permissionless-test/mock-aa-infra/mock-paymaster/relay.ts @@ -2,6 +2,7 @@ import util from "node:util" import type { FastifyReply, FastifyRequest } from "fastify" import { type Account, + type Address, BaseError, type Chain, type Client, @@ -13,12 +14,12 @@ import { type WalletClient, concat, encodeAbiParameters, + getAddress, toHex } from "viem" import { type BundlerClient, type UserOperation, - entryPoint06Abi, entryPoint06Address, entryPoint07Address } from "viem/account-abstraction" @@ -35,6 +36,7 @@ import { RpcError, ValidationErrors, jsonRpcSchema, + pimlicoGetTokenQuotesSchema, pmGetPaymasterData, pmGetPaymasterStubDataParamsSchema, pmSponsorUserOperationParamsSchema @@ -260,7 +262,7 @@ const handleMethod = async ( verifyingPaymasterV06: GetContractReturnType< typeof VERIFYING_PAYMASTER_V06_ABI >, - publicClient: PublicClient, + publicClient: PublicClient, walletClient: WalletClient, parsedBody: JsonRpcSchema ) => { @@ -405,6 +407,44 @@ const handleMethod = async ( ] } + if (parsedBody.method === "pimlico_getTokenQuotes") { + const params = pimlicoGetTokenQuotesSchema.safeParse(parsedBody.params) + + if (!params.success) { + throw new RpcError( + fromZodError(params.error).message, + ValidationErrors.InvalidFields + ) + } + + const [context, entryPoint] = params.data + const { tokens } = context + + const quotes = { + [getAddress("0xffffffffffffffffffffffffffffffffffffffff")]: { + exchangeRate: "0x5cc717fbb3450c0000", + postOpGas: "0xc350" + } + } + + let paymaster: Address + if (entryPoint === entryPoint07Address) { + paymaster = verifyingPaymasterV07.address + } else { + paymaster = verifyingPaymasterV06.address + } + + return { + quotes: tokens + .filter((t) => quotes[t]) // Filter out unrecongized tokens + .map((token) => ({ + ...quotes[token], + paymaster, + token + })) + } + } + throw new RpcError( `Attempted to call an unknown method ${parsedBody.method}`, ValidationErrors.InvalidFields @@ -419,7 +459,7 @@ export const createRpcHandler = ( verifyingPaymasterV06: GetContractReturnType< typeof VERIFYING_PAYMASTER_V06_ABI >, - publicClient: PublicClient, + publicClient: PublicClient, walletClient: WalletClient ) => { return async (request: FastifyRequest, _reply: FastifyReply) => { diff --git a/packages/permissionless-test/src/utils.ts b/packages/permissionless-test/src/utils.ts index 0a859c63..bf2fa01d 100644 --- a/packages/permissionless-test/src/utils.ts +++ b/packages/permissionless-test/src/utils.ts @@ -184,7 +184,7 @@ export const getPimlicoClient = ({ entryPointVersion: entryPointVersion altoRpc: string }) => - createPimlicoClient({ + createPimlicoClient({ chain: foundry, entryPoint: { address: (entryPointVersion === "0.6" diff --git a/packages/permissionless/actions/pimlico.ts b/packages/permissionless/actions/pimlico.ts index c067c254..908aba0d 100644 --- a/packages/permissionless/actions/pimlico.ts +++ b/packages/permissionless/actions/pimlico.ts @@ -1,3 +1,8 @@ +import { + type GetTokenQuotesParameters, + type GetTokenQuotesReturnType, + getTokenQuotes +} from "./pimlico/getTokenQuotes" import { type GetUserOperationGasPriceReturnType, getUserOperationGasPrice @@ -35,7 +40,9 @@ export type { SendCompressedUserOperationParameters, SponsorUserOperationReturnType, ValidateSponsorshipPolicies, - ValidateSponsorshipPoliciesParameters + ValidateSponsorshipPoliciesParameters, + GetTokenQuotesParameters, + GetTokenQuotesReturnType } export { @@ -44,5 +51,6 @@ export { pimlicoActions, sendCompressedUserOperation, sponsorUserOperation, - validateSponsorshipPolicies + validateSponsorshipPolicies, + getTokenQuotes } diff --git a/packages/permissionless/actions/pimlico/getTokenQuotes.test.ts b/packages/permissionless/actions/pimlico/getTokenQuotes.test.ts new file mode 100644 index 00000000..db6f26ca --- /dev/null +++ b/packages/permissionless/actions/pimlico/getTokenQuotes.test.ts @@ -0,0 +1,36 @@ +import { getAddress, isAddress } from "viem" +import { entryPoint07Address } from "viem/account-abstraction" +import { foundry } from "viem/chains" +import { describe, expect } from "vitest" +import { testWithRpc } from "../../../permissionless-test/src/testWithRpc" +import { getPimlicoClient } from "../../../permissionless-test/src/utils" +import { getTokenQuotes } from "./getTokenQuotes" + +describe("getTokenQuotes", () => { + testWithRpc("getTokenQuotes", async ({ rpc }) => { + const pimlicoBundlerClient = getPimlicoClient({ + entryPointVersion: "0.7", + altoRpc: rpc.paymasterRpc + }) + + const token = getAddress("0xffffffffffffffffffffffffffffffffffffffff") + + const quotes = await getTokenQuotes(pimlicoBundlerClient, { + tokens: [token], + entryPointAddress: entryPoint07Address, + chain: foundry + }) + + expect(quotes).toBeTruthy() + expect(Array.isArray(quotes)).toBe(true) + expect(quotes[0].token).toBeTruthy() + expect(isAddress(quotes[0].token)) + expect(quotes[0].token).toEqual(token) + expect(quotes[0].paymaster).toBeTruthy() + expect(isAddress(quotes[0].paymaster)) + expect(quotes[0].exchangeRate).toBeTruthy() + expect(quotes[0].exchangeRate).toBeGreaterThan(0n) + expect(quotes[0].postOpGas).toBeTruthy() + expect(quotes[0].postOpGas).toBeGreaterThan(0n) + }) +}) diff --git a/packages/permissionless/actions/pimlico/getTokenQuotes.ts b/packages/permissionless/actions/pimlico/getTokenQuotes.ts new file mode 100644 index 00000000..59d47b78 --- /dev/null +++ b/packages/permissionless/actions/pimlico/getTokenQuotes.ts @@ -0,0 +1,67 @@ +import { + type Account, + type Address, + type Chain, + ChainNotFoundError, + type Client, + type GetChainParameter, + type Transport, + hexToBigInt, + numberToHex +} from "viem" +import type { PimlicoRpcSchema } from "../../types/pimlico" + +export type GetTokenQuotesParameters< + TChain extends Chain | undefined, + TChainOverride extends Chain | undefined = Chain | undefined +> = { + tokens: Address[] + entryPointAddress: Address +} & GetChainParameter + +export type GetTokenQuotesReturnType = { + paymaster: Address + token: Address + postOpGas: bigint + exchangeRate: bigint +}[] + +/** + * Returns all related fields to calculate the potential cost of a userOperation in ERC-20 tokens. + * + * - Docs: https://docs.pimlico.io/permissionless/reference/pimlico-bundler-actions/getTokenQuotes + * + * @param client that you created using viem's createClient whose transport url is pointing to the Pimlico's bundler. + * @returns slow, standard & fast values for maxFeePerGas & maxPriorityFeePerGas + * @returns quotes, see {@link GetTokenQuotesReturnType} + * + */ +export const getTokenQuotes = async < + TChain extends Chain | undefined, + TTransport extends Transport = Transport, + TChainOverride extends Chain | undefined = Chain | undefined +>( + client: Client, + args: GetTokenQuotesParameters +): Promise => { + const chainId = args.chain?.id ?? client.chain?.id + + if (!chainId) { + throw new ChainNotFoundError() + } + + const res = await client.request({ + method: "pimlico_getTokenQuotes", + params: [ + { tokens: args.tokens }, + args.entryPointAddress, + numberToHex(chainId) + ] + }) + + return res.quotes.map((quote) => ({ + ...quote, + postOpGas: hexToBigInt(quote.postOpGas), + exchangeRate: hexToBigInt(quote.exchangeRate) + })) +} diff --git a/packages/permissionless/clients/decorators/pimlico.ts b/packages/permissionless/clients/decorators/pimlico.ts index f172b01c..499a0827 100644 --- a/packages/permissionless/clients/decorators/pimlico.ts +++ b/packages/permissionless/clients/decorators/pimlico.ts @@ -1,8 +1,11 @@ -import type { Address, Client, Hash, Prettify } from "viem" +import type { Address, Chain, Client, Hash, Prettify, Transport } from "viem" import { + type GetTokenQuotesParameters, + type GetTokenQuotesReturnType, type SendCompressedUserOperationParameters, type ValidateSponsorshipPolicies, type ValidateSponsorshipPoliciesParameters, + getTokenQuotes, sendCompressedUserOperation, validateSponsorshipPolicies } from "../../actions/pimlico" @@ -22,6 +25,7 @@ import { } from "../../actions/pimlico/sponsorUserOperation" export type PimlicoActions< + TChain extends Chain | undefined, entryPointVersion extends "0.6" | "0.7" = "0.6" | "0.7" > = { /** @@ -111,6 +115,16 @@ export type PimlicoActions< Omit > ) => Promise[]> + getTokenQuotes: < + TChainOverride extends Chain | undefined = Chain | undefined + >( + args: Prettify< + Omit< + GetTokenQuotesParameters, + "entryPointAddress" + > + > + ) => Promise> } export const pimlicoActions = @@ -119,7 +133,12 @@ export const pimlicoActions = }: { entryPoint: { address: Address; version: entryPointVersion } }) => - (client: Client): PimlicoActions => ({ + < + TTransport extends Transport, + TChain extends Chain | undefined = Chain | undefined + >( + client: Client + ): PimlicoActions => ({ getUserOperationGasPrice: async () => getUserOperationGasPrice(client), getUserOperationStatus: async ( args: GetUserOperationStatusParameters @@ -138,5 +157,11 @@ export const pimlicoActions = validateSponsorshipPolicies(client, { ...args, entryPointAddress: entryPoint.address + }), + getTokenQuotes: async (args) => + getTokenQuotes(client, { + ...args, + chain: args.chain, + entryPointAddress: entryPoint.address }) }) diff --git a/packages/permissionless/clients/pimlico.ts b/packages/permissionless/clients/pimlico.ts index 33c4cda2..4cd141b9 100644 --- a/packages/permissionless/clients/pimlico.ts +++ b/packages/permissionless/clients/pimlico.ts @@ -42,7 +42,7 @@ export type PimlicoClient< : [...BundlerRpcSchema, ...PimlicoRpcSchema], BundlerActions & PaymasterActions & - PimlicoActions + PimlicoActions > > diff --git a/packages/permissionless/types/pimlico.ts b/packages/permissionless/types/pimlico.ts index da16601b..c4ad3094 100644 --- a/packages/permissionless/types/pimlico.ts +++ b/packages/permissionless/types/pimlico.ts @@ -28,6 +28,15 @@ export type PimlicoUserOperationStatus = { transactionHash: Hash | null } +type GetTokenQuotesWithBigIntAsHex = { + quotes: { + paymaster: Address + token: Address + postOpGas: Hex + exchangeRate: Hex + }[] +} + export type PimlicoRpcSchema< entryPointVersion extends "0.6" | "0.7" = "0.6" | "0.7" > = [ @@ -116,5 +125,10 @@ export type PimlicoRpcSchema< description: string | null } }[] + }, + { + Method: "pimlico_getTokenQuotes" + Parameters: [{ tokens: Address[] }, entryPoint: Address, chainId: Hex] + ReturnType: GetTokenQuotesWithBigIntAsHex } ]