@0xsequence/dapp-client 0.0.0-20250910142613

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/CHANGELOG.md +11 -0
  3. package/LICENSE +202 -0
  4. package/README.md +238 -0
  5. package/dist/ChainSessionManager.d.ts +203 -0
  6. package/dist/ChainSessionManager.d.ts.map +1 -0
  7. package/dist/ChainSessionManager.js +742 -0
  8. package/dist/DappClient.d.ts +409 -0
  9. package/dist/DappClient.d.ts.map +1 -0
  10. package/dist/DappClient.js +667 -0
  11. package/dist/DappTransport.d.ts +47 -0
  12. package/dist/DappTransport.d.ts.map +1 -0
  13. package/dist/DappTransport.js +443 -0
  14. package/dist/index.d.ts +11 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +7 -0
  17. package/dist/types/index.d.ts +168 -0
  18. package/dist/types/index.d.ts.map +1 -0
  19. package/dist/types/index.js +25 -0
  20. package/dist/utils/constants.d.ts +6 -0
  21. package/dist/utils/constants.d.ts.map +1 -0
  22. package/dist/utils/constants.js +5 -0
  23. package/dist/utils/errors.d.ts +28 -0
  24. package/dist/utils/errors.d.ts.map +1 -0
  25. package/dist/utils/errors.js +54 -0
  26. package/dist/utils/index.d.ts +11 -0
  27. package/dist/utils/index.d.ts.map +1 -0
  28. package/dist/utils/index.js +135 -0
  29. package/dist/utils/storage.d.ts +64 -0
  30. package/dist/utils/storage.d.ts.map +1 -0
  31. package/dist/utils/storage.js +196 -0
  32. package/eslint.config.mjs +4 -0
  33. package/package.json +38 -0
  34. package/src/ChainSessionManager.ts +978 -0
  35. package/src/DappClient.ts +801 -0
  36. package/src/DappTransport.ts +518 -0
  37. package/src/index.ts +47 -0
  38. package/src/types/index.ts +218 -0
  39. package/src/utils/constants.ts +5 -0
  40. package/src/utils/errors.ts +62 -0
  41. package/src/utils/index.ts +158 -0
  42. package/src/utils/storage.ts +272 -0
  43. package/tsconfig.json +10 -0
