Skip to content

Commit c97c62f

Browse files
authored
feat(sdk): session state checks and tx verifier (#140)
1 parent b47558d commit c97c62f

File tree

9 files changed

+412
-11
lines changed

9 files changed

+412
-11
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ simplifying user authentication, session management, and transaction processing.
1919
- 🔑 Passkey authentication (no seed phrases)
2020
- ⏰ Sessions w/ easy configuration and management
2121
- 💰 Integrated paymaster support
22-
- ❤️‍🩹 Account recovery _(Coming Soon)_
22+
- ❤️‍🩹 Account recovery
2323
- 💻 Simple SDKs : JavaScript, iOS/Android _(Coming Soon)_
2424
- 🤝 Open-source authentication server
2525
- 🎓 Examples to get started quickly
@@ -82,6 +82,15 @@ const ssoConnector = zksyncSsoConnector({
8282
}),
8383
],
8484
},
85+
86+
// Optional: Receive notifications about session state changes
87+
onSessionStateChange: ({ state, address, chainId }) => {
88+
console.log(`Session state for address ${address} changed: ${state.type} - ${state.message}`);
89+
90+
// Use this to notify users and restart the session if needed
91+
// - Session expired: state.type === 'session_expired'
92+
// - Session inactive (e.g. was revoked): state.type === 'session_inactive'
93+
},
8594
});
8695

8796
const wagmiConfig = createConfig({

packages/sdk/README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ simplifying user authentication, session management, and transaction processing.
1717
- 🔑 Passkey authentication (no seed phrases)
1818
- ⏰ Sessions w/ easy configuration and management
1919
- 💰 Integrated paymaster support
20-
- ❤️‍🩹 Account recovery _(Coming Soon)_
20+
- ❤️‍🩹 Account recovery
2121
- 💻 Simple SDKs : JavaScript, iOS/Android _(Coming Soon)_
2222
- 🤝 Open-source authentication server
2323
- 🎓 Examples to get started quickly
@@ -79,7 +79,16 @@ const ssoConnector = zksyncSsoConnector({
7979
],
8080
}),
8181
],
82-
},
82+
},
83+
84+
// Optional: Receive notifications about session state changes
85+
onSessionStateChange: ({ state, address, chainId }) => {
86+
console.log(`Session state for address ${address} changed: ${state.type} - ${state.message}`);
87+
88+
// Use this to notify users and restart the session if needed
89+
// - Session expired: state.type === 'session_expired'
90+
// - Session inactive (e.g. was revoked): eve.state.type === 'session_inactive'
91+
},
8392
});
8493

