diff --git a/packages/sdk/package.json b/packages/sdk/package.json index c6ff992..b5f84f5 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -104,6 +104,11 @@ "import": "./dist/_esm/abi/index.js", "require": "./dist/_cjs/abi/index.js" }, + "./paymaster": { + "types": "./dist/_types/paymaster/index.d.ts", + "import": "./dist/_esm/paymaster/index.js", + "require": "./dist/_cjs/paymaster/index.js" + }, "./package.json": "./package.json" }, "typesVersions": { diff --git a/packages/sdk/src/client-auth-server/Signer.ts b/packages/sdk/src/client-auth-server/Signer.ts index ac9ccd3..c1514d6 100644 --- a/packages/sdk/src/client-auth-server/Signer.ts +++ b/packages/sdk/src/client-auth-server/Signer.ts @@ -1,7 +1,9 @@ import { type Address, type Chain, createWalletClient, custom, type Hash, http, type RpcSchema as RpcSchemaGeneric, type SendTransactionParameters, type Transport, type WalletClient } from "viem"; +import type { TransactionRequestEIP712 } from "viem/chains"; import { createZksyncSessionClient, type ZksyncSsoSessionClient } from "../client/index.js"; import type { Communicator } from "../communicator/index.js"; +import { type CustomPaymasterHandler, getTransactionWithPaymasterData } from "../paymaster/index.js"; import { StorageItem } from "../utils/storage.js"; import type { AppMetadata, RequestArguments } from "./interface.js"; import type { AuthServerRpcSchema, ExtractParams, ExtractReturnType, Method, RPCRequestMessage, RPCResponseMessage, RpcSchema } from "./rpc.js"; @@ -38,6 +40,7 @@ type SignerConstructorParams = { chains: readonly Chain[]; transports?: Record; session?: () => SessionPreferences | Promise; + paymasterHandler?: CustomPaymasterHandler; }; type ChainsInfo = ExtractReturnType<"eth_requestAccounts", AuthServerRpcSchema>["chainsInfo"]; @@ -49,12 +52,13 @@ export class Signer implements SignerInterface { private readonly chains: readonly Chain[]; private readonly transports: Record = {}; private readonly sessionParameters?: () => (SessionPreferences | Promise); + private readonly paymasterHandler?: CustomPaymasterHandler; private _account: StorageItem; private _chainsInfo = new StorageItem(StorageItem.scopedStorageKey("chainsInfo"), []); private client: { instance: ZksyncSsoSessionClient; type: "session" } | { instance: WalletClient; type: "auth-server" } | undefined; - constructor({ metadata, communicator, updateListener, session, chains, transports }: SignerConstructorParams) { + constructor({ metadata, communicator, updateListener, session, chains, transports, paymasterHandler }: SignerConstructorParams) { if (!chains.length) throw new Error("At least one chain must be included in the config"); this.getMetadata = metadata; @@ -63,6 +67,7 @@ export class Signer implements SignerInterface { this.sessionParameters = session; this.chains = chains; this.transports = transports || {}; + this.paymasterHandler = paymasterHandler; this._account = new StorageItem(StorageItem.scopedStorageKey("account"), null, { onChange: (newValue) => { @@ -136,6 +141,7 @@ export class Signer implements SignerInterface { contracts: chainInfo.contracts, chain, transport: this.transports[chain.id] || http(), + paymasterHandler: this.paymasterHandler, }), }; } else { @@ -266,6 +272,23 @@ export class Signer implements SignerInterface { // Open popup immediately to make sure popup won't be blocked by Safari await this.communicator.ready(); + if (request.method === "eth_sendTransaction") { + const params = request.params![0] as TransactionRequestEIP712; + if (params) { + /* eslint-disable @typescript-eslint/no-unused-vars */ + const { chainId: _, ...transaction } = await getTransactionWithPaymasterData( + this.chain.id, + params.from, + params, + this.paymasterHandler, + ); + request = { + method: request.method, + params: [transaction] as ExtractParams, + }; + } + } + const message = this.createRequestMessage({ action: request, chainId: this.chain.id, diff --git a/packages/sdk/src/client-auth-server/WalletProvider.ts b/packages/sdk/src/client-auth-server/WalletProvider.ts index 693bf6d..35279ae 100644 --- a/packages/sdk/src/client-auth-server/WalletProvider.ts +++ b/packages/sdk/src/client-auth-server/WalletProvider.ts @@ -4,6 +4,7 @@ import { toHex } from "viem"; import { PopupCommunicator } from "../communicator/PopupCommunicator.js"; import { serializeError, standardErrors } from "../errors/index.js"; +import type { CustomPaymasterHandler } from "../paymaster/index.js"; import { getFavicon, getWebsiteName } from "../utils/helpers.js"; import type { AppMetadata, @@ -22,13 +23,14 @@ export type WalletProviderConstructorOptions = { transports?: Record; session?: SessionPreferences | (() => SessionPreferences | Promise); authServerUrl?: string; + paymasterHandler?: CustomPaymasterHandler; }; export class WalletProvider extends EventEmitter implements ProviderInterface { readonly isZksyncSso = true; private signer: Signer; - constructor({ metadata, chains, transports, session, authServerUrl }: WalletProviderConstructorOptions) { + constructor({ metadata, chains, transports, session, authServerUrl, paymasterHandler }: WalletProviderConstructorOptions) { super(); const communicator = new PopupCommunicator(authServerUrl || DEFAULT_AUTH_SERVER_URL); this.signer = new Signer({ @@ -41,6 +43,7 @@ export class WalletProvider extends EventEmitter implements ProviderInterface { chains, transports, session: typeof session === "object" ? () => session : session, + paymasterHandler, }); } diff --git a/packages/sdk/src/client/session/client.ts b/packages/sdk/src/client/session/client.ts index 1aa7616..6a1fa32 100644 --- a/packages/sdk/src/client/session/client.ts +++ b/packages/sdk/src/client/session/client.ts @@ -2,6 +2,7 @@ import { type Account, type Address, type Chain, type Client, createClient, crea import { privateKeyToAccount } from "viem/accounts"; import { zksyncInMemoryNode } from "viem/chains"; +import type { CustomPaymasterHandler } from "../../paymaster/index.js"; import { encodeSessionTx } from "../../utils/encoding.js"; import type { SessionConfig } from "../../utils/session.js"; import { toSessionAccount } from "./account.js"; @@ -88,6 +89,7 @@ export function createZksyncSessionClient< sessionKey: parameters.sessionKey, sessionConfig: parameters.sessionConfig, contracts: parameters.contracts, + paymasterHandler: parameters.paymasterHandler, })) .extend(publicActions) .extend(publicActionsRewrite) @@ -102,6 +104,7 @@ type ZksyncSsoSessionData = { sessionKey: Hash; sessionConfig: SessionConfig; contracts: SessionRequiredContracts; + paymasterHandler?: CustomPaymasterHandler; }; export type ClientWithZksyncSsoSessionData< @@ -139,4 +142,5 @@ export interface ZksyncSsoSessionClientConfig< contracts: SessionRequiredContracts; key?: string; name?: string; + paymasterHandler?: CustomPaymasterHandler; } diff --git a/packages/sdk/src/client/session/decorators/wallet.ts b/packages/sdk/src/client/session/decorators/wallet.ts index 4ff8598..b4d1f7c 100644 --- a/packages/sdk/src/client/session/decorators/wallet.ts +++ b/packages/sdk/src/client/session/decorators/wallet.ts @@ -2,6 +2,7 @@ import { type Account, bytesToHex, type Chain, formatTransaction, type Transport import { deployContract, getAddresses, getChainId, sendRawTransaction, signMessage, signTypedData, writeContract } from "viem/actions"; import { signTransaction, type ZksyncEip712Meta } from "viem/zksync"; +import { getTransactionWithPaymasterData } from "../../../paymaster/index.js"; import { sendEip712Transaction } from "../actions/sendEip712Transaction.js"; import type { ClientWithZksyncSsoSessionData } from "../client.js"; @@ -33,11 +34,19 @@ export function zksyncSsoWalletActions< delete unformattedTx.eip712Meta; } + /* eslint-disable @typescript-eslint/no-unused-vars */ + const { chainId: _, ...unformattedTxWithPaymaster } = await getTransactionWithPaymasterData( + client.chain.id, + client.account.address, + unformattedTx, + client.paymasterHandler, + ); + const formatters = client.chain?.formatters; const format = formatters?.transaction?.format || formatTransaction; const tx = { - ...format(unformattedTx), + ...format(unformattedTxWithPaymaster as any), type: "eip712", }; diff --git a/packages/sdk/src/connector/index.ts b/packages/sdk/src/connector/index.ts index 3167c4b..1bf0525 100644 --- a/packages/sdk/src/connector/index.ts +++ b/packages/sdk/src/connector/index.ts @@ -19,12 +19,14 @@ import { import type { ZksyncSsoSessionClient } from "../client/index.js"; import { EthereumProviderError } from "../errors/errors.js"; import { type AppMetadata, type ProviderInterface, type SessionPreferences, WalletProvider } from "../index.js"; +import type { CustomPaymasterHandler } from "../paymaster/index.js"; export { callPolicy } from "../client-auth-server/index.js"; export type ZksyncSsoConnectorOptions = { metadata?: Partial; session?: SessionPreferences | (() => SessionPreferences | Promise); authServerUrl?: string; + paymasterHandler?: CustomPaymasterHandler; }; export const zksyncSsoConnector = (parameters: ZksyncSsoConnectorOptions) => { @@ -140,6 +142,7 @@ export const zksyncSsoConnector = (parameters: ZksyncSsoConnectorOptions) => { session: parameters.session, transports: config.transports, chains: config.chains, + paymasterHandler: parameters.paymasterHandler, }); } return walletProvider; diff --git a/packages/sdk/src/paymaster/handlers/index.ts b/packages/sdk/src/paymaster/handlers/index.ts new file mode 100644 index 0000000..a9b5966 --- /dev/null +++ b/packages/sdk/src/paymaster/handlers/index.ts @@ -0,0 +1 @@ +export * from "./zyfi.js"; diff --git a/packages/sdk/src/paymaster/handlers/zyfi.ts b/packages/sdk/src/paymaster/handlers/zyfi.ts new file mode 100644 index 0000000..cf924f1 --- /dev/null +++ b/packages/sdk/src/paymaster/handlers/zyfi.ts @@ -0,0 +1,108 @@ +import type { Address, Hex } from "viem"; + +import type { CustomPaymasterHandler, CustomPaymasterHandlerResponse, CustomPaymasterParameters } from "../index.js"; + +const ZYFI_ENDPOINT = "https://api.zyfi.org/api/"; +const ERC20_PAYMASTER = `${ZYFI_ENDPOINT}erc20_paymaster/v1`; +const SPONSORED_PAYMASTER = `${ZYFI_ENDPOINT}erc20_sponsored_paymaster/v1`; + +export interface ZyfiPaymasterParams { + /** Your API key. Get it from Dashboard on https://www.zyfi.org/ */ + apiKey: string; + /** The address of the token to be used for fee payment */ + feeTokenAddress?: Address; + /** Whether to check for NFT ownership for fee payment */ + checkNFT?: boolean; + /** Whether the transaction is on a testnet */ + isTestnet?: boolean; + /** The ratio of fees to be sponsored by the paymaster (0-100) */ + sponsorshipRatio?: number; + /** determines the user nonces interval for which the response will be valid (current nonce + replayLimit). */ + replayLimit?: number; +} + +export function createZyfiPaymaster(params: ZyfiPaymasterParams): CustomPaymasterHandler { + if (!params.apiKey) throw new Error("ZyFi: Provide API KEY"); + if ( + params.sponsorshipRatio !== undefined + && (params.sponsorshipRatio > 100 + || params.sponsorshipRatio < 0) + ) throw new Error("ZyFi: Sponsorship ratio must be between 0-100"); + + return async function zyfiPaymaster(args: CustomPaymasterParameters): CustomPaymasterHandlerResponse { + const url = params.sponsorshipRatio ? SPONSORED_PAYMASTER : ERC20_PAYMASTER; + const payload = { + replayLimit: params.replayLimit, + sponsorshipRatio: params.sponsorshipRatio, + chainId: args.chainId, + checkNFT: params.checkNFT, + feeTokenAddress: params.feeTokenAddress, + isTestnet: params.isTestnet, + gasLimit: args.gas || undefined, + txData: { + from: args.from, + to: args.to, + data: args.data, + value: args.value, + }, + }; + const response: ApiResponse = await fetch( + url, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-Key": params.apiKey, + }, + body: stringify(payload), + }, + ).then((response) => { + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + return response.json(); + }); + + const { paymasterParams, gasPerPubdata } = response.txData.customData; + return { + paymaster: paymasterParams.paymaster, + paymasterInput: paymasterParams.paymasterInput, + maxFeePerGas: BigInt(response.txData.maxFeePerGas), + gas: BigInt(response.txData.gasLimit), + maxPriorityFeePerGas: BigInt(response.txData.maxPriorityFeePerGas ?? 0), + gasPerPubdata: gasPerPubdata ? BigInt(gasPerPubdata) : undefined, + }; + }; +} + +type ApiResponse = { + txData: { + customData: { + paymasterParams: { + paymaster: Address; + paymasterInput: Hex; + }; + gasPerPubdata?: number; + }; + maxFeePerGas: string; + maxPriorityFeePerGas?: string; + gasLimit: number; + }; + gasLimit: string; + gasPrice: string; + tokenAddress: Address; + tokenPrice: string; + feeTokenAmount: string; + feeTokendecimals: string; + feeUSD: string; + expirationTime: string; + expiresIn: string; + paymasterAddress: Address; + messageHash: string; + maxNonce?: string; + protocolAddress?: Address; + sponsorshipRatio?: string; + warnings?: string[]; +}; + +function stringify(payload: any) { + return JSON.stringify(payload, (_, v) => typeof v === "bigint" ? v.toString() : v); +} diff --git a/packages/sdk/src/paymaster/index.ts b/packages/sdk/src/paymaster/index.ts new file mode 100644 index 0000000..f778d69 --- /dev/null +++ b/packages/sdk/src/paymaster/index.ts @@ -0,0 +1,76 @@ +import type { Address, Hex, UnionRequiredBy } from "viem"; +import type { TransactionRequestEIP712 } from "viem/chains"; + +export interface CustomPaymasterParameters { + nonce: number; + from: Address; + to: Address; + gas: bigint; + gasPrice: bigint; + gasPerPubdata: bigint; + value: bigint; + data: Hex | undefined; + maxFeePerGas: bigint; + maxPriorityFeePerGas: bigint; + chainId: number; +} + +export type CustomPaymasterHandlerResponse = Promise<{ + paymaster: Address; + paymasterInput: Hex; + maxFeePerGas?: bigint; + maxPriorityFeePerGas?: bigint; + gasPerPubdata?: bigint; + gas?: bigint; +}>; + +export type CustomPaymasterHandler = ( + args: CustomPaymasterParameters, +) => CustomPaymasterHandlerResponse; + +export async function getTransactionWithPaymasterData( + chainId: number, + fromAccount: Address, + transaction: TransactionRequestEIP712, + customPaymasterHandler: CustomPaymasterHandler | undefined = undefined, +): Promise< + UnionRequiredBy & { chainId: number } + > { + if ( + customPaymasterHandler + && !transaction.paymaster + && !transaction.paymasterInput + ) { + const paymasterResult = await customPaymasterHandler({ + chainId, + from: fromAccount, + data: transaction.data, + gas: transaction.gas ?? 0n, + gasPrice: transaction.gasPrice ?? 0n, + gasPerPubdata: transaction.gasPerPubdata ?? 0n, + maxFeePerGas: transaction.maxFeePerGas ?? 0n, + maxPriorityFeePerGas: transaction.maxPriorityFeePerGas ?? 0n, + nonce: transaction.nonce ?? 0, + to: transaction.to ?? "0x0", + value: transaction.value ?? 0n, + }); + return { + ...transaction, + paymaster: paymasterResult.paymaster, + paymasterInput: paymasterResult.paymasterInput, + gas: paymasterResult.gas ?? transaction.gas, + maxFeePerGas: paymasterResult.maxFeePerGas ?? transaction.maxFeePerGas, + maxPriorityFeePerGas: paymasterResult.maxPriorityFeePerGas ?? transaction.maxPriorityFeePerGas, + gasPerPubdata: paymasterResult.gasPerPubdata ?? transaction.gasPerPubdata, + from: fromAccount, + chainId, + }; + } + return { + ...transaction, + from: fromAccount, + chainId, + }; +} + +export * from "./handlers/index.js";