@@ -0,0 +1,978 @@
1
+ import { Envelope, Relayer, Signers, State, Wallet } from '@0xsequence/wallet-core'
2
+ import { Attestation, Constants, Extensions, Payload, SessionConfig } from '@0xsequence/wallet-primitives'
3
+ import * as Guard from '@0xsequence/guard'
4
+ import { AbiFunction, Address, Hex, Provider, RpcTransport, Secp256k1 } from 'ox'
5
+
6
+ import { DappTransport } from './DappTransport.js'
7
+
8
+ import {
9
+ AddExplicitSessionError,
10
+ FeeOptionError,
11
+ InitializationError,
12
+ ModifyExplicitSessionError,
13
+ SessionConfigError,
14
+ TransactionError,
15
+ WalletRedirectError,
16
+ } from './utils/errors.js'
17
+ import { SequenceStorage } from './utils/storage.js'
18
+ import { getRelayerUrl, getRpcUrl } from './utils/index.js'
19
+
20
+ import {
21
+ AddExplicitSessionPayload,
22
+ CreateNewSessionPayload,
23
+ ConnectSuccessResponsePayload,
24
+ ExplicitSessionEventListener,
25
+ ModifySessionPayload,
26
+ ModifySessionSuccessResponsePayload,
27
+ LoginMethod,
28
+ RandomPrivateKeyFn,
29
+ RequestActionType,
30
+ Session,
31
+ Transaction,
32
+ TransportMode,
33
+ GuardConfig,
34
+ } from './types/index.js'
35
+ import { CACHE_DB_NAME, VALUE_FORWARDER_ADDRESS } from './utils/constants.js'
36
+
37
+ interface ChainSessionManagerEventMap {
38
+ explicitSessionResponse: ExplicitSessionEventListener
39
+ }
40
+
41
+ /**
42
+ * Manages sessions and wallet interactions for a single blockchain.
43
+ * This class is used internally by the DappClient to handle chain-specific logic.
44
+ */
45
+ export class ChainSessionManager {
46
+ private readonly instanceId: string
47
+
48
+ private stateProvider: State.Provider
49
+
50
+ private readonly redirectUrl: string
51
+ private readonly randomPrivateKeyFn: RandomPrivateKeyFn
52
+
53
+ private eventListeners: {
54
+ [K in keyof ChainSessionManagerEventMap]?: Set<ChainSessionManagerEventMap[K]>
55
+ } = {}
56
+
57
+ private sessions: Session[] = []
58
+
59
+ private walletAddress: Address.Address | null = null
60
+ private sessionManager: Signers.SessionManager | null = null
61
+ private wallet: Wallet | null = null
62
+ private provider: Provider.Provider | null = null
63
+ private relayer: Relayer.Standard.Rpc.RpcRelayer
64
+ private readonly chainId: number
65
+ public transport: DappTransport | null = null
66
+ private sequenceStorage: SequenceStorage
67
+ public isInitialized: boolean = false
68
+ private isInitializing: boolean = false
69
+ public loginMethod: LoginMethod | null = null
70
+ public userEmail: string | null = null
71
+ private guard?: GuardConfig
72
+
73
+ /**
74
+ * @param chainId The ID of the chain this manager is responsible for.
75
+ * @param keyMachineUrl The URL of the key management service.
76
+ * @param transport The transport mechanism for communicating with the wallet.
77
+ * @param sequenceStorage The storage implementation for persistent session data.
78
+ * @param redirectUrl (Optional) The URL to redirect back to after a redirect-based flow.
79
+ * @param guard (Optional) The guard config to use for the session.
80
+ * @param randomPrivateKeyFn (Optional) A function to generate random private keys.
81
+ * @param canUseIndexedDb (Optional) A flag to enable or disable IndexedDB for caching.
82
+ */
83
+ constructor(
84
+ chainId: number,
85
+ transport: DappTransport,
86
+ projectAccessKey: string,
87
+ keyMachineUrl: string,
88
+ nodesUrl: string,
89
+ relayerUrl: string,
90
+ sequenceStorage: SequenceStorage,
91
+ redirectUrl: string,
92
+ guard?: GuardConfig,
93
+ randomPrivateKeyFn?: RandomPrivateKeyFn,
94
+ canUseIndexedDb: boolean = true,
95
+ ) {
96
+ this.instanceId = `manager-${Math.random().toString(36).substring(2, 9)}`
97
+ console.log(`ChainSessionManager instance created: ${this.instanceId} for chain ${chainId}`)
98
+
99
+ const rpcUrl = getRpcUrl(chainId, nodesUrl, projectAccessKey)
100
+ this.chainId = chainId
101
+
102
+ if (canUseIndexedDb) {
103
+ this.stateProvider = new State.Cached({
104
+ source: new State.Sequence.Provider(keyMachineUrl),
105
+ cache: new State.Local.Provider(new State.Local.IndexedDbStore(CACHE_DB_NAME)),
106
+ })
107
+ } else {
108
+ this.stateProvider = new State.Sequence.Provider(keyMachineUrl)
109
+ }
110
+ this.guard = guard
111
+ this.provider = Provider.from(RpcTransport.fromHttp(rpcUrl))
112
+ this.relayer = new Relayer.Standard.Rpc.RpcRelayer(
113
+ getRelayerUrl(chainId, relayerUrl),
114
+ this.chainId,
115
+ getRpcUrl(chainId, nodesUrl, projectAccessKey),
116
+ )
117
+
118
+ this.transport = transport
119
+ this.sequenceStorage = sequenceStorage
120
+
121
+ this.redirectUrl = redirectUrl
122
+ this.randomPrivateKeyFn = randomPrivateKeyFn ?? Secp256k1.randomPrivateKey
123
+ }
124
+
125
+ /**
126
+ * Registers an event listener for a specific event within this chain manager.
127
+ * @param event The event to listen for ChainSessionManagerEvent events.
128
+ * @param listener The function to call when the event occurs.
129
+ * @returns A function to unsubscribe the listener.
130
+ */
131
+ public on<K extends keyof ChainSessionManagerEventMap>(
132
+ event: K,
133
+ listener: ChainSessionManagerEventMap[K],
134
+ ): () => void {
135
+ if (!this.eventListeners[event]) {
136
+ this.eventListeners[event] = new Set() as any
137
+ }
138
+ ;(this.eventListeners[event] as any).add(listener)
139
+ return () => {
140
+ ;(this.eventListeners[event] as any)?.delete(listener)
141
+ }
142
+ }
143
+
144
+ /**
145
+ * @private Emits an event to all registered listeners for this chain manager.
146
+ * @param event The event to emit.
147
+ * @param data The data to pass to the listener.
148
+ */
149
+ private emit<K extends keyof ChainSessionManagerEventMap>(
150
+ event: K,
151
+ data: Parameters<ChainSessionManagerEventMap[K]>[0],
152
+ ): void {
153
+ const listeners = this.eventListeners[event]
154
+ if (listeners) {
155
+ listeners.forEach((listener) => (listener as (d: typeof data) => void)(data))
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Initializes the manager by loading sessions from storage for this specific chain.
161
+ * @returns A promise resolving to the login method and email if an implicit session is found, or void.
162
+ * @throws {InitializationError} If initialization fails.
163
+ */
164
+ async initialize(): Promise<{
165
+ loginMethod: string | null
166
+ userEmail: string | null
167
+ } | void> {
168
+ if (this.isInitializing) return
169
+ this.isInitializing = true
170
+
171
+ this._resetState()
172
+
173
+ try {
174
+ const implicitSession = await this.sequenceStorage.getImplicitSession()
175
+ const explicitSessions = await this.sequenceStorage.getExplicitSessions()
176
+ const walletAddress = implicitSession?.walletAddress || explicitSessions[0]?.walletAddress
177
+
178
+ if (walletAddress) {
179
+ this.walletAddress = walletAddress
180
+ this.loginMethod = implicitSession?.loginMethod || explicitSessions[0]?.loginMethod || null
181
+ this.userEmail = implicitSession?.userEmail || explicitSessions[0]?.userEmail || null
182
+ await this._loadSessionFromStorage(walletAddress)
183
+ }
184
+ } catch (err) {
185
+ await this._resetStateAndClearCredentials()
186
+ throw new InitializationError(`Initialization failed: ${err}`)
187
+ } finally {
188
+ this.isInitializing = false
189
+ this.isInitialized = !!this.walletAddress
190
+ }
191
+ return { loginMethod: this.loginMethod, userEmail: this.userEmail }
192
+ }
193
+
194
+ /**
195
+ * Initializes the manager with a known wallet address, without loading sessions from storage.
196
+ * This is used when a wallet address is known but the session manager for this chain hasn't been instantiated yet.
197
+ * @param walletAddress The address of the wallet to initialize with.
198
+ */
199
+ public initializeWithWallet(walletAddress: Address.Address) {
200
+ if (this.isInitialized) return
201
+
202
+ this.walletAddress = walletAddress
203
+ this.wallet = new Wallet(this.walletAddress, {
204
+ stateProvider: this.stateProvider,
205
+ })
206
+ this.sessionManager = new Signers.SessionManager(this.wallet, {
207
+ sessionManagerAddress: Extensions.Dev1.sessions,
208
+ provider: this.provider!,
209
+ })
210
+ this.isInitialized = true
211
+ }
212
+
213
+ /**
214
+ * @private Loads implicit and explicit sessions from storage for the current wallet address.
215
+ * @param walletAddress The walletAddress for all sessions.
216
+ */
217
+ private async _loadSessionFromStorage(walletAddress: Address.Address) {
218
+ this.initializeWithWallet(walletAddress)
219
+
220
+ const implicitSession = await this.sequenceStorage.getImplicitSession()
221
+
222
+ if (implicitSession && implicitSession.chainId === this.chainId) {
223
+ await this._initializeImplicitSessionInternal(
224
+ implicitSession.pk,
225
+ walletAddress,
226
+ implicitSession.attestation,
227
+ implicitSession.identitySignature,
228
+ false,
229
+ implicitSession.loginMethod,
230
+ implicitSession.userEmail,
231
+ )
232
+ }
233
+
234
+ const allExplicitSessions = await this.sequenceStorage.getExplicitSessions()
235
+ const walletExplicitSessions = allExplicitSessions.filter(
236
+ (s) => Address.isEqual(Address.from(s.walletAddress), walletAddress) && s.chainId === this.chainId,
237
+ )
238
+
239
+ for (const sessionData of walletExplicitSessions) {
240
+ await this._initializeExplicitSessionInternal(
241
+ sessionData.pk,
242
+ sessionData.loginMethod,
243
+ sessionData.userEmail,
244
+ sessionData.guard,
245
+ true,
246
+ )
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Initiates the creation of a new session by sending a request to the wallet.
252
+ * @param permissions (Optional) Permissions for an initial explicit session.
253
+ * @param options (Optional) Additional options like preferred login method.
254
+ * @throws {InitializationError} If a session already exists or the transport fails to initialize.
255
+ */
256
+ async createNewSession(
257
+ origin: string,
258
+ permissions?: Signers.Session.ExplicitParams,
259
+ options: {
260
+ preferredLoginMethod?: LoginMethod
261
+ email?: string
262
+ includeImplicitSession?: boolean
263
+ } = {},
264
+ ): Promise<void> {
265
+ if (this.isInitialized) {
266
+ throw new InitializationError('A session already exists. Disconnect first.')
267
+ }
268
+
269
+ const newPk = await this.randomPrivateKeyFn()
270
+ const newSignerAddress = Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey: newPk }))
271
+
272
+ try {
273
+ if (!this.transport) throw new InitializationError('Transport failed to initialize.')
274
+
275
+ const payload: CreateNewSessionPayload = {
276
+ sessionAddress: newSignerAddress,
277
+ origin,
278
+ permissions,
279
+ includeImplicitSession: options.includeImplicitSession ?? false,
280
+ preferredLoginMethod: options.preferredLoginMethod,
281
+ email: options.preferredLoginMethod === 'email' ? options.email : undefined,
282
+ }
283
+
284
+ if (this.transport.mode === TransportMode.REDIRECT) {
285
+ await this.sequenceStorage.saveTempSessionPk(newPk)
286
+ await this.sequenceStorage.savePendingRequest({
287
+ chainId: this.chainId,
288
+ action: RequestActionType.CREATE_NEW_SESSION,
289
+ payload,
290
+ })
291
+ await this.sequenceStorage.setPendingRedirectRequest(true)
292
+ }
293
+
294
+ const connectResponse = await this.transport.sendRequest<ConnectSuccessResponsePayload>(
295
+ RequestActionType.CREATE_NEW_SESSION,
296
+ this.redirectUrl,
297
+ payload,
298
+ { path: '/request/connect' },
299
+ )
300
+
301
+ const receivedAddress = Address.from(connectResponse.walletAddress)
302
+ const { attestation, signature, userEmail, loginMethod, guard } = connectResponse
303
+
304
+ if (attestation && signature) {
305
+ await this._resetStateAndClearCredentials()
306
+
307
+ this.initializeWithWallet(receivedAddress)
308
+
309
+ await this._initializeImplicitSessionInternal(
310
+ newPk,
311
+ receivedAddress,
312
+ attestation,
313
+ signature,
314
+ true,
315
+ loginMethod,
316
+ userEmail,
317
+ guard,
318
+ )
319
+ }
320
+
321
+ if (permissions) {
322
+ this.initializeWithWallet(receivedAddress)
323
+ await this._initializeExplicitSessionInternal(newPk, loginMethod, userEmail, guard, true)
324
+ await this.sequenceStorage.saveExplicitSession({
325
+ pk: newPk,
326
+ walletAddress: receivedAddress,
327
+ chainId: this.chainId,
328
+ guard,
329
+ loginMethod,
330
+ userEmail,
331
+ })
332
+ }
333
+
334
+ if (this.transport.mode === TransportMode.POPUP) {
335
+ this.transport.closeWallet()
336
+ }
337
+ } catch (err) {
338
+ this._resetState()
339
+ if (this.transport?.mode === TransportMode.POPUP) this.transport.closeWallet()
340
+ throw new InitializationError(`Session creation failed: ${err}`)
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Initiates the addition of a new explicit session by sending a request to the wallet.
346
+ * @param permissions The permissions for the new explicit session.
347
+ * @throws {InitializationError} If the manager is not initialized.
348
+ * @throws {AddExplicitSessionError} If adding the session fails.
349
+ */
350
+ async addExplicitSession(permissions: Signers.Session.ExplicitParams): Promise<void> {
351
+ if (!this.walletAddress) {
352
+ throw new InitializationError(
353
+ 'Cannot add an explicit session without a wallet address. Initialize the manager with a wallet address first.',
354
+ )
355
+ }
356
+
357
+ const newPk = await this.randomPrivateKeyFn()
358
+
359
+ try {
360
+ if (!this.transport) throw new InitializationError('Transport failed to initialize.')
361
+
362
+ const newSignerAddress = Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey: newPk }))
363
+
364
+ const payload: AddExplicitSessionPayload = {
365
+ sessionAddress: newSignerAddress,
366
+ permissions,
367
+ }
368
+
369
+ if (this.transport.mode === TransportMode.REDIRECT) {
370
+ await this.sequenceStorage.saveTempSessionPk(newPk)
371
+ await this.sequenceStorage.savePendingRequest({
372
+ chainId: this.chainId,
373
+ action: RequestActionType.ADD_EXPLICIT_SESSION,
374
+ payload,
375
+ })
376
+ await this.sequenceStorage.setPendingRedirectRequest(true)
377
+ }
378
+
379
+ const response = await this.transport.sendRequest<ConnectSuccessResponsePayload>(
380
+ RequestActionType.ADD_EXPLICIT_SESSION,
381
+ this.redirectUrl,
382
+ payload,
383
+ { path: '/request/connect' },
384
+ )
385
+
386
+ if (!Address.isEqual(Address.from(response.walletAddress), this.walletAddress)) {
387
+ throw new AddExplicitSessionError('Wallet address mismatch.')
388
+ }
389
+
390
+ if (this.transport?.mode === TransportMode.POPUP) {
391
+ this.transport?.closeWallet()
392
+ }
393
+
394
+ await this._initializeExplicitSessionInternal(
395
+ newPk,
396
+ response.loginMethod,
397
+ response.userEmail,
398
+ response.guard,
399
+ true,
400
+ )
401
+ await this.sequenceStorage.saveExplicitSession({
402
+ pk: newPk,
403
+ walletAddress: this.walletAddress,
404
+ chainId: this.chainId,
405
+ loginMethod: response.loginMethod,
406
+ userEmail: response.userEmail,
407
+ guard: response.guard,
408
+ })
409
+ } catch (err) {
410
+ if (this.transport?.mode === TransportMode.POPUP) this.transport.closeWallet()
411
+ throw new AddExplicitSessionError(`Adding explicit session failed: ${err}`)
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Initiates the modification of an existing explicit session by sending a request to the wallet.
417
+ * @param sessionAddress The address of the explicit session to modify.
418
+ * @param newPermissions The new permissions for the session.
419
+ * @throws {InitializationError} If the manager is not initialized.
420
+ * @throws {ModifyExplicitSessionError} If modifying the session fails.
421
+ */
422
+ async modifyExplicitSession(
423
+ sessionAddress: Address.Address,
424
+ newPermissions: Signers.Session.ExplicitParams,
425
+ ): Promise<void> {
426
+ if (!this.walletAddress) {
427
+ throw new InitializationError(
428
+ 'Cannot modify an explicit session without a wallet address. Initialize the manager with a wallet address first.',
429
+ )
430
+ }
431
+
432
+ try {
433
+ if (!this.transport) throw new InitializationError('Transport failed to initialize.')
434
+
435
+ const session = this.sessions.find((s) => Address.isEqual(s.address, sessionAddress))
436
+ if (!session) {
437
+ throw new ModifyExplicitSessionError('Session not found.')
438
+ }
439
+
440
+ const payload: ModifySessionPayload = {
441
+ walletAddress: this.walletAddress,
442
+ sessionAddress: sessionAddress,
443
+ permissions: newPermissions,
444
+ }
445
+
446
+ if (this.transport.mode === TransportMode.REDIRECT) {
447
+ await this.sequenceStorage.savePendingRequest({
448
+ chainId: this.chainId,
449
+ action: RequestActionType.MODIFY_EXPLICIT_SESSION,
450
+ payload,
451
+ })
452
+ await this.sequenceStorage.setPendingRedirectRequest(true)
453
+ }
454
+
455
+ const response = await this.transport.sendRequest<ModifySessionSuccessResponsePayload>(
456
+ RequestActionType.MODIFY_EXPLICIT_SESSION,
457
+ this.redirectUrl,
458
+ payload,
459
+ { path: '/request/modify' },
460
+ )
461
+
462
+ if (
463
+ !Address.isEqual(Address.from(response.walletAddress), this.walletAddress) &&
464
+ !Address.isEqual(Address.from(response.sessionAddress), sessionAddress)
465
+ ) {
466
+ throw new ModifyExplicitSessionError('Wallet or session address mismatch.')
467
+ }
468
+
469
+ session.permissions = newPermissions
470
+
471
+ if (this.transport?.mode === TransportMode.POPUP) {
472
+ this.transport?.closeWallet()
473
+ }
474
+ } catch (err) {
475
+ if (this.transport?.mode === TransportMode.POPUP) this.transport.closeWallet()
476
+ throw new ModifyExplicitSessionError(`Modifying explicit session failed: ${err}`)
477
+ }
478
+ }
479
+
480
+ /**
481
+ * @private Handles the connection-related part of a redirect response, initializing sessions.
482
+ * @param response The response payload from the redirect.
483
+ * @returns A promise resolving to true on success.
484
+ */
485
+ private async _handleRedirectConnectionResponse(response: {
486
+ payload: ConnectSuccessResponsePayload
487
+ action: string
488
+ }): Promise<boolean> {
489
+ const tempPk = await this.sequenceStorage.getAndClearTempSessionPk()
490
+ if (!tempPk) {
491
+ throw new InitializationError('Failed to retrieve temporary session key after redirect.')
492
+ }
493
+
494
+ try {
495
+ const connectResponse = response.payload
496
+ const receivedAddress = Address.from(connectResponse.walletAddress)
497
+ const { userEmail, loginMethod, guard } = connectResponse
498
+
499
+ if (response.action === RequestActionType.CREATE_NEW_SESSION) {
500
+ const { attestation, signature } = connectResponse
501
+
502
+ const savedRequest = await this.sequenceStorage.peekPendingRequest()
503
+ const savedPayload = savedRequest?.payload as CreateNewSessionPayload | undefined
504
+ await this._resetStateAndClearCredentials()
505
+
506
+ this.initializeWithWallet(receivedAddress)
507
+
508
+ if (attestation && signature) {
509
+ await this._initializeImplicitSessionInternal(
510
+ tempPk,
511
+ receivedAddress,
512
+ attestation,
513
+ signature,
514
+ true,
515
+ loginMethod,
516
+ userEmail,
517
+ guard,
518
+ )
519
+ }
520
+
521
+ if (savedRequest && savedPayload && savedPayload.permissions) {
522
+ await this._initializeExplicitSessionInternal(tempPk, loginMethod, userEmail, guard, true)
523
+ await this.sequenceStorage.saveExplicitSession({
524
+ pk: tempPk,
525
+ walletAddress: receivedAddress,
526
+ chainId: this.chainId,
527
+ loginMethod,
528
+ userEmail,
529
+ guard,
530
+ })
531
+ }
532
+ } else if (response.action === RequestActionType.ADD_EXPLICIT_SESSION) {
533
+ if (!this.walletAddress || !Address.isEqual(receivedAddress, this.walletAddress)) {
534
+ throw new InitializationError('Received an explicit session for a wallet that is not active.')
535
+ }
536
+
537
+ await this._initializeExplicitSessionInternal(
538
+ tempPk,
539
+ this.loginMethod ?? undefined,
540
+ this.userEmail ?? undefined,
541
+ this.guard ?? undefined,
542
+ true,
543
+ )
544
+ await this.sequenceStorage.saveExplicitSession({
545
+ pk: tempPk,
546
+ walletAddress: receivedAddress,
547
+ chainId: this.chainId,
548
+ loginMethod: this.loginMethod ?? undefined,
549
+ userEmail: this.userEmail ?? undefined,
550
+ guard: this.guard ?? undefined,
551
+ })
552
+
553
+ const newSignerAddress = Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey: tempPk }))
554
+
555
+ this.emit('explicitSessionResponse', {
556
+ action: RequestActionType.ADD_EXPLICIT_SESSION,
557
+ response: {
558
+ walletAddress: receivedAddress,
559
+ sessionAddress: newSignerAddress,
560
+ },
561
+ })
562
+ }
563
+ this.isInitialized = true
564
+ return true
565
+ } catch (err) {
566
+ throw new InitializationError(`Failed to initialize session from redirect: ${err}`)
567
+ }
568
+ }
569
+
570
+ /**
571
+ * Resets the manager state and clears all credentials from storage.
572
+ */
573
+ async disconnect(): Promise<void> {
574
+ await this._resetStateAndClearCredentials()
575
+ if (this.transport) {
576
+ this.transport.destroy()
577
+ this.transport = null
578
+ }
579
+ this.loginMethod = null
580
+ this.userEmail = null
581
+ this.isInitialized = false
582
+ }
583
+
584
+ /**
585
+ * @private Initializes an implicit session signer and adds it to the session manager.
586
+ * @param pk The private key of the session.
587
+ * @param address The wallet address.
588
+ * @param attestation The attestation from the wallet.
589
+ * @param identitySignature The identity signature from the wallet.
590
+ * @param saveSession Whether to persist the session in storage.
591
+ * @param loginMethod The login method used.
592
+ * @param userEmail The email associated with the session.
593
+ * @param guard The guard configuration.
594
+ */
595
+ private async _initializeImplicitSessionInternal(
596
+ pk: Hex.Hex,
597
+ address: Address.Address,
598
+ attestation: Attestation.Attestation,
599
+ identitySignature: Hex.Hex,
600
+ saveSession: boolean = false,
601
+ loginMethod?: LoginMethod,
602
+ userEmail?: string,
603
+ guard?: GuardConfig,
604
+ ): Promise<void> {
605
+ if (!this.sessionManager) throw new InitializationError('Manager not instantiated for implicit session.')
606
+ try {
607
+ const implicitSigner = new Signers.Session.Implicit(
608
+ pk,
609
+ attestation,
610
+ identitySignature,
611
+ this.sessionManager.address,
612
+ )
613
+ this.sessionManager = this.sessionManager.withImplicitSigner(implicitSigner)
614
+
615
+ this.sessions.push({
616
+ address: implicitSigner.address,
617
+ isImplicit: true,
618
+ })
619
+
620
+ this.walletAddress = address
621
+ if (saveSession)
622
+ await this.sequenceStorage.saveImplicitSession({
623
+ pk,
624
+ walletAddress: address,
625
+ attestation,
626
+ identitySignature,
627
+ chainId: this.chainId,
628
+ loginMethod,
629
+ userEmail,
630
+ guard,
631
+ })
632
+ if (loginMethod) this.loginMethod = loginMethod
633
+ if (userEmail) this.userEmail = userEmail
634
+ if (guard) this.guard = guard
635
+ } catch (err) {
636
+ throw new InitializationError(`Implicit session init failed: ${err}`)
637
+ }
638
+ }
639
+
640
+ /**
641
+ * @private Initializes an explicit session signer and adds it to the session manager.
642
+ * It retries fetching permissions from the network if allowed.
643
+ * @param pk The private key of the session.
644
+ * @param loginMethod The login method used for the session.
645
+ * @param userEmail The email associated with the session.
646
+ * @param allowRetries Whether to retry fetching permissions on failure.
647
+ */
648
+ private async _initializeExplicitSessionInternal(
649
+ pk: Hex.Hex,
650
+ loginMethod?: LoginMethod,
651
+ userEmail?: string,
652
+ guard?: GuardConfig,
653
+ allowRetries: boolean = false,
654
+ ): Promise<void> {
655
+ if (!this.provider || !this.wallet)
656
+ throw new InitializationError('Manager core components not ready for explicit session.')
657
+
658
+ const maxRetries = allowRetries ? 3 : 1
659
+ let lastError: Error | null = null
660
+
661
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
662
+ try {
663
+ const tempManager = new Signers.SessionManager(this.wallet, {
664
+ sessionManagerAddress: Extensions.Dev1.sessions,
665
+ provider: this.provider,
666
+ })
667
+ const topology = await tempManager.getTopology()
668
+
669
+ const signerAddress = Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey: pk }))
670
+ const permissions = SessionConfig.getSessionPermissions(topology, signerAddress)
671
+
672
+ if (!permissions) {
673
+ throw new InitializationError(`Permissions not found for session key.`)
674
+ }
675
+
676
+ if (!this.sessionManager) throw new InitializationError('Main session manager is not initialized.')
677
+
678
+ const explicitSigner = new Signers.Session.Explicit(pk, permissions)
679
+ this.sessionManager = this.sessionManager.withExplicitSigner(explicitSigner)
680
+
681
+ this.sessions.push({
682
+ address: explicitSigner.address,
683
+ isImplicit: false,
684
+ chainId: this.chainId,
685
+ permissions,
686
+ })
687
+
688
+ if (guard && !this.guard) this.guard = guard
689
+
690
+ return
691
+ } catch (err) {
692
+ lastError = err instanceof Error ? err : new Error(String(err))
693
+ if (attempt < maxRetries) {
694
+ await new Promise((resolve) => setTimeout(resolve, 1000 * attempt))
695
+ }
696
+ }
697
+ }
698
+ if (lastError)
699
+ throw new InitializationError(`Explicit session init failed after ${maxRetries} attempts: ${lastError.message}`)
700
+ }
701
+
702
+ /**
703
+ * Checks if the current session has permission to execute a set of transactions.
704
+ * @param transactions The transactions to check permissions for.
705
+ * @returns A promise that resolves to true if the session has permission, false otherwise.
706
+ */
707
+ async hasPermission(transactions: Transaction[]): Promise<boolean> {
708
+ if (!this.wallet || !this.sessionManager || !this.provider || !this.isInitialized) {
709
+ return false
710
+ }
711
+
712
+ try {
713
+ const calls: Payload.Call[] = transactions.map((tx) => ({
714
+ to: tx.to,
715
+ value: tx.value,
716
+ data: tx.data,
717
+ gasLimit: tx.gasLimit ?? BigInt(0),
718
+ delegateCall: tx.delegateCall ?? false,
719
+ onlyFallback: tx.onlyFallback ?? false,
720
+ behaviorOnError: tx.behaviorOnError ?? ('revert' as const),
721
+ }))
722
+
723
+ // Attempt to prepare and sign the transaction. If this succeeds, the session
724
+ // has the necessary permissions. We don't relay the transaction.
725
+ await this._buildAndSignCalls(calls)
726
+ return true
727
+ } catch (error) {
728
+ // If building or signing fails, it indicates a lack of permissions or another issue.
729
+ // For the purpose of this check, we treat it as a permission failure.
730
+ console.warn(
731
+ `Permission check failed for chain ${this.chainId}:`,
732
+ error instanceof Error ? error.message : String(error),
733
+ )
734
+ return false
735
+ }
736
+ }
737
+
738
+ /**
739
+ * Fetches fee options for a set of transactions.
740
+ * @param calls The transactions to estimate fees for.
741
+ * @returns A promise that resolves with an array of fee options.
742
+ * @throws {FeeOptionError} If fetching fee options fails.
743
+ */
744
+ async getFeeOptions(calls: Transaction[]): Promise<Relayer.FeeOption[]> {
745
+ const callsToSend = calls.map((tx) => ({
746
+ to: tx.to,
747
+ value: tx.value,
748
+ data: tx.data,
749
+ gasLimit: tx.gasLimit ?? BigInt(0),
750
+ delegateCall: tx.delegateCall ?? false,
751
+ onlyFallback: tx.onlyFallback ?? false,
752
+ behaviorOnError: tx.behaviorOnError ?? ('revert' as const),
753
+ }))
754
+ try {
755
+ const signedCall = await this._buildAndSignCalls(callsToSend)
756
+ const feeOptions = await this.relayer.feeOptions(signedCall.to, this.chainId, callsToSend)
757
+ return feeOptions.options
758
+ } catch (err) {
759
+ throw new FeeOptionError(`Failed to get fee options: ${err instanceof Error ? err.message : String(err)}`)
760
+ }
761
+ }
762
+
763
+ /**
764
+ * Builds, signs, and sends a batch of transactions.
765
+ * @param transactions The transactions to be sent.
766
+ * @param feeOption (Optional) The fee option to use for sponsoring the transaction. If provided, a token transfer call will be prepended.
767
+ * @returns A promise that resolves with the transaction hash.
768
+ * @throws {InitializationError} If the session is not initialized.
769
+ * @throws {TransactionError} If the transaction fails at any stage.
770
+ */
771
+ async buildSignAndSendTransactions(transactions: Transaction[], feeOption?: Relayer.FeeOption): Promise<Hex.Hex> {
772
+ if (!this.wallet || !this.sessionManager || !this.provider || !this.isInitialized)
773
+ throw new InitializationError('Session is not initialized.')
774
+ try {
775
+ const calls: Payload.Call[] = transactions.map((tx) => ({
776
+ to: tx.to,
777
+ value: tx.value,
778
+ data: tx.data,
779
+ gasLimit: tx.gasLimit ?? BigInt(0),
780
+ delegateCall: tx.delegateCall ?? false,
781
+ onlyFallback: tx.onlyFallback ?? false,
782
+ behaviorOnError: tx.behaviorOnError ?? ('revert' as const),
783
+ }))
784
+
785
+ const callsToSend = calls
786
+ if (feeOption) {
787
+ if (feeOption.token.contractAddress === Constants.ZeroAddress) {
788
+ const forwardValue = AbiFunction.from(['function forwardValue(address to, uint256 value)'])
789
+ callsToSend.unshift({
790
+ to: VALUE_FORWARDER_ADDRESS,
791
+ value: BigInt(feeOption.value),
792
+ data: AbiFunction.encodeData(forwardValue, [feeOption.to as Address.Address, BigInt(feeOption.value)]),
793
+ gasLimit: BigInt(feeOption.gasLimit),
794
+ delegateCall: false,
795
+ onlyFallback: false,
796
+ behaviorOnError: 'revert' as const,
797
+ })
798
+ } else {
799
+ const transfer = AbiFunction.from(['function transfer(address to, uint256 value)'])
800
+ const transferCall: Payload.Call = {
801
+ to: feeOption.token.contractAddress as `0x${string}`,
802
+ value: BigInt(0),
803
+ data: AbiFunction.encodeData(transfer, [feeOption.to as Address.Address, BigInt(feeOption.value)]),
804
+ gasLimit: BigInt(feeOption.gasLimit),
805
+ delegateCall: false,
806
+ onlyFallback: false,
807
+ behaviorOnError: 'revert' as const,
808
+ }
809
+ callsToSend.unshift(transferCall)
810
+ }
811
+ }
812
+ const signedCalls = await this._buildAndSignCalls(callsToSend)
813
+ const hash = await this.relayer.relay(signedCalls.to, signedCalls.data, this.chainId)
814
+ const status = await this._waitForTransactionReceipt(hash.opHash, this.chainId)
815
+ if (status.status === 'confirmed') {
816
+ return status.transactionHash
817
+ } else {
818
+ const failedStatus = status as Relayer.OperationFailedStatus
819
+ const reason = failedStatus.reason || `unexpected status ${status.status}`
820
+ throw new TransactionError(`Transaction failed: ${reason}`)
821
+ }
822
+ } catch (err) {
823
+ throw new TransactionError(`Transaction failed: ${err instanceof Error ? err.message : String(err)}`)
824
+ }
825
+ }
826
+
827
+ /**
828
+ * Handles a redirect response from the wallet for this specific chain.
829
+ * @param response The pre-parsed response from the transport.
830
+ * @returns A promise that resolves to true if the response was handled successfully.
831
+ * @throws {WalletRedirectError} If the response is invalid or causes an error.
832
+ * @throws {InitializationError} If the session cannot be initialized from the response.
833
+ */
834
+ public async handleRedirectResponse(
835
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
836
+ response: { payload: any; action: string } | { error: any; action: string },
837
+ ): Promise<boolean> {
838
+ if (!response) return false
839
+
840
+ if ('error' in response && response.error) {
841
+ const { action } = response
842
+
843
+ if (action === RequestActionType.ADD_EXPLICIT_SESSION || action === RequestActionType.MODIFY_EXPLICIT_SESSION) {
844
+ this.emit('explicitSessionResponse', { action, error: response.error })
845
+ return true
846
+ }
847
+ }
848
+
849
+ if ('payload' in response && response.payload) {
850
+ if (
851
+ response.action === RequestActionType.CREATE_NEW_SESSION ||
852
+ response.action === RequestActionType.ADD_EXPLICIT_SESSION
853
+ ) {
854
+ return this._handleRedirectConnectionResponse(response)
855
+ } else if (response.action === RequestActionType.MODIFY_EXPLICIT_SESSION) {
856
+ const modifyResponse = response.payload as ModifySessionSuccessResponsePayload
857
+ if (!Address.isEqual(Address.from(modifyResponse.walletAddress), this.walletAddress!)) {
858
+ throw new ModifyExplicitSessionError('Wallet address mismatch on redirect response.')
859
+ }
860
+
861
+ this.emit('explicitSessionResponse', {
862
+ action: RequestActionType.MODIFY_EXPLICIT_SESSION,
863
+ response: modifyResponse,
864
+ })
865
+
866
+ return true
867
+ } else {
868
+ throw new WalletRedirectError(`Received unhandled redirect action: ${response.action}`)
869
+ }
870
+ }
871
+
872
+ throw new WalletRedirectError('Received an invalid redirect response from the wallet.')
873
+ }
874
+
875
+ /**
876
+ * Gets the wallet address associated with this manager.
877
+ * @returns The wallet address, or null if not initialized.
878
+ */
879
+ getWalletAddress(): Address.Address | null {
880
+ return this.walletAddress
881
+ }
882
+
883
+ /**
884
+ * Gets the sessions (signers) managed by this instance.
885
+ * @returns An array of session objects.
886
+ */
887
+ getSessions(): Session[] {
888
+ return this.sessions
889
+ }
890
+
891
+ /**
892
+ * @private Prepares, signs, and builds a transaction envelope.
893
+ * @param calls The payload calls to include in the transaction.
894
+ * @returns The signed transaction data ready for relaying.
895
+ */
896
+ private async _buildAndSignCalls(calls: Payload.Call[]): Promise<{ to: Address.Address; data: Hex.Hex }> {
897
+ if (!this.wallet || !this.sessionManager || !this.provider)
898
+ throw new InitializationError('Session not fully initialized.')
899
+
900
+ try {
901
+ const preparedIncrement = await this.sessionManager.prepareIncrement(this.wallet.address, this.chainId, calls)
902
+ if (preparedIncrement) calls.push(preparedIncrement)
903
+
904
+ const envelope = await this.wallet.prepareTransaction(this.provider, calls, {
905
+ noConfigUpdate: true,
906
+ })
907
+ const parentedEnvelope: Payload.Parented = {
908
+ ...envelope.payload,
909
+ parentWallets: [this.wallet.address],
910
+ }
911
+ const imageHash = await this.sessionManager.imageHash
912
+ if (imageHash === undefined) throw new SessionConfigError('Session manager image hash is undefined')
913
+
914
+ const signature = await this.sessionManager.signSapient(
915
+ this.wallet.address,
916
+ this.chainId,
917
+ parentedEnvelope,
918
+ imageHash,
919
+ )
920
+ const sapientSignature: Envelope.SapientSignature = {
921
+ imageHash,
922
+ signature,
923
+ }
924
+ const signedEnvelope = Envelope.toSigned(envelope, [sapientSignature])
925
+
926
+ if (this.guard && !Envelope.reachedThreshold(signedEnvelope)) {
927
+ // TODO: this might fail if 2FA is required
928
+ const guard = new Signers.Guard(new Guard.Sequence.Guard(this.guard.url, this.guard.address))
929
+ const guardSignature = await guard.signEnvelope(signedEnvelope)
930
+ signedEnvelope.signatures.push(guardSignature)
931
+ }
932
+
933
+ return await this.wallet.buildTransaction(this.provider, signedEnvelope)
934
+ } catch (err) {
935
+ throw new TransactionError(`Transaction failed building: ${err instanceof Error ? err.message : String(err)}`)
936
+ }
937
+ }
938
+
939
+ /**
940
+ * @private Polls the relayer for the status of a transaction until it is confirmed or fails.
941
+ * @param opHash The operation hash of the relayed transaction.
942
+ * @param chainId The chain ID of the transaction.
943
+ * @returns The final status of the transaction.
944
+ */
945
+ private async _waitForTransactionReceipt(opHash: `0x${string}`, chainId: number): Promise<Relayer.OperationStatus> {
946
+ try {
947
+ while (true) {
948
+ const currentStatus = await this.relayer.status(opHash, chainId)
949
+ if (currentStatus.status === 'confirmed' || currentStatus.status === 'failed') return currentStatus
950
+ await new Promise((resolve) => setTimeout(resolve, 1500))
951
+ }
952
+ } catch (err) {
953
+ throw new TransactionError(
954
+ `Transaction failed waiting for receipt: ${err instanceof Error ? err.message : String(err)}`,
955
+ )
956
+ }
957
+ }
958
+
959
+ /**
960
+ * @private Resets the internal state of the manager without clearing stored credentials.
961
+ */
962
+ private _resetState(): void {
963
+ this.sessions = []
964
+ this.walletAddress = null
965
+ this.wallet = null
966
+ this.sessionManager = null
967
+ this.isInitialized = false
968
+ }
969
+
970
+ /**
971
+ * @private Resets the internal state and clears all persisted session data from storage.
972
+ */
973
+ private async _resetStateAndClearCredentials(): Promise<void> {
974
+ this._resetState()
975
+ await this.sequenceStorage.clearImplicitSession()
976
+ await this.sequenceStorage.clearExplicitSessions()
977
+ }
978
+ }