8594
const wagmiConfig = createConfig({

packages/sdk/src/client-auth-server/Signer.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { TransactionRequestEIP712 } from "viem/chains";
44
import { createZksyncSessionClient, type ZksyncSsoSessionClient } from "../client/index.js";
55
import type { Communicator } from "../communicator/index.js";
66
import { type CustomPaymasterHandler, getTransactionWithPaymasterData } from "../paymaster/index.js";
7+
import type { SessionStateEvent } from "../utils/session.js";
78
import { StorageItem } from "../utils/storage.js";
89
import type { AppMetadata, RequestArguments } from "./interface.js";
910
import type { AuthServerRpcSchema, ExtractParams, ExtractReturnType, Method, RPCRequestMessage, RPCResponseMessage, RpcSchema } from "./rpc.js";
@@ -41,6 +42,7 @@ type SignerConstructorParams = {
4142
transports?: Record<number, Transport>;
4243
session?: () => SessionPreferences | Promise<SessionPreferences>;
4344
paymasterHandler?: CustomPaymasterHandler;
45+
onSessionStateChange?: (event: { address: Address; chainId: number; state: SessionStateEvent }) => void;
4446
};
4547

4648
type ChainsInfo = ExtractReturnType<"eth_requestAccounts", AuthServerRpcSchema>["chainsInfo"];
@@ -53,12 +55,13 @@ export class Signer implements SignerInterface {
5355
private readonly transports: Record<number, Transport> = {};
5456
private readonly sessionParameters?: () => (SessionPreferences | Promise<SessionPreferences>);
5557
private readonly paymasterHandler?: CustomPaymasterHandler;
58+
private readonly onSessionStateChange?: SignerConstructorParams["onSessionStateChange"];
5659

5760
private _account: StorageItem<Account | null>;
5861
private _chainsInfo = new StorageItem<ChainsInfo>(StorageItem.scopedStorageKey("chainsInfo"), []);
5962
private client: { instance: ZksyncSsoSessionClient; type: "session" } | { instance: WalletClient; type: "auth-server" } | undefined;
6063

61-
constructor({ metadata, communicator, updateListener, session, chains, transports, paymasterHandler }: SignerConstructorParams) {
64+
constructor({ metadata, communicator, updateListener, session, chains, transports, paymasterHandler, onSessionStateChange }: SignerConstructorParams) {
6265
if (!chains.length) throw new Error("At least one chain must be included in the config");
6366

6467
this.getMetadata = metadata;
@@ -68,6 +71,7 @@ export class Signer implements SignerInterface {
6871
this.chains = chains;
6972
this.transports = transports || {};
7073
this.paymasterHandler = paymasterHandler;
74+
this.onSessionStateChange = onSessionStateChange;
7175

7276
this._account = new StorageItem<Account | null>(StorageItem.scopedStorageKey("account"), null, {
7377
onChange: (newValue) => {
@@ -142,6 +146,14 @@ export class Signer implements SignerInterface {
142146
chain,
143147
transport: this.transports[chain.id] || http(),
144148
paymasterHandler: this.paymasterHandler,
149+
onSessionStateChange: (event: SessionStateEvent) => {
150+
if (!this.onSessionStateChange) return;
151+
this.onSessionStateChange({
152+
state: event,
153+
address: this.account!.address,
154+
chainId: chain.id,
155+
});
156+
},
145157
}),
146158
};
147159
} else {

packages/sdk/src/client-auth-server/WalletProvider.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { PopupCommunicator } from "../communicator/PopupCommunicator.js";
66
import { serializeError, standardErrors } from "../errors/index.js";
77
import type { CustomPaymasterHandler } from "../paymaster/index.js";
88
import { getFavicon, getWebsiteName } from "../utils/helpers.js";
9+
import type { SessionStateEvent } from "../utils/session.js";
910
import type {
1011
AppMetadata,
1112
ProviderInterface,
@@ -24,13 +25,14 @@ export type WalletProviderConstructorOptions = {
2425
session?: SessionPreferences | (() => SessionPreferences | Promise<SessionPreferences>);
2526
authServerUrl?: string;
2627
paymasterHandler?: CustomPaymasterHandler;
28+
onSessionStateChange?: (state: { address: Address; chainId: number; state: SessionStateEvent }) => void;
2729
};
2830

2931
export class WalletProvider extends EventEmitter implements ProviderInterface {
3032
readonly isZksyncSso = true;
3133
private signer: Signer;
3234

33-
constructor({ metadata, chains, transports, session, authServerUrl, paymasterHandler }: WalletProviderConstructorOptions) {
35+
constructor({ metadata, chains, transports, session, authServerUrl, paymasterHandler, onSessionStateChange }: WalletProviderConstructorOptions) {
3436
super();
3537
const communicator = new PopupCommunicator(authServerUrl || DEFAULT_AUTH_SERVER_URL);
3638
this.signer = new Signer({
@@ -45,6 +47,7 @@ export class WalletProvider extends EventEmitter implements ProviderInterface {
4547
transports,
4648
session: typeof session === "object" ? () => session : session,
4749
paymasterHandler,
50+
onSessionStateChange,
4851
});
4952
}
5053

packages/sdk/src/client/session/actions/session.ts

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { type Account, type Address, type Chain, type Client, encodeFunctionData, type Hash, type Hex, type Prettify, type TransactionReceipt, type Transport } from "viem";
2-
import { waitForTransactionReceipt } from "viem/actions";
2+
import { readContract, waitForTransactionReceipt } from "viem/actions";
33
import { getGeneralPaymasterInput, sendTransaction } from "viem/zksync";
44

55
import { SessionKeyValidatorAbi } from "../../../abi/SessionKeyValidator.js";
66
import { type CustomPaymasterHandler, getTransactionWithPaymasterData } from "../../../paymaster/index.js";
77
import { noThrow } from "../../../utils/helpers.js";
8-
import type { SessionConfig } from "../../../utils/session.js";
8+
import type { SessionConfig, SessionState, SessionStateEventCallback } from "../../../utils/session.js";
9+
import { SessionEventType, SessionStatus } from "../../../utils/session.js";
910

1011
export type CreateSessionArgs = {
1112
sessionConfig: SessionConfig;
@@ -119,3 +120,85 @@ export const revokeSession = async <
119120
transactionReceipt,
120121
};
121122
};
123+
124+
export type GetSessionStateArgs = {
125+
account: Address;
126+
sessionConfig: SessionConfig;
127+
contracts: {
128+
session: Address; // session module
129+
};
130+
};
131+
export type GetSessionStateReturnType = {
132+
sessionState: SessionState;
133+
};
134+
export const getSessionState = async <
135+
transport extends Transport,
136+
chain extends Chain,
137+
>(client: Client<transport, chain>, args: Prettify<GetSessionStateArgs>): Promise<Prettify<GetSessionStateReturnType>> => {
138+
const sessionState = await readContract(client, {
139+
address: args.contracts.session,
140+
abi: SessionKeyValidatorAbi,
141+
functionName: "sessionState",
142+
args: [args.account, args.sessionConfig],
143+
});
144+
145+
return {
146+
sessionState: sessionState as SessionState,
147+
};
148+
};
149+
150+
export type CheckSessionStateArgs = {
151+
sessionConfig: SessionConfig;
152+
sessionState: SessionState;
153+
onSessionStateChange: SessionStateEventCallback;
154+
sessionNotifyTimeout?: NodeJS.Timeout;
155+
};
156+
export type CheckSessionStateReturnType = {
157+
newTimeout?: NodeJS.Timeout;
158+
};
159+
160+
/**
161+
* Checks the current session state and sets up expiry notification.
162+
* This function will trigger the callback with the session state.
163+
*/
164+
export const sessionStateNotify = (args: Prettify<CheckSessionStateArgs>): CheckSessionStateReturnType => {
165+
// Generate a session ID for tracking timeouts
166+
const { sessionState } = args;
167+
const now = BigInt(Math.floor(Date.now() / 1000));
168+
169+
// Check session status
170+
if (sessionState.status === SessionStatus.NotInitialized) { // Not initialized
171+
args.onSessionStateChange({
172+
type: SessionEventType.Inactive,
173+
message: "Session is not initialized",
174+
});
175+
} else if (sessionState.status === SessionStatus.Closed) { // Closed/Revoked
176+
args.onSessionStateChange({
177+
type: SessionEventType.Revoked,
178+
message: "Session has been revoked",
179+
});
180+
} else if (args.sessionConfig.expiresAt <= now) {
181+
// Session has expired
182+
args.onSessionStateChange({
183+
type: SessionEventType.Expired,
184+
message: "Session has expired",
185+
});
186+
} else {
187+
// Session is active, set up expiry notification
188+
const timeToExpiry = Number(args.sessionConfig.expiresAt - now) * 1000;
189+
if (args.sessionNotifyTimeout) {
190+
clearTimeout(args.sessionNotifyTimeout);
191+
}
192+
const newTimeout = setTimeout(() => {
193+
args.onSessionStateChange({
194+
type: SessionEventType.Expired,
195+
message: "Session has expired",
196+
});
197+
}, timeToExpiry);
198+
return {
199+
newTimeout,
200+
};
201+
}
202+
203+
return {};
204+
};

packages/sdk/src/client/session/client.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import { zksyncInMemoryNode } from "viem/chains";
44

55
import type { CustomPaymasterHandler } from "../../paymaster/index.js";
66
import { encodeSessionTx } from "../../utils/encoding.js";
7-
import type { SessionConfig } from "../../utils/session.js";
7+
import type { SessionConfig, SessionStateEventCallback } from "../../utils/session.js";
88
import { toSessionAccount } from "./account.js";
9+
import { getSessionState, sessionStateNotify } from "./actions/session.js";
910
import { publicActionsRewrite } from "./decorators/publicActionsRewrite.js";
1011
import { type ZksyncSsoWalletActions, zksyncSsoWalletActions } from "./decorators/wallet.js";
1112

@@ -90,10 +91,31 @@ export function createZksyncSessionClient<
9091
sessionConfig: parameters.sessionConfig,
9192
contracts: parameters.contracts,
9293
paymasterHandler: parameters.paymasterHandler,
94+
onSessionStateChange: parameters.onSessionStateChange,
95+
_sessionNotifyTimeout: undefined as NodeJS.Timeout | undefined,
9396
}))
9497
.extend(publicActions)
9598
.extend(publicActionsRewrite)
9699
.extend(zksyncSsoWalletActions);
100+
101+
// Check session state on initialization if callback is provided
102+
if (client.onSessionStateChange) {
103+
getSessionState(client, {
104+
account: client.account.address,
105+
sessionConfig: client.sessionConfig,
106+
contracts: client.contracts,
107+
}).then(({ sessionState }) => {
108+
sessionStateNotify({
109+
sessionConfig: client.sessionConfig,
110+
sessionState,
111+
onSessionStateChange: client.onSessionStateChange!,
112+
sessionNotifyTimeout: client._sessionNotifyTimeout,
113+
});
114+
}).catch((error) => {
115+
console.error("Failed to get session state on initialization:", error);
116+
});
117+
}
118+
97119
return client;
98120
}
99121

@@ -105,6 +127,8 @@ type ZksyncSsoSessionData = {
105127
sessionConfig: SessionConfig;
106128
contracts: SessionRequiredContracts;
107129
paymasterHandler?: CustomPaymasterHandler;
130+
onSessionStateChange?: SessionStateEventCallback;
131+
_sessionNotifyTimeout?: NodeJS.Timeout;
108132
};
109133

110134
export type ClientWithZksyncSsoSessionData<
@@ -143,4 +167,5 @@ export interface ZksyncSsoSessionClientConfig<
143167
key?: string;
144168
name?: string;
145169
paymasterHandler?: CustomPaymasterHandler;
170+
onSessionStateChange?: SessionStateEventCallback;
146171
}

packages/sdk/src/client/session/decorators/wallet.ts

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,51 @@ import {
44
type Transport, type WalletActions } from "viem";
55
import {
66
deployContract, getAddresses, getCallsStatus, getCapabilities, getChainId, prepareAuthorization, sendCalls, sendRawTransaction,
7-
showCallsStatus,
8-
signAuthorization,
9-
signMessage, signTypedData, waitForCallsStatus, writeContract,
7+
showCallsStatus, signAuthorization, signMessage, signTypedData, waitForCallsStatus, writeContract,
108
} from "viem/actions";
119
import { signTransaction, type TransactionRequestEIP712, type ZksyncEip712Meta } from "viem/zksync";
1210

1311
import { getTransactionWithPaymasterData } from "../../../paymaster/index.js";
12+
import { SessionErrorType, SessionEventType, type SessionState, validateSessionTransaction } from "../../../utils/session.js";
1413
import { sendEip712Transaction } from "../actions/sendEip712Transaction.js";
14+
import { getSessionState, sessionStateNotify } from "../actions/session.js";
1515
import type { ClientWithZksyncSsoSessionData } from "../client.js";
1616

1717
export type ZksyncSsoWalletActions<chain extends Chain, account extends Account> = Omit<
1818
WalletActions<chain, account>, "addChain" | "getPermissions" | "requestAddresses" | "requestPermissions" | "switchChain" | "watchAsset" | "prepareTransactionRequest"
1919
>;
2020

21+
const sessionErrorToSessionEventType = {
22+
[SessionErrorType.SessionInactive]: SessionEventType.Inactive,
23+
[SessionErrorType.SessionExpired]: SessionEventType.Expired,
24+
};
25+
26+
/**
27+
* Helper function to check session state and notify via callback
28+
*/
29+
async function getSessionStateAndNotify<
30+
transport extends Transport,
31+
chain extends Chain,
32+
account extends Account,
33+
>(client: ClientWithZksyncSsoSessionData<transport, chain, account>): Promise<SessionState> {
34+
const { sessionState } = await getSessionState(client, {
35+
account: client.account.address,
36+
sessionConfig: client.sessionConfig,
37+
contracts: client.contracts,
38+
});
39+
40+
if (client.onSessionStateChange) {
41+
sessionStateNotify({
42+
sessionConfig: client.sessionConfig,
43+
sessionState,
44+
onSessionStateChange: client.onSessionStateChange,
45+
sessionNotifyTimeout: client._sessionNotifyTimeout,
46+
});
47+
}
48+
49+
return sessionState;
50+
}
51+
2152
export function zksyncSsoWalletActions<
2253
transport extends Transport,
2354
chain extends Chain,
@@ -29,6 +60,29 @@ export function zksyncSsoWalletActions<
2960
getChainId: () => getChainId(client),
3061
sendRawTransaction: (args) => sendRawTransaction(client, args),
3162
sendTransaction: async (args) => {
63+
// Get current session state and trigger callback if needed
64+
const sessionState = await getSessionStateAndNotify(client);
65+
66+
// Validate transaction against session constraints
67+
const validationResult = validateSessionTransaction({
68+
sessionState,
69+
sessionConfig: client.sessionConfig,
70+
transaction: args as any,
71+
});
72+
73+
// Throw error if validation fails
74+
if (validationResult.error) {
75+
// If validation fails due to session issues, notify via callback
76+
if (client.onSessionStateChange && Object.keys(sessionErrorToSessionEventType).includes(validationResult.error.type)) {
77+
client.onSessionStateChange({
78+
type: sessionErrorToSessionEventType[validationResult.error.type as keyof typeof sessionErrorToSessionEventType],
79+
message: validationResult.error.message,
80+
});
81+
}
82+
throw new Error(`Session validation failed: ${validationResult.error.message} (${validationResult.error.type})`);
83+
}
84+
85+
// Process transaction if it's valid
3286
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3387
const unformattedTx: any = Object.assign({}, args);
3488

packages/sdk/src/utils/helpers.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,12 @@ export function noThrow<T>(fn: () => T): T | null {
4141
return null;
4242
}
4343
}
44+
45+
export function findSmallestBigInt(arr: bigint[]): bigint {
46+
if (arr.length === 0) throw new Error("Array must not be empty");
47+
let smallest = arr[0];
48+
for (const num of arr) {
49+
if (num < smallest) smallest = num;
50+
}
51+
return smallest;
52+
}

0 commit comments

Comments
 (0)