diff --git a/.env.example b/.env.example index 61167deb8..ae99fef60 100644 --- a/.env.example +++ b/.env.example @@ -3,4 +3,5 @@ CHAIN_ID=84532 RPC_URL= BUNDLER_URL= BICONOMY_SDK_DEBUG=false -RUN_PLAYGROUND=false \ No newline at end of file +RUN_PLAYGROUND=false +PAYMASTER_URL= \ No newline at end of file diff --git a/.size-limit.json b/.size-limit.json index 254ab1c67..c261c3368 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -30,7 +30,7 @@ "name": "paymaster (tree-shaking)", "path": "./dist/_esm/paymaster/index.js", "limit": "15 kB", - "import": "{ createPaymaster }", + "import": "{ toPaymaster }", "ignore": ["node:fs", "fs"] } ] diff --git a/src/account/NexusSmartAccount.ts b/src/account/NexusSmartAccount.ts index 5798385f5..eb6c520f3 100644 --- a/src/account/NexusSmartAccount.ts +++ b/src/account/NexusSmartAccount.ts @@ -50,7 +50,6 @@ import { type IHybridPaymaster, type IPaymaster, Paymaster, - PaymasterMode, type SponsorUserOperationDto } from "../paymaster/index.js" import { @@ -152,10 +151,6 @@ export class NexusSmartAccount extends BaseSmartContractAccount { this.paymaster = new Paymaster({ paymasterUrl: nexusSmartAccountConfig.paymasterUrl }) - } else if (nexusSmartAccountConfig.biconomyPaymasterApiKey) { - this.paymaster = new Paymaster({ - paymasterUrl: `https://paymaster.biconomy.io/api/v1/${nexusSmartAccountConfig.chain.id}/${nexusSmartAccountConfig.biconomyPaymasterApiKey}` - }) } else { this.paymaster = nexusSmartAccountConfig.paymaster } @@ -328,7 +323,7 @@ export class NexusSmartAccount extends BaseSmartContractAccount { * * const amountInWei = await smartAccount.getGasEstimates([tx, tx], { * paymasterServiceData: { - * mode: PaymasterMode.SPONSORED, + * mode: "SPONSORED", * }, * }); * @@ -495,14 +490,14 @@ export class NexusSmartAccount extends BaseSmartContractAccount { * ], * account.address, // Default recipient used if no recipient is present in the withdrawal request * { - * paymasterServiceData: { mode: PaymasterMode.SPONSORED }, + * paymasterServiceData: { mode: "SPONSORED" }, * } * ); * * // OR to withdraw all of the native token, leaving no dust in the smart account * * const { wait } = await smartAccount.withdraw([], account.address, { - * paymasterServiceData: { mode: PaymasterMode.SPONSORED }, + * paymasterServiceData: { mode: "SPONSORED" }, * }); * * const { success } = await wait(); @@ -906,13 +901,13 @@ export class NexusSmartAccount extends BaseSmartContractAccount { * data: encodedCall * } * - * const feeQuotesResponse: FeeQuotesOrDataResponse = await smartAccount.getTokenFees(transaction, { paymasterServiceData: { mode: PaymasterMode.ERC20 } }); + * const feeQuotesResponse: FeeQuotesOrDataResponse = await smartAccount.getTokenFees(transaction, { paymasterServiceData: { mode: "ERC20" } }); * * const userSeletedFeeQuote = feeQuotesResponse.feeQuotes?.[0]; * * const { wait } = await smartAccount.sendTransaction(transaction, { * paymasterServiceData: { - * mode: PaymasterMode.ERC20, + * mode: "ERC20", * feeQuote: userSeletedFeeQuote, * spender: feeQuotesResponse.tokenPaymasterAddress, * }, @@ -976,7 +971,7 @@ export class NexusSmartAccount extends BaseSmartContractAccount { to: await this.getAddress() }, { - paymasterServiceData: { mode: PaymasterMode.ERC20 } + paymasterServiceData: { mode: "ERC20" } } ) @@ -1209,7 +1204,7 @@ export class NexusSmartAccount extends BaseSmartContractAccount { bundlerUrl: `https://bundler.biconomy.io/api/v2/84532/nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f44`, chainId: 84532 }); - const response = await smartAccount.transferOwnership(newOwner, DEFAULT_ECDSA_OWNERSHIP_MODULE, {paymasterServiceData: {mode: PaymasterMode.SPONSORED}}); + const response = await smartAccount.transferOwnership(newOwner, DEFAULT_ECDSA_OWNERSHIP_MODULE, {paymasterServiceData: {mode: "SPONSORED"}}); walletClient = createWalletClient({ newOwnerAccount, @@ -1438,9 +1433,40 @@ export class NexusSmartAccount extends BaseSmartContractAccount { userOp = await this.estimateUserOpGas(userOp) + if (buildUseropDto?.paymasterServiceData?.mode === "SPONSORED") { + userOp = await this.getPaymasterAndData( + userOp, + buildUseropDto?.paymasterServiceData + ) + } + return userOp } + private async getPaymasterAndData( + userOp: Partial, + paymasterServiceData: PaymasterUserOperationDto + ): Promise { + const paymaster = this + .paymaster as IHybridPaymaster + const paymasterData = await paymaster.getPaymasterAndData( + userOp, + paymasterServiceData + ) + const userOpStruct = { + ...userOp, + ...paymasterData, + callGasLimit: BigInt(userOp.callGasLimit ?? 0n), + verificationGasLimit: BigInt(userOp.verificationGasLimit ?? 0n), + preVerificationGas: BigInt(userOp.preVerificationGas ?? 0n), + sender: (await this.getAddress()) as Hex, + paymasterAndData: undefined + // paymasterAndData: paymasterData?.paymasterAndData as Hex + } as UserOperationStruct + + return userOpStruct + } + private validateUserOpAndPaymasterRequest( userOp: Partial, tokenPaymasterRequest: BiconomyTokenPaymasterRequest @@ -1612,7 +1638,7 @@ export class NexusSmartAccount extends BaseSmartContractAccount { * * // If you want to use a paymaster... * const { wait } = await smartAccount.deploy({ - * paymasterServiceData: { mode: PaymasterMode.SPONSORED }, + * paymasterServiceData: { mode: "SPONSORED" }, * }); * * // Or if you can't use a paymaster send native token to this address: diff --git a/src/account/utils/Types.ts b/src/account/utils/Types.ts index 725eb7ec6..2474d1f3e 100644 --- a/src/account/utils/Types.ts +++ b/src/account/utils/Types.ts @@ -18,7 +18,6 @@ import type { FeeQuotesOrDataDto, IPaymaster, PaymasterFeeQuote, - PaymasterMode, SmartAccountData, SponsorUserOperationDto } from "../../paymaster" @@ -256,7 +255,7 @@ export type InitilizationData = { export type PaymasterUserOperationDto = SponsorUserOperationDto & FeeQuotesOrDataDto & { /** mode: sponsored or erc20 */ - mode: PaymasterMode + mode: "SPONSORED" | "ERC20" /** Always recommended, especially when using token paymaster */ calculateGasLimits?: boolean /** Expiry duration in seconds */ diff --git a/src/paymaster/BiconomyPaymaster.ts b/src/paymaster/Paymaster.ts similarity index 94% rename from src/paymaster/BiconomyPaymaster.ts rename to src/paymaster/Paymaster.ts index 042c7ba50..f833ceee3 100644 --- a/src/paymaster/BiconomyPaymaster.ts +++ b/src/paymaster/Paymaster.ts @@ -6,20 +6,20 @@ import { type Transaction, type UserOperationStruct, sendRequest -} from "../account" +} from "../account/index.js" +import { deepHexlify } from "../bundler/index.js" import type { IHybridPaymaster } from "./interfaces/IHybridPaymaster.js" import { ADDRESS_ZERO, ERC20_ABI, MAX_UINT256 } from "./utils/Constants.js" import { getTimestampInSeconds } from "./utils/Helpers.js" -import { - type FeeQuotesOrDataDto, - type FeeQuotesOrDataResponse, - type Hex, - type JsonRpcResponse, - type PaymasterAndDataResponse, - type PaymasterConfig, - type PaymasterFeeQuote, - PaymasterMode, - type SponsorUserOperationDto +import type { + FeeQuotesOrDataDto, + FeeQuotesOrDataResponse, + Hex, + JsonRpcResponse, + PaymasterAndDataResponse, + PaymasterConfig, + PaymasterFeeQuote, + SponsorUserOperationDto } from "./utils/Types.js" const defaultPaymasterConfig: PaymasterConfig = { @@ -29,9 +29,7 @@ const defaultPaymasterConfig: PaymasterConfig = { /** * @dev Hybrid - Generic Gas Abstraction paymaster */ -export class BiconomyPaymaster - implements IHybridPaymaster -{ +export class Paymaster implements IHybridPaymaster { paymasterConfig: PaymasterConfig constructor(config: PaymasterConfig) { @@ -163,7 +161,7 @@ export class BiconomyPaymaster ): Promise { // const userOp = await this.prepareUserOperation(_userOp) - let mode: PaymasterMode | null = null + let mode: "SPONSORED" | "ERC20" | null = null let expiryDuration: number | null = null const calculateGasLimits = paymasterServiceData.calculateGasLimits ?? true let preferredToken: string | null = null @@ -230,7 +228,7 @@ export class BiconomyPaymaster ) if (response?.result) { - if (response.result.mode === PaymasterMode.ERC20) { + if (response.result.mode === "ERC20") { const feeQuotesResponse: Array = response.result.feeQuotes const paymasterAddress: Hex = response.result.paymasterAddress @@ -240,7 +238,7 @@ export class BiconomyPaymaster tokenPaymasterAddress: paymasterAddress } } - if (response.result.mode === PaymasterMode.SPONSORED) { + if (response.result.mode === "SPONSORED") { const paymasterAndData: Hex = response.result.paymasterAndData const preVerificationGas = response.result.preVerificationGas const verificationGasLimit = response.result.verificationGasLimit @@ -267,7 +265,7 @@ export class BiconomyPaymaster // Note: we may not throw if we include strictMode off and return paymasterData '0x'. if ( !this.paymasterConfig.strictMode && - paymasterServiceData.mode === PaymasterMode.SPONSORED && + paymasterServiceData.mode === "SPONSORED" && (error?.message.includes("Smart contract data not found") || error?.message.includes("No policies were set")) // can also check based on error.code being -32xxx @@ -317,7 +315,7 @@ export class BiconomyPaymaster let webhookData: Record | null = null let expiryDuration: number | null = null - if (mode === PaymasterMode.ERC20) { + if (mode === "ERC20") { if ( !paymasterServiceData?.feeTokenAddress && paymasterServiceData?.feeTokenAddress === ADDRESS_ZERO @@ -336,6 +334,8 @@ export class BiconomyPaymaster // Note: The idea is before calling this below rpc, userOp values presense and types should be in accordance with how we call eth_estimateUseropGas on the bundler + const hexlifiedUserOp = deepHexlify(userOp) + try { const response: JsonRpcResponse = await sendRequest( { @@ -344,7 +344,7 @@ export class BiconomyPaymaster body: { method: "pm_sponsorUserOperation", params: [ - userOp, + hexlifiedUserOp, { mode: mode, calculateGasLimits: calculateGasLimits, @@ -401,9 +401,10 @@ export class BiconomyPaymaster return "0x" } - public static async create( - config: PaymasterConfig - ): Promise { - return new BiconomyPaymaster(config) + public static async create(config: PaymasterConfig): Promise { + return new Paymaster(config) } } + +export const toPaymaster = Paymaster.create +export default toPaymaster diff --git a/src/paymaster/index.ts b/src/paymaster/index.ts index 2cebd53f5..70cbb99d8 100644 --- a/src/paymaster/index.ts +++ b/src/paymaster/index.ts @@ -1,8 +1,7 @@ -import { BiconomyPaymaster } from "./BiconomyPaymaster.js" +import { Paymaster } from "./Paymaster.js" export * from "./interfaces/IPaymaster.js" export * from "./interfaces/IHybridPaymaster.js" export * from "./utils/Types.js" -export * from "./BiconomyPaymaster.js" +export * from "./Paymaster.js" -export const Paymaster = BiconomyPaymaster export const createPaymaster = Paymaster.create diff --git a/src/paymaster/utils/Types.ts b/src/paymaster/utils/Types.ts index 1d19d4773..25326220f 100644 --- a/src/paymaster/utils/Types.ts +++ b/src/paymaster/utils/Types.ts @@ -23,7 +23,7 @@ export type PaymasterConfig = { export type SponsorUserOperationDto = { /** mode: sponsored or erc20 */ - mode: PaymasterMode + mode: "SPONSORED" | "ERC20" /** Always recommended, especially when using token paymaster */ calculateGasLimits?: boolean /** Expiry duration in seconds */ @@ -38,7 +38,7 @@ export type SponsorUserOperationDto = { export type FeeQuotesOrDataDto = { /** mode: sponsored or erc20 */ - mode?: PaymasterMode + mode?: "SPONSORED" | "ERC20" /** Expiry duration in seconds */ expiryDuration?: number /** Always recommended, especially when using token paymaster */ @@ -121,8 +121,3 @@ export type PaymasterAndDataResponse = { /* Value used by inner account execution */ callGasLimit: number } - -export enum PaymasterMode { - ERC20 = "ERC20", - SPONSORED = "SPONSORED" -} diff --git a/tests/playground.test.ts b/tests/playground.test.ts index bbfd7db50..1a689c397 100644 --- a/tests/playground.test.ts +++ b/tests/playground.test.ts @@ -1,8 +1,5 @@ -import { config } from "dotenv" import { http, - Account, - type Address, type Chain, type Hex, type PrivateKeyAccount, @@ -11,28 +8,17 @@ import { createPublicClient, createWalletClient } from "viem" -import { privateKeyToAccount } from "viem/accounts" -import { beforeAll, describe, expect, test } from "vitest" +import { beforeAll, expect, test } from "vitest" import { type NexusSmartAccount, - createSmartAccountClient, - getChain, - getCustomChain + createSmartAccountClient } from "../src/account" -import { createK1ValidatorModule } from "../src/modules" import { type TestFileNetworkType, describeWithPlaygroundGuard, toNetwork } from "./src/testSetup" -import { - type MasterClient, - type NetworkConfig, - getBundlerUrl, - getTestAccount, - toTestClient, - topUp -} from "./src/testUtils" +import type { NetworkConfig } from "./src/testUtils" const NETWORK_TYPE: TestFileNetworkType = "PUBLIC_TESTNET" @@ -46,6 +32,7 @@ describeWithPlaygroundGuard("playground", () => { // Nexus Config let chain: Chain let bundlerUrl: string + let paymasterUrl: undefined | string let walletClient: WalletClient // Test utils @@ -59,6 +46,7 @@ describeWithPlaygroundGuard("playground", () => { chain = network.chain bundlerUrl = network.bundlerUrl + paymasterUrl = network.paymasterUrl account = network.account as PrivateKeyAccount walletClient = createWalletClient({ @@ -165,4 +153,37 @@ describeWithPlaygroundGuard("playground", () => { expect(balanceAfter - balanceBefore).toBe(1n) }) + + test("should send a userOp using pm_sponsorUserOperation", async () => { + if (!paymasterUrl) { + console.log("No paymaster url provided") + return + } + + const smartAccount = await createSmartAccountClient({ + signer: walletClient, + chain, + paymasterUrl, + bundlerUrl, + // Remove the following lines to use the default factory and validator addresses + // These are relevant only for now on sopelia chain and are likely to change + k1ValidatorAddress, + factoryAddress + }) + + expect(async () => + smartAccount.sendTransaction( + { + to: account.address, + data: "0x", + value: 1n + }, + { + paymasterServiceData: { + mode: "SPONSORED" + } + } + ) + ).rejects.toThrow("Error in generating paymasterAndData") + }) }) diff --git a/tests/src/testSetup.ts b/tests/src/testSetup.ts index e4f0e13c6..53505827e 100644 --- a/tests/src/testSetup.ts +++ b/tests/src/testSetup.ts @@ -57,3 +57,7 @@ export const toNetwork = async ( export const describeWithPlaygroundGuard = process.env.RUN_PLAYGROUND === "true" ? describe : describe.skip + +export const describeWithPaymasterGuard = process.env.PAYMASTER_URL + ? describe + : describe.skip diff --git a/tests/src/testUtils.ts b/tests/src/testUtils.ts index 054233dc6..c2e4fcf5d 100644 --- a/tests/src/testUtils.ts +++ b/tests/src/testUtils.ts @@ -55,6 +55,7 @@ export type NetworkConfig = Omit< "instance" | "bundlerInstance" > & { account?: PrivateKeyAccount + paymasterUrl?: string } export const pKey = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" // This is a publicly available private key meant only for testing only @@ -91,11 +92,13 @@ export const initTestnetNetwork = async (): Promise => { const chainId = process.env.CHAIN_ID const rpcUrl = process.env.RPC_URL //Optional, taken from chain (using chainId) if not provided const _bundlerUrl = process.env.BUNDLER_URL // Optional, taken from chain (using chainId) if not provided + const paymasterUrl = process.env.PAYMASTER_URL // Optional let chain: Chain if (!privateKey) throw new Error("Missing env var E2E_PRIVATE_KEY_ONE") if (!chainId) throw new Error("Missing env var CHAIN_ID") + if (!paymasterUrl) console.log("Missing env var PAYMASTER_URL") try { chain = getChain(+chainId) @@ -110,6 +113,7 @@ export const initTestnetNetwork = async (): Promise => { rpcPort: 0, chain, bundlerUrl, + paymasterUrl, bundlerPort: 0, account: privateKeyToAccount( privateKey?.startsWith("0x") ? (privateKey as Hex) : `0x${privateKey}`