@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,801 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { Relayer, Signers } from '@0xsequence/wallet-core'
|
|
3
|
+
import { Address, Hex } from 'ox'
|
|
4
|
+
|
|
5
|
+
import { ChainSessionManager } from './ChainSessionManager.js'
|
|
6
|
+
import { DappTransport } from './DappTransport.js'
|
|
7
|
+
import { InitializationError, SigningError, TransactionError } from './utils/errors.js'
|
|
8
|
+
import { SequenceStorage, WebStorage } from './utils/storage.js'
|
|
9
|
+
import {
|
|
10
|
+
DappClientExplicitSessionEventListener,
|
|
11
|
+
DappClientWalletActionEventListener,
|
|
12
|
+
GuardConfig,
|
|
13
|
+
LoginMethod,
|
|
14
|
+
RandomPrivateKeyFn,
|
|
15
|
+
RequestActionType,
|
|
16
|
+
SendWalletTransactionPayload,
|
|
17
|
+
SequenceSessionStorage,
|
|
18
|
+
Session,
|
|
19
|
+
SignMessagePayload,
|
|
20
|
+
SignTypedDataPayload,
|
|
21
|
+
Transaction,
|
|
22
|
+
TransactionRequest,
|
|
23
|
+
TransportMode,
|
|
24
|
+
WalletActionResponse,
|
|
25
|
+
} from './types/index.js'
|
|
26
|
+
import { TypedData } from 'ox/TypedData'
|
|
27
|
+
import { KEYMACHINE_URL, NODES_URL, RELAYER_URL } from './utils/constants.js'
|
|
28
|
+
|
|
29
|
+
export type DappClientEventListener = (data?: any) => void
|
|
30
|
+
|
|
31
|
+
interface DappClientEventMap {
|
|
32
|
+
sessionsUpdated: () => void
|
|
33
|
+
walletActionResponse: DappClientWalletActionEventListener
|
|
34
|
+
explicitSessionResponse: DappClientExplicitSessionEventListener
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The main entry point for interacting with the Wallet.
|
|
39
|
+
* This client manages user sessions across multiple chains, handles connection
|
|
40
|
+
* and disconnection, and provides methods for signing and sending transactions.
|
|
41
|
+
*
|
|
42
|
+
* @see {@link https://docs.sequence.xyz/sdk/typescript/v3/dapp-client} for more detailed documentation.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // It is recommended to manage a singleton instance of this client.
|
|
46
|
+
* const dappClient = new DappClient('http://localhost:5173');
|
|
47
|
+
*
|
|
48
|
+
* async function main() {
|
|
49
|
+
* // Initialize the client on page load to restore existing sessions.
|
|
50
|
+
* await dappClient.initialize();
|
|
51
|
+
*
|
|
52
|
+
* // If not connected, prompt the user to connect.
|
|
53
|
+
* if (!dappClient.isInitialized) {
|
|
54
|
+
* await client.connect(137, window.location.origin);
|
|
55
|
+
* }
|
|
56
|
+
* }
|
|
57
|
+
*/
|
|
58
|
+
export class DappClient {
|
|
59
|
+
public isInitialized = false
|
|
60
|
+
|
|
61
|
+
public loginMethod: string | null = null
|
|
62
|
+
public userEmail: string | null = null
|
|
63
|
+
public guard?: GuardConfig
|
|
64
|
+
|
|
65
|
+
public readonly origin: string
|
|
66
|
+
|
|
67
|
+
private chainSessionManagers: Map<number, ChainSessionManager> = new Map()
|
|
68
|
+
|
|
69
|
+
private walletUrl: string
|
|
70
|
+
private transport: DappTransport
|
|
71
|
+
private projectAccessKey: string
|
|
72
|
+
private nodesUrl: string
|
|
73
|
+
private relayerUrl: string
|
|
74
|
+
private keymachineUrl: string
|
|
75
|
+
private sequenceStorage: SequenceStorage
|
|
76
|
+
private redirectPath?: string
|
|
77
|
+
private sequenceSessionStorage?: SequenceSessionStorage
|
|
78
|
+
private randomPrivateKeyFn?: RandomPrivateKeyFn
|
|
79
|
+
private redirectActionHandler?: (url: string) => void
|
|
80
|
+
private canUseIndexedDb: boolean
|
|
81
|
+
|
|
82
|
+
private isInitializing = false
|
|
83
|
+
|
|
84
|
+
private walletAddress: Address.Address | null = null
|
|
85
|
+
private eventListeners: {
|
|
86
|
+
[K in keyof DappClientEventMap]?: Set<DappClientEventMap[K]>
|
|
87
|
+
} = {}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param walletUrl The URL of the Wallet Webapp.
|
|
91
|
+
* @param origin The origin of the dapp
|
|
92
|
+
* @param projectAccessKey Your project access key from sequence.build. Used for services like relayer and nodes.
|
|
93
|
+
* @param options Configuration options for the client.
|
|
94
|
+
* @param options.transportMode The communication mode to use with the wallet. Defaults to 'popup'.
|
|
95
|
+
* @param options.redirectPath The path to redirect back to after a redirect-based flow. Constructed with origin + redirectPath.
|
|
96
|
+
* @param options.nodesUrl The URL template for the nodes service. Use `{network}` as a placeholder for the network name. Defaults to the Sequence nodes ('https://nodes.sequence.app/{network}').
|
|
97
|
+
* @param options.relayerUrl The URL template for the relayer service. Use `{network}` as a placeholder for the network name. Defaults to the Sequence relayer ('https://dev-{network}-relayer.sequence.app').
|
|
98
|
+
* @param options.keymachineUrl The URL of the key management service.
|
|
99
|
+
* @param options.sequenceStorage The storage implementation for persistent session data. Defaults to WebStorage using IndexedDB.
|
|
100
|
+
* @param options.sequenceSessionStorage The storage implementation for temporary data (e.g., pending requests). Defaults to sessionStorage.
|
|
101
|
+
* @param options.randomPrivateKeyFn A function to generate random private keys for new sessions.
|
|
102
|
+
* @param options.redirectActionHandler A handler to manually control navigation for redirect flows.
|
|
103
|
+
* @param options.canUseIndexedDb A flag to enable or disable the use of IndexedDB for caching.
|
|
104
|
+
*/
|
|
105
|
+
constructor(
|
|
106
|
+
walletUrl: string,
|
|
107
|
+
origin: string,
|
|
108
|
+
projectAccessKey: string,
|
|
109
|
+
options?: {
|
|
110
|
+
transportMode?: TransportMode
|
|
111
|
+
redirectPath?: string
|
|
112
|
+
keymachineUrl?: string
|
|
113
|
+
nodesUrl?: string
|
|
114
|
+
relayerUrl?: string
|
|
115
|
+
sequenceStorage?: SequenceStorage
|
|
116
|
+
sequenceSessionStorage?: SequenceSessionStorage
|
|
117
|
+
randomPrivateKeyFn?: RandomPrivateKeyFn
|
|
118
|
+
redirectActionHandler?: (url: string) => void
|
|
119
|
+
canUseIndexedDb?: boolean
|
|
120
|
+
},
|
|
121
|
+
) {
|
|
122
|
+
const {
|
|
123
|
+
transportMode = TransportMode.POPUP,
|
|
124
|
+
keymachineUrl = KEYMACHINE_URL,
|
|
125
|
+
redirectPath,
|
|
126
|
+
sequenceStorage = new WebStorage(),
|
|
127
|
+
sequenceSessionStorage,
|
|
128
|
+
randomPrivateKeyFn,
|
|
129
|
+
redirectActionHandler,
|
|
130
|
+
canUseIndexedDb = true,
|
|
131
|
+
} = options || {}
|
|
132
|
+
|
|
133
|
+
this.transport = new DappTransport(
|
|
134
|
+
walletUrl,
|
|
135
|
+
transportMode,
|
|
136
|
+
undefined,
|
|
137
|
+
sequenceSessionStorage,
|
|
138
|
+
redirectActionHandler,
|
|
139
|
+
)
|
|
140
|
+
this.walletUrl = walletUrl
|
|
141
|
+
this.projectAccessKey = projectAccessKey
|
|
142
|
+
this.nodesUrl = options?.nodesUrl || NODES_URL
|
|
143
|
+
this.relayerUrl = options?.relayerUrl || RELAYER_URL
|
|
144
|
+
this.origin = origin
|
|
145
|
+
this.keymachineUrl = keymachineUrl
|
|
146
|
+
this.sequenceStorage = sequenceStorage
|
|
147
|
+
this.redirectPath = redirectPath
|
|
148
|
+
this.sequenceSessionStorage = sequenceSessionStorage
|
|
149
|
+
this.randomPrivateKeyFn = randomPrivateKeyFn
|
|
150
|
+
this.redirectActionHandler = redirectActionHandler
|
|
151
|
+
this.canUseIndexedDb = canUseIndexedDb
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @returns The transport mode of the client. {@link TransportMode}
|
|
156
|
+
*/
|
|
157
|
+
public get transportMode(): TransportMode {
|
|
158
|
+
return this.transport.mode
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Registers an event listener for a specific event.
|
|
163
|
+
* @param event The event to listen for.
|
|
164
|
+
* @param listener The listener to call when the event occurs.
|
|
165
|
+
* @returns A function to remove the listener.
|
|
166
|
+
*
|
|
167
|
+
* @see {@link https://docs.sequence.xyz/sdk/typescript/v3/dapp-client/on} for more detailed documentation.
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* useEffect(() => {
|
|
171
|
+
* const handleWalletAction = (response) => {
|
|
172
|
+
* console.log('Received wallet action response:', response);
|
|
173
|
+
* };
|
|
174
|
+
*
|
|
175
|
+
* const unsubscribe = dappClient.on("walletActionResponse", handleWalletAction);
|
|
176
|
+
*
|
|
177
|
+
* return () => unsubscribe();
|
|
178
|
+
* }, [dappClient]);
|
|
179
|
+
*/
|
|
180
|
+
public on<K extends keyof DappClientEventMap>(event: K, listener: DappClientEventMap[K]): () => void {
|
|
181
|
+
if (!this.eventListeners[event]) {
|
|
182
|
+
this.eventListeners[event] = new Set() as any
|
|
183
|
+
}
|
|
184
|
+
;(this.eventListeners[event] as any).add(listener)
|
|
185
|
+
return () => {
|
|
186
|
+
;(this.eventListeners[event] as any)?.delete(listener)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Retrieves the wallet address of the current session.
|
|
192
|
+
* @returns The wallet address of the current session, or null if not initialized. {@link Address.Address}
|
|
193
|
+
*
|
|
194
|
+
* @see {@link https://docs.sequence.xyz/sdk/typescript/v3/dapp-client/get-wallet-address} for more detailed documentation.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* const dappClient = new DappClient('http://localhost:5173');
|
|
198
|
+
* await dappClient.initialize();
|
|
199
|
+
*
|
|
200
|
+
* if (dappClient.isInitialized) {
|
|
201
|
+
* const walletAddress = dappClient.getWalletAddress();
|
|
202
|
+
* console.log('Wallet address:', walletAddress);
|
|
203
|
+
* }
|
|
204
|
+
*/
|
|
205
|
+
public getWalletAddress(): Address.Address | null {
|
|
206
|
+
return this.walletAddress
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Retrieves a list of all active sessions (signers) associated with the current wallet.
|
|
211
|
+
* @returns An array of all the active sessions. {@link { address: Address.Address, isImplicit: boolean }[]}
|
|
212
|
+
*
|
|
213
|
+
* @see {@link https://docs.sequence.xyz/sdk/typescript/v3/dapp-client/get-all-sessions} for more detailed documentation.
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* const dappClient = new DappClient('http://localhost:5173');
|
|
217
|
+
* await dappClient.initialize();
|
|
218
|
+
*
|
|
219
|
+
* if (dappClient.isInitialized) {
|
|
220
|
+
* const sessions = dappClient.getAllSessions();
|
|
221
|
+
* console.log('Sessions:', sessions);
|
|
222
|
+
* }
|
|
223
|
+
*/
|
|
224
|
+
public getAllSessions(): Session[] {
|
|
225
|
+
const allSessions = new Map<string, Session>()
|
|
226
|
+
Array.from(this.chainSessionManagers.values()).forEach((chainSessionManager) => {
|
|
227
|
+
chainSessionManager.getSessions().forEach((session) => {
|
|
228
|
+
const uniqueKey = `${session.address.toLowerCase()}-${session.isImplicit}`
|
|
229
|
+
if (!allSessions.has(uniqueKey)) {
|
|
230
|
+
allSessions.set(uniqueKey, session)
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
return Array.from(allSessions.values())
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* @private Loads the client's state from storage, initializing all chain managers
|
|
239
|
+
* for previously established sessions.
|
|
240
|
+
*/
|
|
241
|
+
private async _loadStateFromStorage(): Promise<void> {
|
|
242
|
+
const implicitSession = await this.sequenceStorage.getImplicitSession()
|
|
243
|
+
|
|
244
|
+
const explicitSessions = await this.sequenceStorage.getExplicitSessions()
|
|
245
|
+
const chainIdsToInitialize = new Set([
|
|
246
|
+
...(implicitSession?.chainId !== undefined ? [implicitSession.chainId] : []),
|
|
247
|
+
...explicitSessions.map((s) => s.chainId),
|
|
248
|
+
])
|
|
249
|
+
|
|
250
|
+
if (chainIdsToInitialize.size === 0) {
|
|
251
|
+
this.isInitialized = false
|
|
252
|
+
this.emit('sessionsUpdated')
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const initPromises = Array.from(chainIdsToInitialize).map((chainId) =>
|
|
257
|
+
this.getChainSessionManager(chainId).initialize(),
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
const result = await Promise.all(initPromises)
|
|
261
|
+
|
|
262
|
+
this.walletAddress = implicitSession?.walletAddress || explicitSessions[0]?.walletAddress || null
|
|
263
|
+
this.loginMethod = result[0]?.loginMethod || null
|
|
264
|
+
this.userEmail = result[0]?.userEmail || null
|
|
265
|
+
this.guard = implicitSession?.guard || explicitSessions.find((s) => !!s.guard)?.guard
|
|
266
|
+
|
|
267
|
+
this.isInitialized = true
|
|
268
|
+
this.emit('sessionsUpdated')
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Initializes the client by loading any existing session from storage and handling any pending redirect responses.
|
|
273
|
+
* This should be called once when your application loads.
|
|
274
|
+
*
|
|
275
|
+
* @remarks
|
|
276
|
+
* An `Implicit` session is a session that can interact only with specific, Dapp-defined contracts.
|
|
277
|
+
* An `Explicit` session is a session that can interact with any contract as long as the user has granted the necessary permissions.
|
|
278
|
+
*
|
|
279
|
+
* @throws If the initialization process fails. {@link InitializationError}
|
|
280
|
+
*
|
|
281
|
+
* @returns A promise that resolves when initialization is complete.
|
|
282
|
+
*
|
|
283
|
+
* @see {@link https://docs.sequence.xyz/sdk/typescript/v3/dapp-client/initialize} for more detailed documentation.
|
|
284
|
+
*
|
|
285
|
+
* @example
|
|
286
|
+
* const dappClient = new DappClient('http://localhost:5173');
|
|
287
|
+
* await dappClient.initialize();
|
|
288
|
+
*/
|
|
289
|
+
async initialize(): Promise<void> {
|
|
290
|
+
if (this.isInitializing) return
|
|
291
|
+
this.isInitializing = true
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
// First, load any existing session from storage. This is crucial so that
|
|
295
|
+
// when we process a redirect for an explicit session, we know the wallet address.
|
|
296
|
+
await this._loadStateFromStorage()
|
|
297
|
+
|
|
298
|
+
// Now, check if there's a response from a redirect flow.
|
|
299
|
+
if (await this.sequenceStorage.isRedirectRequestPending()) {
|
|
300
|
+
try {
|
|
301
|
+
// Attempt to handle any response from the wallet redirect.
|
|
302
|
+
await this.handleRedirectResponse()
|
|
303
|
+
} finally {
|
|
304
|
+
// We have to clear pending redirect data here as well in case we received an error from the wallet.
|
|
305
|
+
await this.sequenceStorage.setPendingRedirectRequest(false)
|
|
306
|
+
await this.sequenceStorage.getAndClearTempSessionPk()
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// After handling the redirect, the session state will have changed,
|
|
310
|
+
// so we must load it again.
|
|
311
|
+
await this._loadStateFromStorage()
|
|
312
|
+
}
|
|
313
|
+
} catch (e) {
|
|
314
|
+
await this.disconnect()
|
|
315
|
+
throw e
|
|
316
|
+
} finally {
|
|
317
|
+
this.isInitializing = false
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Handles the redirect response from the Wallet.
|
|
323
|
+
* This is called automatically on `initialize()` for web environments but can be called manually
|
|
324
|
+
* with a URL in environments like React Native.
|
|
325
|
+
* @param url The full redirect URL from the wallet. If not provided, it will be read from the browser's current location.
|
|
326
|
+
* @returns A promise that resolves when the redirect has been handled.
|
|
327
|
+
*/
|
|
328
|
+
public async handleRedirectResponse(url?: string): Promise<void> {
|
|
329
|
+
const pendingRequest = await this.sequenceStorage.peekPendingRequest()
|
|
330
|
+
|
|
331
|
+
const response = await this.transport.getRedirectResponse(true, url)
|
|
332
|
+
if (!response) {
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const { action } = response
|
|
337
|
+
const chainId = pendingRequest?.chainId
|
|
338
|
+
|
|
339
|
+
if (
|
|
340
|
+
action === RequestActionType.SIGN_MESSAGE ||
|
|
341
|
+
action === RequestActionType.SIGN_TYPED_DATA ||
|
|
342
|
+
action === RequestActionType.SEND_WALLET_TRANSACTION
|
|
343
|
+
) {
|
|
344
|
+
if (chainId === undefined) {
|
|
345
|
+
throw new InitializationError('Could not find a chainId for the pending signature request.')
|
|
346
|
+
}
|
|
347
|
+
const eventPayload = {
|
|
348
|
+
action,
|
|
349
|
+
response: 'payload' in response ? response.payload : undefined,
|
|
350
|
+
error: 'error' in response ? response.error : undefined,
|
|
351
|
+
chainId,
|
|
352
|
+
}
|
|
353
|
+
this.emit('walletActionResponse', eventPayload)
|
|
354
|
+
} else if (chainId !== undefined) {
|
|
355
|
+
const chainSessionManager = this.getChainSessionManager(chainId)
|
|
356
|
+
await chainSessionManager.handleRedirectResponse(response)
|
|
357
|
+
} else {
|
|
358
|
+
throw new InitializationError(`Could not find a pending request context for the redirect action: ${action}`)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Initiates a connection with the wallet and creates a new session.
|
|
364
|
+
* @param chainId The primary chain ID for the new session.
|
|
365
|
+
* @param permissions (Optional) Permissions to request for an initial explicit session. {@link Signers.Session.ExplicitParams}
|
|
366
|
+
* @param options (Optional) Connection options, such as a preferred login method or email for social or email logins.
|
|
367
|
+
* @throws If the connection process fails. {@link ConnectionError}
|
|
368
|
+
* @throws If a session already exists. {@link InitializationError}
|
|
369
|
+
*
|
|
370
|
+
* @returns A promise that resolves when the connection is established.
|
|
371
|
+
*
|
|
372
|
+
* @see {@link https://docs.sequence.xyz/sdk/typescript/v3/dapp-client/connect} for more detailed documentation.
|
|
373
|
+
*
|
|
374
|
+
* @example
|
|
375
|
+
* const dappClient = new DappClient('http://localhost:5173');
|
|
376
|
+
* await dappClient.connect(137, window.location.origin, undefined, {
|
|
377
|
+
* preferredLoginMethod: 'google',
|
|
378
|
+
* });
|
|
379
|
+
*/
|
|
380
|
+
async connect(
|
|
381
|
+
chainId: number,
|
|
382
|
+
permissions?: Signers.Session.ExplicitParams,
|
|
383
|
+
options: {
|
|
384
|
+
preferredLoginMethod?: LoginMethod
|
|
385
|
+
email?: string
|
|
386
|
+
includeImplicitSession?: boolean
|
|
387
|
+
} = {},
|
|
388
|
+
): Promise<void> {
|
|
389
|
+
if (this.isInitialized) {
|
|
390
|
+
throw new InitializationError('A session already exists. Disconnect first.')
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
const chainSessionManager = this.getChainSessionManager(chainId)
|
|
395
|
+
await chainSessionManager.createNewSession(this.origin, permissions, options)
|
|
396
|
+
|
|
397
|
+
// For popup mode, we need to manually update the state and emit an event.
|
|
398
|
+
// For redirect mode, this code won't be reached; the page will navigate away.
|
|
399
|
+
if (this.transport.mode === TransportMode.POPUP) {
|
|
400
|
+
await this._loadStateFromStorage()
|
|
401
|
+
}
|
|
402
|
+
} catch (err) {
|
|
403
|
+
await this.disconnect()
|
|
404
|
+
throw err
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Adds a new explicit session for a given chain to an existing wallet.
|
|
410
|
+
* @remarks
|
|
411
|
+
* An `explicit session` is a session that can interact with any contract, subject to user-approved permissions.
|
|
412
|
+
* @param chainId The chain ID on which to add the explicit session.
|
|
413
|
+
* @param permissions The permissions to request for the new session. {@link Signers.Session.ExplicitParams}
|
|
414
|
+
*
|
|
415
|
+
* @throws If the session cannot be added. {@link AddExplicitSessionError}
|
|
416
|
+
* @throws If the client or relevant chain is not initialized. {@link InitializationError}
|
|
417
|
+
*
|
|
418
|
+
* @returns A promise that resolves when the session is added.
|
|
419
|
+
*
|
|
420
|
+
* @see {@link https://docs.sequence.xyz/sdk/typescript/v3/dapp-client/add-explicit-session} for more detailed documentation.
|
|
421
|
+
*
|
|
422
|
+
* @example
|
|
423
|
+
* ...
|
|
424
|
+
* import { Signers, Utils } from "@0xsequence/wallet-core";
|
|
425
|
+
* import { DappClient } from "@0xsequence/sessions";
|
|
426
|
+
* ...
|
|
427
|
+
*
|
|
428
|
+
* const dappClient = new DappClient('http://localhost:5173');
|
|
429
|
+
* await dappClient.initialize();
|
|
430
|
+
*
|
|
431
|
+
* const amount = 1000000;
|
|
432
|
+
* const USDC_ADDRESS = '0x...';
|
|
433
|
+
*
|
|
434
|
+
* if (dappClient.isInitialized) {
|
|
435
|
+
* // Allow Dapp (Session Signer) to transfer "amount" of USDC
|
|
436
|
+
* const permissions: Signers.Session.ExplicitParams = {
|
|
437
|
+
* chainId: Number(chainId),
|
|
438
|
+
* valueLimit: 0n, // Not allowed to transfer native tokens (ETH, etc)
|
|
439
|
+
* deadline: BigInt(Date.now() + 1000 * 60 * 5000), // 5000 minutes from now
|
|
440
|
+
* permissions: [Utils.ERC20PermissionBuilder.buildTransfer(USDC_ADDRESS, amount)]
|
|
441
|
+
* };
|
|
442
|
+
* await dappClient.addExplicitSession(1, permissions);
|
|
443
|
+
* }
|
|
444
|
+
*/
|
|
445
|
+
async addExplicitSession(chainId: number, permissions: Signers.Session.ExplicitParams): Promise<void> {
|
|
446
|
+
if (!this.isInitialized || !this.walletAddress)
|
|
447
|
+
throw new InitializationError('Cannot add an explicit session without an existing wallet.')
|
|
448
|
+
|
|
449
|
+
const chainSessionManager = this.getChainSessionManager(chainId)
|
|
450
|
+
if (!chainSessionManager.isInitialized) {
|
|
451
|
+
chainSessionManager.initializeWithWallet(this.walletAddress)
|
|
452
|
+
}
|
|
453
|
+
await chainSessionManager.addExplicitSession(permissions)
|
|
454
|
+
|
|
455
|
+
if (this.transport.mode === TransportMode.POPUP) {
|
|
456
|
+
await this._loadStateFromStorage()
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Modifies the permissions of an existing explicit session for a given chain and session address.
|
|
462
|
+
* @param chainId The chain ID on which the explicit session exists.
|
|
463
|
+
* @param sessionAddress The address of the explicit session to modify. {@link Address.Address}
|
|
464
|
+
* @param permissions The new permissions to set for the session. {@link Signers.Session.ExplicitParams}
|
|
465
|
+
*
|
|
466
|
+
* @throws If the client or relevant chain is not initialized. {@link InitializationError}
|
|
467
|
+
* @throws If something goes wrong while modifying the session. {@link ModifyExplicitSessionError}
|
|
468
|
+
*
|
|
469
|
+
* @returns A promise that resolves when the session permissions are updated.
|
|
470
|
+
*
|
|
471
|
+
* @see {@link https://docs.sequence.xyz/sdk/typescript/v3/dapp-client/modify-explicit-session} for more detailed documentation.
|
|
472
|
+
*
|
|
473
|
+
* @example
|
|
474
|
+
* const dappClient = new DappClient('http://localhost:5173');
|
|
475
|
+
* await dappClient.initialize();
|
|
476
|
+
*
|
|
477
|
+
* if (dappClient.isInitialized) {
|
|
478
|
+
* // The address of an existing explicit session (Grants the Dapp permission to transfer 100 USDC for the user)
|
|
479
|
+
* const sessionAddress = '0x...';
|
|
480
|
+
* // We create a new permission object where we can increase the granted transfer amount limit
|
|
481
|
+
* const permissions: Signers.Session.ExplicitParams = {
|
|
482
|
+
* chainId: Number(chainId),
|
|
483
|
+
* valueLimit: 0n,
|
|
484
|
+
* deadline: BigInt(Date.now() + 1000 * 60 * 5000),
|
|
485
|
+
* permissions: [Utils.ERC20PermissionBuilder.buildTransfer(USDC_ADDRESS, amount)]
|
|
486
|
+
* };
|
|
487
|
+
* await dappClient.modifyExplicitSession(1, sessionAddress, permissions);
|
|
488
|
+
* }
|
|
489
|
+
*/
|
|
490
|
+
async modifyExplicitSession(
|
|
491
|
+
chainId: number,
|
|
492
|
+
sessionAddress: Address.Address,
|
|
493
|
+
permissions: Signers.Session.ExplicitParams,
|
|
494
|
+
): Promise<void> {
|
|
495
|
+
if (!this.isInitialized || !this.walletAddress)
|
|
496
|
+
throw new InitializationError('Cannot modify an explicit session without an existing wallet.')
|
|
497
|
+
|
|
498
|
+
const chainSessionManager = this.getChainSessionManager(chainId)
|
|
499
|
+
if (!chainSessionManager.isInitialized) {
|
|
500
|
+
chainSessionManager.initializeWithWallet(this.walletAddress)
|
|
501
|
+
}
|
|
502
|
+
await chainSessionManager.modifyExplicitSession(sessionAddress, permissions)
|
|
503
|
+
|
|
504
|
+
if (this.transport.mode === TransportMode.POPUP) {
|
|
505
|
+
await this._loadStateFromStorage()
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Gets the gas fee options for an array of transactions.
|
|
511
|
+
* @param chainId The chain ID on which to get the fee options.
|
|
512
|
+
* @param transactions An array of transactions to get fee options for. These transactions will not be sent.
|
|
513
|
+
* @throws If the fee options cannot be fetched. {@link FeeOptionError}
|
|
514
|
+
* @throws If the client or relevant chain is not initialized. {@link InitializationError}
|
|
515
|
+
*
|
|
516
|
+
* @returns A promise that resolves with the fee options. {@link Relayer.FeeOption[]}
|
|
517
|
+
*
|
|
518
|
+
* @see {@link https://docs.sequence.xyz/sdk/typescript/v3/dapp-client/get-fee-options} for more detailed documentation.
|
|
519
|
+
*
|
|
520
|
+
* @example
|
|
521
|
+
* const dappClient = new DappClient('http://localhost:5173');
|
|
522
|
+
* await dappClient.initialize();
|
|
523
|
+
*
|
|
524
|
+
* if (dappClient.isInitialized) {
|
|
525
|
+
* const transactions: Transaction[] = [
|
|
526
|
+
* {
|
|
527
|
+
* to: '0x...',
|
|
528
|
+
* value: 0n,
|
|
529
|
+
* data: '0x...'
|
|
530
|
+
* }
|
|
531
|
+
* ];
|
|
532
|
+
* const feeOptions = await dappClient.getFeeOptions(1, transactions);
|
|
533
|
+
* const feeOption = feeOptions[0];
|
|
534
|
+
* // use the fee option to pay the gas
|
|
535
|
+
* const txHash = await dappClient.sendTransaction(1, transactions, feeOption);
|
|
536
|
+
* }
|
|
537
|
+
*/
|
|
538
|
+
async getFeeOptions(chainId: number, transactions: Transaction[]): Promise<Relayer.FeeOption[]> {
|
|
539
|
+
if (!this.isInitialized) throw new InitializationError('Not initialized')
|
|
540
|
+
const chainSessionManager = this.getChainSessionManager(chainId)
|
|
541
|
+
if (!chainSessionManager.isInitialized)
|
|
542
|
+
throw new InitializationError(`ChainSessionManager for chain ${chainId} is not initialized.`)
|
|
543
|
+
return await chainSessionManager.getFeeOptions(transactions)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Checks if the current session has permission to execute a set of transactions on a specific chain.
|
|
548
|
+
* @param chainId The chain ID on which to check the permissions.
|
|
549
|
+
* @param transactions An array of transactions to check permissions for.
|
|
550
|
+
* @returns A promise that resolves to true if the session has permission, otherwise false.
|
|
551
|
+
*/
|
|
552
|
+
async hasPermission(chainId: number, transactions: Transaction[]): Promise<boolean> {
|
|
553
|
+
const chainSessionManager = this.chainSessionManagers.get(chainId)
|
|
554
|
+
if (!chainSessionManager || !chainSessionManager.isInitialized) {
|
|
555
|
+
return false
|
|
556
|
+
}
|
|
557
|
+
return await chainSessionManager.hasPermission(transactions)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Signs and sends a transaction using an available session signer.
|
|
562
|
+
* @param chainId The chain ID on which to send the transaction.
|
|
563
|
+
* @param transactions An array of transactions to be executed atomically in a single batch. {@link Transaction}
|
|
564
|
+
* @param feeOption (Optional) The selected fee option to sponsor the transaction. {@link Relayer.FeeOption}
|
|
565
|
+
* @throws {TransactionError} If the transaction fails to send or confirm.
|
|
566
|
+
* @throws {InitializationError} If the client or relevant chain is not initialized.
|
|
567
|
+
*
|
|
568
|
+
* @returns A promise that resolves with the transaction hash.
|
|
569
|
+
*
|
|
570
|
+
* @see {@link https://docs.sequence.xyz/sdk/typescript/v3/dapp-client/send-transaction} for more detailed documentation.
|
|
571
|
+
*
|
|
572
|
+
* @example
|
|
573
|
+
* const dappClient = new DappClient('http://localhost:5173');
|
|
574
|
+
* await dappClient.initialize();
|
|
575
|
+
*
|
|
576
|
+
* if (dappClient.isInitialized) {
|
|
577
|
+
* const transaction = {
|
|
578
|
+
* to: '0x...',
|
|
579
|
+
* value: 0n,
|
|
580
|
+
* data: '0x...'
|
|
581
|
+
* };
|
|
582
|
+
*
|
|
583
|
+
* const txHash = await dappClient.sendTransaction(1, [transaction]);
|
|
584
|
+
*/
|
|
585
|
+
async sendTransaction(chainId: number, transactions: Transaction[], feeOption?: Relayer.FeeOption): Promise<Hex.Hex> {
|
|
586
|
+
if (!this.isInitialized) throw new InitializationError('Not initialized')
|
|
587
|
+
const chainSessionManager = this.getChainSessionManager(chainId)
|
|
588
|
+
if (!chainSessionManager.isInitialized)
|
|
589
|
+
throw new InitializationError(`ChainSessionManager for chain ${chainId} is not initialized.`)
|
|
590
|
+
return await chainSessionManager.buildSignAndSendTransactions(transactions, feeOption)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Signs a standard message (EIP-191) using an available session signer.
|
|
595
|
+
* @param chainId The chain ID on which to sign the message.
|
|
596
|
+
* @param message The message to sign.
|
|
597
|
+
* @throws If the message cannot be signed. {@link SigningError}
|
|
598
|
+
* @throws If the client is not initialized. {@link InitializationError}
|
|
599
|
+
*
|
|
600
|
+
* @returns A promise that resolves when the signing process is initiated. The signature is delivered via the `walletActionResponse` event listener.
|
|
601
|
+
*
|
|
602
|
+
* @see {@link https://docs.sequence.xyz/sdk/typescript/v3/dapp-client/sign-message} for more detailed documentation.
|
|
603
|
+
*
|
|
604
|
+
* @example
|
|
605
|
+
* const dappClient = new DappClient('http://localhost:5173');
|
|
606
|
+
* await dappClient.initialize();
|
|
607
|
+
*
|
|
608
|
+
* if (dappClient.isInitialized) {
|
|
609
|
+
* const message = 'Hello, world!';
|
|
610
|
+
* await dappClient.signMessage(1, message);
|
|
611
|
+
* }
|
|
612
|
+
*/
|
|
613
|
+
async signMessage(chainId: number, message: string): Promise<void> {
|
|
614
|
+
if (!this.isInitialized || !this.walletAddress) throw new InitializationError('Not initialized')
|
|
615
|
+
const payload: SignMessagePayload = {
|
|
616
|
+
address: this.walletAddress,
|
|
617
|
+
message,
|
|
618
|
+
chainId: chainId,
|
|
619
|
+
}
|
|
620
|
+
try {
|
|
621
|
+
await this._requestWalletAction(RequestActionType.SIGN_MESSAGE, payload, chainId)
|
|
622
|
+
} catch (err) {
|
|
623
|
+
throw new SigningError(`Signing message failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Signs a typed data object (EIP-712) using an available session signer.
|
|
629
|
+
* @param chainId The chain ID on which to sign the typed data.
|
|
630
|
+
* @param typedData The typed data object to sign.
|
|
631
|
+
* @throws If the typed data cannot be signed. {@link SigningError}
|
|
632
|
+
* @throws If the client is not initialized. {@link InitializationError}
|
|
633
|
+
*
|
|
634
|
+
* @returns A promise that resolves when the signing process is initiated. The signature is returned in the `walletActionResponse` event listener.
|
|
635
|
+
*
|
|
636
|
+
* @see {@link https://docs.sequence.xyz/sdk/typescript/v3/dapp-client/sign-typed-data} for more detailed documentation.
|
|
637
|
+
*
|
|
638
|
+
* @example
|
|
639
|
+
* const dappClient = new DappClient('http://localhost:5173');
|
|
640
|
+
* await dappClient.initialize();
|
|
641
|
+
*
|
|
642
|
+
* if (dappClient.isInitialized) {
|
|
643
|
+
* const typedData = {...}
|
|
644
|
+
* await dappClient.signTypedData(1, typedData);
|
|
645
|
+
* }
|
|
646
|
+
*/
|
|
647
|
+
async signTypedData(chainId: number, typedData: TypedData): Promise<void> {
|
|
648
|
+
if (!this.isInitialized || !this.walletAddress) throw new InitializationError('Not initialized')
|
|
649
|
+
const payload: SignTypedDataPayload = {
|
|
650
|
+
address: this.walletAddress,
|
|
651
|
+
typedData,
|
|
652
|
+
chainId: chainId,
|
|
653
|
+
}
|
|
654
|
+
try {
|
|
655
|
+
await this._requestWalletAction(RequestActionType.SIGN_TYPED_DATA, payload, chainId)
|
|
656
|
+
} catch (err) {
|
|
657
|
+
throw new SigningError(`Signing typed data failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Sends transaction data to be signed and submitted by the wallet.
|
|
663
|
+
* @param chainId The chain ID on which to send the transaction.
|
|
664
|
+
* @param transactionRequest The transaction request object.
|
|
665
|
+
* @throws If the transaction cannot be sent. {@link TransactionError}
|
|
666
|
+
* @throws If the client is not initialized. {@link InitializationError}
|
|
667
|
+
*
|
|
668
|
+
* @returns A promise that resolves when the sending process is initiated. The transaction hash is delivered via the `walletActionResponse` event listener.
|
|
669
|
+
*/
|
|
670
|
+
async sendWalletTransaction(chainId: number, transactionRequest: TransactionRequest): Promise<void> {
|
|
671
|
+
if (!this.isInitialized || !this.walletAddress) throw new InitializationError('Not initialized')
|
|
672
|
+
const payload: SendWalletTransactionPayload = {
|
|
673
|
+
address: this.walletAddress,
|
|
674
|
+
transactionRequest,
|
|
675
|
+
chainId: chainId,
|
|
676
|
+
}
|
|
677
|
+
try {
|
|
678
|
+
await this._requestWalletAction(RequestActionType.SEND_WALLET_TRANSACTION, payload, chainId)
|
|
679
|
+
} catch (err) {
|
|
680
|
+
throw new TransactionError(
|
|
681
|
+
`Sending transaction data to wallet failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
682
|
+
)
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Disconnects the client, clearing all session data from browser storage.
|
|
688
|
+
* @remarks This action does not revoke the sessions on-chain. Sessions remain active until they expire or are manually revoked by the user in their wallet.
|
|
689
|
+
* @returns A promise that resolves when disconnection is complete.
|
|
690
|
+
*
|
|
691
|
+
* @see {@link https://docs.sequence.xyz/sdk/typescript/v3/dapp-client/disconnect} for more detailed documentation.
|
|
692
|
+
*
|
|
693
|
+
* @example
|
|
694
|
+
* const dappClient = new DappClient('http://localhost:5173');
|
|
695
|
+
* await dappClient.initialize();
|
|
696
|
+
*
|
|
697
|
+
* if (dappClient.isInitialized) {
|
|
698
|
+
* await dappClient.disconnect();
|
|
699
|
+
* }
|
|
700
|
+
*/
|
|
701
|
+
async disconnect(): Promise<void> {
|
|
702
|
+
const transportMode = this.transport.mode
|
|
703
|
+
|
|
704
|
+
this.transport.destroy()
|
|
705
|
+
this.transport = new DappTransport(
|
|
706
|
+
this.walletUrl,
|
|
707
|
+
transportMode,
|
|
708
|
+
undefined,
|
|
709
|
+
this.sequenceSessionStorage,
|
|
710
|
+
this.redirectActionHandler,
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
this.chainSessionManagers.clear()
|
|
714
|
+
await this.sequenceStorage.clearAllData()
|
|
715
|
+
this.isInitialized = false
|
|
716
|
+
this.walletAddress = null
|
|
717
|
+
this.loginMethod = null
|
|
718
|
+
this.userEmail = null
|
|
719
|
+
this.emit('sessionsUpdated')
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* @private Emits an event to all registered listeners.
|
|
724
|
+
* @param event The event to emit.
|
|
725
|
+
* @param args The data to emit with the event.
|
|
726
|
+
*/
|
|
727
|
+
private emit<K extends keyof DappClientEventMap>(event: K, ...args: Parameters<DappClientEventMap[K]>): void {
|
|
728
|
+
const listeners = this.eventListeners[event]
|
|
729
|
+
if (listeners) {
|
|
730
|
+
listeners.forEach((listener) => (listener as (...a: typeof args) => void)(...args))
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
private async _requestWalletAction(
|
|
735
|
+
action: (typeof RequestActionType)['SIGN_MESSAGE' | 'SIGN_TYPED_DATA' | 'SEND_WALLET_TRANSACTION'],
|
|
736
|
+
payload: SignMessagePayload | SignTypedDataPayload | SendWalletTransactionPayload,
|
|
737
|
+
chainId: number,
|
|
738
|
+
): Promise<void> {
|
|
739
|
+
if (!this.isInitialized || !this.walletAddress) {
|
|
740
|
+
throw new InitializationError('Session not initialized. Cannot request wallet action.')
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
try {
|
|
744
|
+
const redirectUrl = this.origin + (this.redirectPath ? this.redirectPath : '')
|
|
745
|
+
const path = action === RequestActionType.SEND_WALLET_TRANSACTION ? '/request/transaction' : '/request/sign'
|
|
746
|
+
|
|
747
|
+
if (this.transport.mode === TransportMode.REDIRECT) {
|
|
748
|
+
await this.sequenceStorage.savePendingRequest({
|
|
749
|
+
action,
|
|
750
|
+
payload,
|
|
751
|
+
chainId: chainId,
|
|
752
|
+
})
|
|
753
|
+
await this.sequenceStorage.setPendingRedirectRequest(true)
|
|
754
|
+
await this.transport.sendRequest(action, redirectUrl, payload, { path })
|
|
755
|
+
} else {
|
|
756
|
+
const response = await this.transport.sendRequest<WalletActionResponse>(action, redirectUrl, payload, {
|
|
757
|
+
path,
|
|
758
|
+
})
|
|
759
|
+
this.emit('walletActionResponse', { action, response, chainId })
|
|
760
|
+
}
|
|
761
|
+
} catch (err) {
|
|
762
|
+
const error = new SigningError(err instanceof Error ? err.message : String(err))
|
|
763
|
+
this.emit('walletActionResponse', { action, error, chainId })
|
|
764
|
+
throw error
|
|
765
|
+
} finally {
|
|
766
|
+
if (this.transport.mode === TransportMode.POPUP) {
|
|
767
|
+
this.transport.closeWallet()
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* @private Retrieves or creates a ChainSessionManager for a given chain ID.
|
|
774
|
+
* @param chainId The chain ID to get the ChainSessionManager for.
|
|
775
|
+
* @returns The ChainSessionManager for the given chain ID. {@link ChainSessionManager}
|
|
776
|
+
*/
|
|
777
|
+
private getChainSessionManager(chainId: number): ChainSessionManager {
|
|
778
|
+
let chainSessionManager = this.chainSessionManagers.get(chainId)
|
|
779
|
+
if (!chainSessionManager) {
|
|
780
|
+
chainSessionManager = new ChainSessionManager(
|
|
781
|
+
chainId,
|
|
782
|
+
this.transport,
|
|
783
|
+
this.projectAccessKey,
|
|
784
|
+
this.keymachineUrl,
|
|
785
|
+
this.nodesUrl,
|
|
786
|
+
this.relayerUrl,
|
|
787
|
+
this.sequenceStorage,
|
|
788
|
+
this.origin + (this.redirectPath ? this.redirectPath : ''),
|
|
789
|
+
this.guard,
|
|
790
|
+
this.randomPrivateKeyFn,
|
|
791
|
+
this.canUseIndexedDb,
|
|
792
|
+
)
|
|
793
|
+
this.chainSessionManagers.set(chainId, chainSessionManager)
|
|
794
|
+
|
|
795
|
+
chainSessionManager.on('explicitSessionResponse', (data) => {
|
|
796
|
+
this.emit('explicitSessionResponse', { ...data, chainId })
|
|
797
|
+
})
|
|
798
|
+
}
|
|
799
|
+
return chainSessionManager
|
|
800
|
+
}
|
|
801
|
+
}
|