zksync-sso
Version:
ZKsync Smart Sign On SDK
205 lines (184 loc) • 6.86 kB
text/typescript
import { type Account, type Address, type Chain, type Client, encodeFunctionData, type Hash, type Hex, type Prettify, type TransactionReceipt, type Transport } from "viem";
import { readContract, waitForTransactionReceipt } from "viem/actions";
import { getGeneralPaymasterInput, sendTransaction } from "viem/zksync";
import { SessionKeyValidatorAbi } from "../../../abi/SessionKeyValidator.js";
import { type CustomPaymasterHandler, getTransactionWithPaymasterData } from "../../../paymaster/index.js";
import { noThrow } from "../../../utils/helpers.js";
import type { SessionConfig, SessionState, SessionStateEventCallback } from "../../../utils/session.js";
import { SessionEventType, SessionStatus } from "../../../utils/session.js";
export type CreateSessionArgs = {
sessionConfig: SessionConfig;
contracts: {
session: Address; // session module
};
paymaster?: {
address: Address;
paymasterInput?: Hex;
};
onTransactionSent?: (hash: Hash) => void;
paymasterHandler?: CustomPaymasterHandler;
};
export type CreateSessionReturnType = {
transactionReceipt: TransactionReceipt;
};
export const createSession = async <
transport extends Transport,
chain extends Chain,
account extends Account,
>(client: Client<transport, chain, account>, args: Prettify<CreateSessionArgs>): Promise<Prettify<CreateSessionReturnType>> => {
const callData = encodeFunctionData({
abi: SessionKeyValidatorAbi,
functionName: "createSession",
args: [args.sessionConfig],
});
const sendTransactionArgs = {
account: client.account,
to: args.contracts.session,
paymaster: args.paymaster?.address,
paymasterInput: args.paymaster?.address ? (args.paymaster?.paymasterInput || getGeneralPaymasterInput({ innerInput: "0x" })) : undefined,
data: callData,
gas: 10_000_000n, // TODO: Remove when gas estimation is fixed
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
const transactionWithPaymasterData: any = await getTransactionWithPaymasterData(
client.chain.id,
client.account.address,
sendTransactionArgs,
args.paymasterHandler,
);
const transactionHash = await sendTransaction(client, transactionWithPaymasterData);
if (args.onTransactionSent) {
noThrow(() => args.onTransactionSent?.(transactionHash));
}
const transactionReceipt = await waitForTransactionReceipt(client, { hash: transactionHash });
if (transactionReceipt.status !== "success") throw new Error("createSession transaction reverted");
return {
transactionReceipt,
};
};
export type RevokeSessionArgs = {
sessionId: Hash;
contracts: {
session: Address; // session module
};
paymaster?: {
address: Address;
paymasterInput?: Hex;
};
onTransactionSent?: (hash: Hash) => void;
paymasterHandler?: CustomPaymasterHandler;
};
export type RevokeSessionReturnType = {
transactionReceipt: TransactionReceipt;
};
export const revokeSession = async <
transport extends Transport,
chain extends Chain,
account extends Account,
>(client: Client<transport, chain, account>, args: Prettify<RevokeSessionArgs>): Promise<Prettify<RevokeSessionReturnType>> => {
const callData = encodeFunctionData({
abi: SessionKeyValidatorAbi,
functionName: "revokeKey",
args: [args.sessionId],
});
const sendTransactionArgs = {
account: client.account,
to: args.contracts.session,
paymaster: args.paymaster?.address,
paymasterInput: args.paymaster?.address ? (args.paymaster?.paymasterInput || getGeneralPaymasterInput({ innerInput: "0x" })) : undefined,
data: callData,
gas: 10_000_000n, // TODO: Remove when gas estimation is fixed
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
const transactionWithPaymasterData: any = await getTransactionWithPaymasterData(
client.chain.id,
client.account.address,
sendTransactionArgs,
args.paymasterHandler,
);
const transactionHash = await sendTransaction(client, transactionWithPaymasterData);
if (args.onTransactionSent) {
noThrow(() => args.onTransactionSent?.(transactionHash));
}
const transactionReceipt = await waitForTransactionReceipt(client, { hash: transactionHash });
if (transactionReceipt.status !== "success") throw new Error("createSession transaction reverted");
return {
transactionReceipt,
};
};
export type GetSessionStateArgs = {
account: Address;
sessionConfig: SessionConfig;
contracts: {
session: Address; // session module
};
};
export type GetSessionStateReturnType = {
sessionState: SessionState;
};
export const getSessionState = async <
transport extends Transport,
chain extends Chain,
>(client: Client<transport, chain>, args: Prettify<GetSessionStateArgs>): Promise<Prettify<GetSessionStateReturnType>> => {
const sessionState = await readContract(client, {
address: args.contracts.session,
abi: SessionKeyValidatorAbi,
functionName: "sessionState",
args: [args.account, args.sessionConfig],
});
return {
sessionState: sessionState as SessionState,
};
};
export type CheckSessionStateArgs = {
sessionConfig: SessionConfig;
sessionState: SessionState;
onSessionStateChange: SessionStateEventCallback;
sessionNotifyTimeout?: NodeJS.Timeout;
};
export type CheckSessionStateReturnType = {
newTimeout?: NodeJS.Timeout;
};
/**
* Checks the current session state and sets up expiry notification.
* This function will trigger the callback with the session state.
*/
export const sessionStateNotify = (args: Prettify<CheckSessionStateArgs>): CheckSessionStateReturnType => {
// Generate a session ID for tracking timeouts
const { sessionState } = args;
const now = BigInt(Math.floor(Date.now() / 1000));
// Check session status
if (sessionState.status === SessionStatus.NotInitialized) { // Not initialized
args.onSessionStateChange({
type: SessionEventType.Inactive,
message: "Session is not initialized",
});
} else if (sessionState.status === SessionStatus.Closed) { // Closed/Revoked
args.onSessionStateChange({
type: SessionEventType.Revoked,
message: "Session has been revoked",
});
} else if (args.sessionConfig.expiresAt <= now) {
// Session has expired
args.onSessionStateChange({
type: SessionEventType.Expired,
message: "Session has expired",
});
} else {
// Session is active, set up expiry notification
const timeToExpiry = Number(args.sessionConfig.expiresAt - now) * 1000;
if (args.sessionNotifyTimeout) {
clearTimeout(args.sessionNotifyTimeout);
}
const newTimeout = setTimeout(() => {
args.onSessionStateChange({
type: SessionEventType.Expired,
message: "Session has expired",
});
}, timeToExpiry);
return {
newTimeout,
};
}
return {};
};