Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Draft] custom paymaster handler & zyfi integration #51

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
25 changes: 24 additions & 1 deletion packages/sdk/src/client-auth-server/Signer.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -38,6 +40,7 @@ type SignerConstructorParams = {
chains: readonly Chain[];
transports?: Record<number, Transport>;
session?: () => SessionPreferences | Promise<SessionPreferences>;
paymasterHandler?: CustomPaymasterHandler;
};

type ChainsInfo = ExtractReturnType<"eth_requestAccounts", AuthServerRpcSchema>["chainsInfo"];
Expand All @@ -49,12 +52,13 @@ export class Signer implements SignerInterface {
private readonly chains: readonly Chain[];
private readonly transports: Record<number, Transport> = {};
private readonly sessionParameters?: () => (SessionPreferences | Promise<SessionPreferences>);
private readonly paymasterHandler?: CustomPaymasterHandler;

private _account: StorageItem<Account | null>;
private _chainsInfo = new StorageItem<ChainsInfo>(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;
Expand All @@ -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<Account | null>(StorageItem.scopedStorageKey("account"), null, {
onChange: (newValue) => {
Expand Down Expand Up @@ -136,6 +141,7 @@ export class Signer implements SignerInterface {
contracts: chainInfo.contracts,
chain,
transport: this.transports[chain.id] || http(),
paymasterHandler: this.paymasterHandler,
}),
};
} else {
Expand Down Expand Up @@ -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<TMethod, TSchema>,
};
}
}

const message = this.createRequestMessage<TMethod, TSchema>({
action: request,
chainId: this.chain.id,
Expand Down
5 changes: 4 additions & 1 deletion packages/sdk/src/client-auth-server/WalletProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -22,13 +23,14 @@ export type WalletProviderConstructorOptions = {
transports?: Record<number, Transport>;
session?: SessionPreferences | (() => SessionPreferences | Promise<SessionPreferences>);
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({
Expand All @@ -41,6 +43,7 @@ export class WalletProvider extends EventEmitter implements ProviderInterface {
chains,
transports,
session: typeof session === "object" ? () => session : session,
paymasterHandler,
});
}

Expand Down
4 changes: 4 additions & 0 deletions packages/sdk/src/client/session/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -88,6 +89,7 @@ export function createZksyncSessionClient<
sessionKey: parameters.sessionKey,
sessionConfig: parameters.sessionConfig,
contracts: parameters.contracts,
paymasterHandler: parameters.paymasterHandler,
}))
.extend(publicActions)
.extend(publicActionsRewrite)
Expand All @@ -102,6 +104,7 @@ type ZksyncSsoSessionData = {
sessionKey: Hash;
sessionConfig: SessionConfig;
contracts: SessionRequiredContracts;
paymasterHandler?: CustomPaymasterHandler;
};

export type ClientWithZksyncSsoSessionData<
Expand Down Expand Up @@ -139,4 +142,5 @@ export interface ZksyncSsoSessionClientConfig<
contracts: SessionRequiredContracts;
key?: string;
name?: string;
paymasterHandler?: CustomPaymasterHandler;
}
11 changes: 10 additions & 1 deletion packages/sdk/src/client/session/decorators/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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",
};

Expand Down
3 changes: 3 additions & 0 deletions packages/sdk/src/connector/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppMetadata>;
session?: SessionPreferences | (() => SessionPreferences | Promise<SessionPreferences>);
authServerUrl?: string;
paymasterHandler?: CustomPaymasterHandler;
};

export const zksyncSsoConnector = (parameters: ZksyncSsoConnectorOptions) => {
Expand Down Expand Up @@ -140,6 +142,7 @@ export const zksyncSsoConnector = (parameters: ZksyncSsoConnectorOptions) => {
session: parameters.session,
transports: config.transports,
chains: config.chains,
paymasterHandler: parameters.paymasterHandler,
});
}
return walletProvider;
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/src/paymaster/handlers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./zyfi.js";
108 changes: 108 additions & 0 deletions packages/sdk/src/paymaster/handlers/zyfi.ts
Original file line number Diff line number Diff line change
@@ -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);
}
76 changes: 76 additions & 0 deletions packages/sdk/src/paymaster/index.ts
Original file line number Diff line number Diff line change
@@ -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<TransactionRequestEIP712, "from"> & { 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";
Loading