From 697f303ccf6663dbbbe2a5dda7f3c3025a623e98 Mon Sep 17 00:00:00 2001 From: Johnson Chen Date: Fri, 5 Sep 2025 15:26:14 +0800 Subject: [PATCH 1/5] feat: erc-7677 paymaster service --- src/core/UserOpBuilder.ts | 19 ++- src/erc7677-types.ts | 33 +++++ src/index.ts | 1 + test/scripts/candide.ts | 3 +- .../execute-kernel-candide-paymaster.ts | 133 ++++++++++++++++++ 5 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 src/erc7677-types.ts create mode 100644 test/scripts/execute-kernel-candide-paymaster.ts diff --git a/src/core/UserOpBuilder.ts b/src/core/UserOpBuilder.ts index c653e00..3a67d10 100644 --- a/src/core/UserOpBuilder.ts +++ b/src/core/UserOpBuilder.ts @@ -106,14 +106,22 @@ export class UserOpBuilder { paymaster, paymasterData = '0x', paymasterPostOpGasLimit = 0, + paymasterVerificationGasLimit = 0, }: { paymaster: string paymasterData?: string paymasterPostOpGasLimit?: BigNumberish + paymasterVerificationGasLimit?: BigNumberish }): UserOpBuilder { this.userOp.paymaster = paymaster this.userOp.paymasterData = paymasterData this.userOp.paymasterPostOpGasLimit = paymasterPostOpGasLimit + this.userOp.paymasterVerificationGasLimit = paymasterVerificationGasLimit + return this + } + + setPaymasterData(paymasterData: string): UserOpBuilder { + this.userOp.paymasterData = paymasterData return this } @@ -233,6 +241,11 @@ export class UserOpBuilder { return [domain, types, this.pack()] } + /** + * Estimates gas values for the user operation using the bundler. + * Always sets: verificationGasLimit, preVerificationGas, callGasLimit + * Conditionally sets: paymasterVerificationGasLimit (only if not already set), maxFeePerGas, maxPriorityFeePerGas + */ async estimateGas(): Promise { this.checkSender() this.checkEntryPointAddress() @@ -242,7 +255,11 @@ export class UserOpBuilder { this.userOp.verificationGasLimit = estimations.verificationGasLimit this.userOp.preVerificationGas = estimations.preVerificationGas this.userOp.callGasLimit = estimations.callGasLimit - this.userOp.paymasterVerificationGasLimit = estimations.paymasterVerificationGasLimit + + // Only set paymasterVerificationGasLimit if not already set + if (!this.userOp.paymasterVerificationGasLimit) { + this.userOp.paymasterVerificationGasLimit = estimations.paymasterVerificationGasLimit + } // Only etherspot returns these if (estimations.maxFeePerGas) { diff --git a/src/erc7677-types.ts b/src/erc7677-types.ts new file mode 100644 index 0000000..02872af --- /dev/null +++ b/src/erc7677-types.ts @@ -0,0 +1,33 @@ +// https://eips.ethereum.org/EIPS/eip-7677 + +import type { UserOperationHex } from './core' + +// [userOp, entryPoint, chainId, context] +export type GetPaymasterStubDataParams = [ + UserOperationHex, // userOp + string, // EntryPoint + string, // Chain ID + Record, // Context +] + +export type GetPaymasterStubDataResult = { + sponsor?: { name: string; icon?: string } // Sponsor info + paymaster: string // Paymaster address + paymasterData: string // Paymaster data + paymasterVerificationGasLimit: string // Paymaster validation gas + paymasterPostOpGasLimit: string // Paymaster post-op gas + isFinal?: boolean // Indicates that the caller does not need to call pm_getPaymasterData +} + +// [userOp, entryPoint, chainId, context] +export type GetPaymasterDataParams = [ + UserOperationHex, // userOp + string, // Entrypoint + string, // Chain ID + Record, // Context +] + +export type GetPaymasterDataResult = { + paymaster: string // Paymaster address + paymasterData: string // Paymaster data +} diff --git a/src/index.ts b/src/index.ts index a026e0d..4dce82b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,3 +11,4 @@ export * from '@/validations' export * from '@/types' export * from '@/paymasters' export * from '@/fetchGasPrice' +export * from '@/erc7677-types' diff --git a/test/scripts/candide.ts b/test/scripts/candide.ts index 00976ff..70b5263 100644 --- a/test/scripts/candide.ts +++ b/test/scripts/candide.ts @@ -1,14 +1,13 @@ import { KernelAccountAPI } from '@/accounts' import { ADDRESS } from '@/addresses' import { ERC4337Bundler } from '@/core' -import { fetchGasPriceAlchemy, fetchGasPricePimlico } from '@/fetchGasPrice' +import { fetchGasPricePimlico } from '@/fetchGasPrice' import { INTERFACES } from '@/interfaces' import { getECDSAValidator } from '@/validations/getECDSAValidator' import { SingleEOAValidation } from '@/validations/SingleEOAValidation' import { getBytes, JsonRpcProvider, Wallet } from 'ethers' import { alchemy, pimlico } from 'evm-providers' import { buildAccountExecutions } from '../helpers' -import { getPublicPaymaster } from '@/paymasters' // - Candide does not support batching, so you must set `batchMaxCount: 1` on the JSON RPC provider. // - Candide does not support public paymasters, and will throw an error: `ERC4337Error: AA33 revertedb''`. diff --git a/test/scripts/execute-kernel-candide-paymaster.ts b/test/scripts/execute-kernel-candide-paymaster.ts new file mode 100644 index 0000000..9029099 --- /dev/null +++ b/test/scripts/execute-kernel-candide-paymaster.ts @@ -0,0 +1,133 @@ +import { KernelAccountAPI } from '@/accounts' +import { ADDRESS } from '@/addresses' +import { ERC4337Bundler } from '@/core' +import type { + GetPaymasterDataParams, + GetPaymasterDataResult, + GetPaymasterStubDataParams, + GetPaymasterStubDataResult, +} from '@/erc7677-types' +import { fetchGasPricePimlico } from '@/fetchGasPrice' +import { INTERFACES } from '@/interfaces' +import { isSameAddress } from '@/utils' +import { getECDSAValidator } from '@/validations/getECDSAValidator' +import { SingleEOAValidation } from '@/validations/SingleEOAValidation' +import { getAddress, getBytes, JsonRpcProvider, toBeHex, Wallet } from 'ethers' +import { alchemy, pimlico } from 'evm-providers' +import { buildAccountExecutions } from '../helpers' + +const { ALCHEMY_API_KEY = '', PIMLICO_API_KEY = '', DEV_7702_PK = '', CANDIDE_API_KEY = '' } = process.env + +if (!ALCHEMY_API_KEY) { + throw new Error('ALCHEMY_API_KEY is not set') +} +if (!PIMLICO_API_KEY) { + throw new Error('PIMLICO_API_KEY is not set') +} +if (!CANDIDE_API_KEY) { + throw new Error('CANDIDE_API_KEY is not set') +} + +const CHAIN_ID = 84532 + +const rpcUrl = alchemy(CHAIN_ID, ALCHEMY_API_KEY) +const pimlicoUrl = pimlico(CHAIN_ID, PIMLICO_API_KEY) +const candideUrl = `https://api.candide.dev/api/v3/${CHAIN_ID}/${CANDIDE_API_KEY}` +const paymasterUrl = `https://api.candide.dev/paymaster/v3/base-sepolia/${CANDIDE_API_KEY}` + +const client = new JsonRpcProvider(rpcUrl) +const bundler = new ERC4337Bundler(candideUrl, undefined, { + batchMaxCount: 1, // candide doesn't support rpc batching +}) +const paymasterClient = new JsonRpcProvider(paymasterUrl, CHAIN_ID, { + staticNetwork: true, + batchMaxCount: 1, +}) + +const signer = new Wallet(DEV_7702_PK) + +const ecdsaValidator = getECDSAValidator({ ownerAddress: signer.address }) + +const accountAddress = '0x960CBf515F3DcD46f541db66C76Cf7acA5BEf4C7' + +const kernelAPI = new KernelAccountAPI({ + validation: new SingleEOAValidation(), + validatorAddress: ecdsaValidator.address, +}) + +const executions = [ + { + to: ADDRESS.Counter, + value: 0n, + data: INTERFACES.Counter.encodeFunctionData('increment'), + }, +] + +const op = await buildAccountExecutions({ + accountAPI: kernelAPI, + accountAddress, + chainId: CHAIN_ID, + client, + bundler, + executions, +}) + +if (!op.entryPointAddress) { + throw new Error('Entry point address is not set') +} + +const entryPointAddress = getAddress(op.entryPointAddress) + +const supportedEntryPoints = await paymasterClient.send('pm_supportedEntryPoints', []) + +if (!supportedEntryPoints.some((entryPoint: string) => isSameAddress(entryPoint, entryPointAddress))) { + throw new Error('Entry point not supported by paymaster') +} + +const params: GetPaymasterStubDataParams = [ + op.hex(), + getAddress(op.entryPointAddress), + toBeHex(CHAIN_ID), + { sponsorshipPolicyId: 'f0785f78e6678a99' }, +] + +const paymasterStubData: GetPaymasterStubDataResult | null = await paymasterClient.send( + 'pm_getPaymasterStubData', + params, +) + +if (!paymasterStubData) { + throw new Error('Paymaster stub data is falsy') +} +console.log('paymasterStubData', paymasterStubData) + +op.setPaymaster(paymasterStubData) + +op.setGasPrice(await fetchGasPricePimlico(pimlicoUrl)) + +await op.estimateGas() + +if (!paymasterStubData.isFinal) { + const params: GetPaymasterDataParams = [ + op.hex(), + op.entryPointAddress, + toBeHex(CHAIN_ID), + { sponsorshipPolicyId: 'f0785f78e6678a99' }, + ] + const paymasterData: GetPaymasterDataResult | null = await paymasterClient.send('pm_getPaymasterData', params) + console.log('paymasterData', paymasterData) + + if (!paymasterData) { + throw new Error('Paymaster data is falsy') + } + + op.setPaymasterData(paymasterData.paymasterData) +} + +const sig = await signer.signMessage(getBytes(op.hash())) + +op.setSignature(await kernelAPI.formatSignature(sig)) + +await op.send() +const receipt = await op.wait() +console.log('receipt', receipt.success) From 14cab5b8aeb1699707b1b5a4a692ec1f26879a3b Mon Sep 17 00:00:00 2001 From: Johnson Chen Date: Sat, 6 Sep 2025 11:55:37 +0800 Subject: [PATCH 2/5] feat: PaymasterService --- src/index.ts | 1 - src/paymasters/PaymasterService.ts | 84 +++++++++++++++++++ src/{ => paymasters}/erc7677-types.ts | 2 +- src/paymasters/index.ts | 2 + stats.html | 2 +- .../execute-kernel-candide-paymaster.ts | 46 ++++------ 6 files changed, 103 insertions(+), 34 deletions(-) create mode 100644 src/paymasters/PaymasterService.ts rename src/{ => paymasters}/erc7677-types.ts (95%) diff --git a/src/index.ts b/src/index.ts index 4dce82b..a026e0d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,4 +11,3 @@ export * from '@/validations' export * from '@/types' export * from '@/paymasters' export * from '@/fetchGasPrice' -export * from '@/erc7677-types' diff --git a/src/paymasters/PaymasterService.ts b/src/paymasters/PaymasterService.ts new file mode 100644 index 0000000..e442855 --- /dev/null +++ b/src/paymasters/PaymasterService.ts @@ -0,0 +1,84 @@ +import { toUserOpHex, type UserOperation } from '@/core' +import type { AddressLike, BigNumberish, FetchRequest, JsonRpcApiProviderOptions } from 'ethers' +import { getBigInt, JsonRpcProvider, resolveAddress, toBeHex } from 'ethers' +import type { + GetPaymasterDataParams, + GetPaymasterDataResult, + GetPaymasterStubDataParams, + GetPaymasterStubDataResult, +} from './erc7677-types' + +export class PaymasterServiceError extends Error { + constructor(message: string, cause?: unknown) { + super(message, { cause }) + this.name = 'PaymasterServiceError' + } +} + +export class PaymasterService extends JsonRpcProvider { + chainId: bigint + + constructor(url: string | FetchRequest, chainId: BigNumberish, options?: JsonRpcApiProviderOptions) { + const serviceOptions = { + ...options, + batchMaxCount: options?.batchMaxCount ? 1 : options?.batchMaxCount, + staticNetwork: options?.staticNetwork ?? true, + } + super(url, chainId, serviceOptions) + this.chainId = getBigInt(chainId) + } + + /** + * Get supported entry points for this paymaster + * @returns Array of supported entry point addresses + */ + async supportedEntryPoints(): Promise { + return await this.send('pm_supportedEntryPoints', []) + } + + /** + * Get paymaster stub data for a user operation + */ + async getPaymasterStubData({ + userOp, + entryPointAddress, + context, + }: { + userOp: UserOperation + entryPointAddress: AddressLike + context: Record + }): Promise { + let params: GetPaymasterStubDataParams + try { + params = [toUserOpHex(userOp), await resolveAddress(entryPointAddress), toBeHex(this.chainId), context] + } catch (err) { + throw new PaymasterServiceError('Error building params for pm_getPaymasterStubData', { + cause: err, + }) + } + return await this.send('pm_getPaymasterStubData', params) + } + + /** + * Get final paymaster data for a user operation + */ + async getPaymasterData({ + userOp, + entryPointAddress, + context, + }: { + userOp: UserOperation + entryPointAddress: AddressLike + context: Record + }): Promise { + let params: GetPaymasterDataParams + try { + params = [toUserOpHex(userOp), await resolveAddress(entryPointAddress), toBeHex(this.chainId), context] + } catch (err) { + throw new PaymasterServiceError('Error building params for pm_getPaymasterData', { + cause: err, + }) + } + return await this.send('pm_getPaymasterData', params) + } +} diff --git a/src/erc7677-types.ts b/src/paymasters/erc7677-types.ts similarity index 95% rename from src/erc7677-types.ts rename to src/paymasters/erc7677-types.ts index 02872af..e99f534 100644 --- a/src/erc7677-types.ts +++ b/src/paymasters/erc7677-types.ts @@ -1,6 +1,6 @@ // https://eips.ethereum.org/EIPS/eip-7677 -import type { UserOperationHex } from './core' +import type { UserOperationHex } from '../core' // [userOp, entryPoint, chainId, context] export type GetPaymasterStubDataParams = [ diff --git a/src/paymasters/index.ts b/src/paymasters/index.ts index 6b0b773..a7b02f9 100644 --- a/src/paymasters/index.ts +++ b/src/paymasters/index.ts @@ -1 +1,3 @@ export * from './public-paymaster' +export * from './PaymasterService' +export * from './erc7677-types' diff --git a/stats.html b/stats.html index 81215bd..8263220 100644 --- a/stats.html +++ b/stats.html @@ -4929,7 +4929,7 @@