@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.
- package/.turbo/turbo-build.log +5 -0
- package/CHANGELOG.md +11 -0
- package/LICENSE +202 -0
- package/README.md +238 -0
- package/dist/ChainSessionManager.d.ts +203 -0
- package/dist/ChainSessionManager.d.ts.map +1 -0
- package/dist/ChainSessionManager.js +742 -0
- package/dist/DappClient.d.ts +409 -0
- package/dist/DappClient.d.ts.map +1 -0
- package/dist/DappClient.js +667 -0
- package/dist/DappTransport.d.ts +47 -0
- package/dist/DappTransport.d.ts.map +1 -0
- package/dist/DappTransport.js +443 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/types/index.d.ts +168 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +25 -0
- package/dist/utils/constants.d.ts +6 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +5 -0
- package/dist/utils/errors.d.ts +28 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +54 -0
- package/dist/utils/index.d.ts +11 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +135 -0
- package/dist/utils/storage.d.ts +64 -0
- package/dist/utils/storage.d.ts.map +1 -0
- package/dist/utils/storage.js +196 -0
- package/eslint.config.mjs +4 -0
- package/package.json +38 -0
- package/src/ChainSessionManager.ts +978 -0
- package/src/DappClient.ts +801 -0
- package/src/DappTransport.ts +518 -0
- package/src/index.ts +47 -0
- package/src/types/index.ts +218 -0
- package/src/utils/constants.ts +5 -0
- package/src/utils/errors.ts +62 -0
- package/src/utils/index.ts +158 -0
- package/src/utils/storage.ts +272 -0
- 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
|
+
}
|