@0xsequence/provider 0.0.0-2021823181734

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 (54) hide show
  1. package/CHANGELOG.md +909 -0
  2. package/README.md +4 -0
  3. package/dist/0xsequence-provider.cjs.d.ts +1 -0
  4. package/dist/0xsequence-provider.cjs.dev.js +3540 -0
  5. package/dist/0xsequence-provider.cjs.js +7 -0
  6. package/dist/0xsequence-provider.cjs.prod.js +3540 -0
  7. package/dist/0xsequence-provider.esm.js +3508 -0
  8. package/dist/declarations/src/commands/index.d.ts +20 -0
  9. package/dist/declarations/src/index.d.ts +5 -0
  10. package/dist/declarations/src/provider.d.ts +59 -0
  11. package/dist/declarations/src/transports/base-provider-transport.d.ts +40 -0
  12. package/dist/declarations/src/transports/base-wallet-transport.d.ts +38 -0
  13. package/dist/declarations/src/transports/extension-transport/base-injected-transport.d.ts +18 -0
  14. package/dist/declarations/src/transports/extension-transport/extension-message-handler.d.ts +12 -0
  15. package/dist/declarations/src/transports/extension-transport/extension-message-provider.d.ts +11 -0
  16. package/dist/declarations/src/transports/extension-transport/index.d.ts +3 -0
  17. package/dist/declarations/src/transports/index.d.ts +7 -0
  18. package/dist/declarations/src/transports/mux-transport/index.d.ts +1 -0
  19. package/dist/declarations/src/transports/mux-transport/mux-message-provider.d.ts +22 -0
  20. package/dist/declarations/src/transports/proxy-transport/index.d.ts +3 -0
  21. package/dist/declarations/src/transports/proxy-transport/proxy-message-channel.d.ts +15 -0
  22. package/dist/declarations/src/transports/proxy-transport/proxy-message-handler.d.ts +12 -0
  23. package/dist/declarations/src/transports/proxy-transport/proxy-message-provider.d.ts +12 -0
  24. package/dist/declarations/src/transports/wallet-request-handler.d.ts +51 -0
  25. package/dist/declarations/src/transports/window-transport/index.d.ts +2 -0
  26. package/dist/declarations/src/transports/window-transport/window-message-handler.d.ts +15 -0
  27. package/dist/declarations/src/transports/window-transport/window-message-provider.d.ts +13 -0
  28. package/dist/declarations/src/types.d.ts +170 -0
  29. package/dist/declarations/src/utils.d.ts +14 -0
  30. package/dist/declarations/src/wallet.d.ts +99 -0
  31. package/package.json +37 -0
  32. package/src/commands/index.ts +146 -0
  33. package/src/index.ts +5 -0
  34. package/src/provider.ts +494 -0
  35. package/src/transports/base-provider-transport.ts +411 -0
  36. package/src/transports/base-wallet-transport.ts +441 -0
  37. package/src/transports/extension-transport/base-injected-transport.ts +100 -0
  38. package/src/transports/extension-transport/extension-message-handler.ts +25 -0
  39. package/src/transports/extension-transport/extension-message-provider.ts +41 -0
  40. package/src/transports/extension-transport/index.ts +3 -0
  41. package/src/transports/index.ts +7 -0
  42. package/src/transports/mux-transport/index.ts +1 -0
  43. package/src/transports/mux-transport/mux-message-provider.ts +160 -0
  44. package/src/transports/proxy-transport/index.ts +3 -0
  45. package/src/transports/proxy-transport/proxy-message-channel.ts +58 -0
  46. package/src/transports/proxy-transport/proxy-message-handler.ts +46 -0
  47. package/src/transports/proxy-transport/proxy-message-provider.ts +81 -0
  48. package/src/transports/wallet-request-handler.ts +702 -0
  49. package/src/transports/window-transport/index.ts +2 -0
  50. package/src/transports/window-transport/window-message-handler.ts +166 -0
  51. package/src/transports/window-transport/window-message-provider.ts +183 -0
  52. package/src/types.ts +252 -0
  53. package/src/utils.ts +76 -0
  54. package/src/wallet.ts +692 -0
@@ -0,0 +1,411 @@
1
+ import { EventEmitter2 as EventEmitter } from 'eventemitter2'
2
+
3
+ import {
4
+ ProviderTransport,
5
+ ProviderMessage,
6
+ ProviderMessageRequest,
7
+ EventType,
8
+ ProviderEventTypes,
9
+ ProviderMessageResponse,
10
+ ProviderMessageResponseCallback,
11
+ OpenState,
12
+ OpenWalletIntent,
13
+ ConnectDetails,
14
+ WalletSession,
15
+ ProviderRpcError,
16
+ InitState,
17
+ TypedEventEmitter
18
+ } from '../types'
19
+
20
+ import { NetworkConfig, WalletContext, JsonRpcRequest, JsonRpcResponseCallback, JsonRpcResponse } from '@0xsequence/network'
21
+ import { logger } from '@0xsequence/utils'
22
+ import { ethers } from 'ethers'
23
+
24
+ export const PROVIDER_OPEN_TIMEOUT = 30000 // in ms
25
+
26
+ let _messageIdx = 0
27
+
28
+ export const nextMessageIdx = () => ++_messageIdx
29
+
30
+ export abstract class BaseProviderTransport implements ProviderTransport {
31
+ protected pendingMessageRequests: ProviderMessageRequest[] = []
32
+ protected responseCallbacks = new Map<number, ProviderMessageResponseCallback>()
33
+
34
+ protected state: OpenState
35
+ protected confirmationOnly: boolean = false
36
+ protected events: TypedEventEmitter<ProviderEventTypes> = new EventEmitter() as TypedEventEmitter<ProviderEventTypes>
37
+
38
+ protected openPayload: { sessionId?: string; session?: WalletSession } | undefined
39
+ protected connectPayload: ConnectDetails | undefined
40
+ protected accountsChangedPayload: string | undefined
41
+ protected networksPayload: NetworkConfig[] | undefined
42
+ protected walletContextPayload: WalletContext | undefined
43
+
44
+ protected _sessionId?: string
45
+ protected _init: InitState
46
+ protected _registered: boolean
47
+
48
+ constructor() {
49
+ this.state = OpenState.CLOSED
50
+ this._registered = false
51
+ this._init = InitState.NIL
52
+ }
53
+
54
+ get registered(): boolean {
55
+ return this._registered
56
+ }
57
+
58
+ register() {
59
+ throw new Error('abstract method')
60
+ }
61
+
62
+ unregister() {
63
+ throw new Error('abstract method')
64
+ }
65
+
66
+ openWallet(path?: string, intent?: OpenWalletIntent, networkId?: string | number) {
67
+ throw new Error('abstract method')
68
+ }
69
+
70
+ closeWallet() {
71
+ throw new Error('abstract method')
72
+ }
73
+
74
+ isOpened(): boolean {
75
+ return this.registered && this.state === OpenState.OPENED
76
+ }
77
+
78
+ isConnected(): boolean {
79
+ // if we're registered, and we have the account details, then we are connected
80
+ const session = this.openPayload?.session
81
+ return (
82
+ this.registered &&
83
+ session !== undefined &&
84
+ !!session.accountAddress &&
85
+ session.accountAddress.length === 42 &&
86
+ !!session.networks &&
87
+ session.networks.length > 0
88
+ )
89
+ }
90
+
91
+ sendAsync = async (request: JsonRpcRequest, callback: JsonRpcResponseCallback, chainId?: number) => {
92
+ // here, we receive the message from the dapp provider call
93
+
94
+ if (this.state === OpenState.CLOSED) {
95
+ // flag the wallet to auto-close once user submits input. ie.
96
+ // prompting to sign a message or transaction
97
+ this.confirmationOnly = true
98
+ }
99
+
100
+ // open/focus the wallet.
101
+ // automatically open the wallet when a provider request makes it here.
102
+ //
103
+ // NOTE: if we're not signed in, then the provider will fail, users must first connect+sign in.
104
+ //
105
+ // TODO: how does this behave with a session has expired?
106
+ this.openWallet(undefined, { type: 'jsonRpcRequest', method: request.method }, chainId)
107
+
108
+ // send message request, await, and then execute callback after receiving the response
109
+ try {
110
+ if (!this.isOpened()) {
111
+ await this.waitUntilOpened() // will throw on timeout
112
+ }
113
+
114
+ const response = await this.sendMessageRequest({
115
+ idx: nextMessageIdx(),
116
+ type: EventType.MESSAGE,
117
+ data: request,
118
+ chainId: chainId
119
+ })
120
+ callback(undefined, response.data)
121
+ } catch (err) {
122
+ callback(err)
123
+ }
124
+ }
125
+
126
+ // handleMessage will handle message received from the remote wallet
127
+ handleMessage(message: ProviderMessage<any>) {
128
+ // init incoming for initial handshake with transport.
129
+ // always respond to INIT messages, e.g. on popup window reload
130
+ if (message.type === EventType.INIT) {
131
+ logger.debug('MessageProvider, received INIT message', message)
132
+ const { nonce } = message.data as { nonce: string }
133
+ if (!nonce || nonce.length == 0) {
134
+ logger.error('invalid init nonce')
135
+ return
136
+ }
137
+ this._init = InitState.OK
138
+ this.sendMessage({
139
+ idx: -1,
140
+ type: EventType.INIT,
141
+ data: {
142
+ sessionId: this._sessionId,
143
+ nonce: nonce
144
+ }
145
+ })
146
+ }
147
+
148
+ if (this._init !== InitState.OK) {
149
+ // if provider is not init'd, then we drop any received messages. the only
150
+ // message we will process is of event type 'init', as our acknowledgement
151
+ return
152
+ }
153
+
154
+ // message is either a notification, or its a ProviderMessageResponse
155
+ logger.debug('RECEIVED MESSAGE FROM WALLET', message.idx, message)
156
+
157
+ const requestIdx = message.idx
158
+ const responseCallback = this.responseCallbacks.get(requestIdx)
159
+ if (requestIdx) {
160
+ this.responseCallbacks.delete(requestIdx)
161
+ }
162
+
163
+ // OPEN response
164
+ //
165
+ // Flip opened flag, and flush the pending queue
166
+ if (message.type === EventType.OPEN && !this.isOpened()) {
167
+ if (this._sessionId && this._sessionId !== message.data?.sessionId) {
168
+ logger.debug('open event received from wallet, but does not match sessionId', this._sessionId)
169
+ return
170
+ }
171
+
172
+ // check if open error occured due to invalid defaultNetworkId
173
+ if (message.data?.error) {
174
+ const err = new Error(`opening wallet failed: received ${message.data?.error}`)
175
+ logger.error(err)
176
+ this.close()
177
+ throw err
178
+ }
179
+
180
+ // success!
181
+ this.state = OpenState.OPENED
182
+ this.openPayload = message.data
183
+ this.events.emit('open', this.openPayload!)
184
+
185
+ // flush pending requests when connected
186
+ if (this.pendingMessageRequests.length !== 0) {
187
+ const pendingMessageRequests = this.pendingMessageRequests.splice(0, this.pendingMessageRequests.length)
188
+
189
+ pendingMessageRequests.forEach(async pendingMessageRequest => {
190
+ this.sendMessage(pendingMessageRequest)
191
+ })
192
+ }
193
+
194
+ return
195
+ }
196
+
197
+ // MESSAGE resposne
198
+ if (message.type === EventType.MESSAGE) {
199
+ // Require user confirmation, bring up wallet to prompt for input then close
200
+ // TODO: perhaps apply technique like in multicall to queue messages within
201
+ // a period of time, then close the window if responseCallbacks is empty, this is better.
202
+ if (this.confirmationOnly) {
203
+ setTimeout(() => {
204
+ if (this.responseCallbacks.size === 0) {
205
+ this.closeWallet()
206
+ }
207
+ }, 500) // TODO: be smarter about timer as we're processing the response callbacks..
208
+ }
209
+
210
+ if (!responseCallback) {
211
+ // NOTE: this would occur if 'idx' isn't set, which should never happen
212
+ // or when we register two handler, or duplicate messages with the same idx are sent,
213
+ // all of which should be prevented prior to getting to this point
214
+ throw new Error('impossible state')
215
+ }
216
+
217
+ // Callback to original caller
218
+ if (responseCallback) {
219
+ this.events.emit('message', message)
220
+ responseCallback((message as ProviderMessageResponse).data.error, message)
221
+ return
222
+ }
223
+ }
224
+
225
+ // ACCOUNTS_CHANGED -- when a user logs in or out
226
+ if (message.type === EventType.ACCOUNTS_CHANGED) {
227
+ this.accountsChangedPayload = undefined
228
+ if (message.data && message.data.length > 0) {
229
+ this.accountsChangedPayload = ethers.utils.getAddress(message.data[0])
230
+ this.events.emit('accountsChanged', [this.accountsChangedPayload])
231
+ } else {
232
+ this.events.emit('accountsChanged', [])
233
+ }
234
+ return
235
+ }
236
+
237
+ // CHAIN_CHANGED -- when a user changes their default chain
238
+ if (message.type === EventType.CHAIN_CHANGED) {
239
+ this.events.emit('chainChanged', message.data)
240
+ return
241
+ }
242
+
243
+ // NOTIFY NETWORKS -- when a user connects or logs in
244
+ if (message.type === EventType.NETWORKS) {
245
+ this.networksPayload = message.data
246
+ this.events.emit('networks', this.networksPayload!)
247
+ return
248
+ }
249
+
250
+ // NOTIFY WALLET_CONTEXT -- when a user connects or logs in
251
+ if (message.type === EventType.WALLET_CONTEXT) {
252
+ this.walletContextPayload = message.data
253
+ this.events.emit('walletContext', this.walletContextPayload!)
254
+ return
255
+ }
256
+
257
+ // NOTIFY CLOSE -- when wallet instructs to close
258
+ if (message.type === EventType.CLOSE) {
259
+ if (this.state !== OpenState.CLOSED) {
260
+ this.close(message.data)
261
+ }
262
+ }
263
+
264
+ // NOTIFY CONNECT -- when wallet instructs we've connected
265
+ if (message.type === EventType.CONNECT) {
266
+ this.connectPayload = message.data
267
+ this.events.emit('connect', this.connectPayload!)
268
+ }
269
+
270
+ // NOTIFY DISCONNECT -- when wallet instructs to disconnect
271
+ if (message.type === EventType.DISCONNECT) {
272
+ if (this.isConnected()) {
273
+ this.events.emit('disconnect', message.data)
274
+ this.close()
275
+ }
276
+ }
277
+ }
278
+
279
+ // sendMessageRequest sends a ProviderMessageRequest over the wire to the wallet
280
+ sendMessageRequest = async (message: ProviderMessageRequest): Promise<ProviderMessageResponse> => {
281
+ return new Promise((resolve, reject) => {
282
+ if ((!message.idx || message.idx <= 0) && message.type !== 'init') {
283
+ reject(new Error('message idx not set'))
284
+ }
285
+
286
+ const responseCallback: ProviderMessageResponseCallback = (error: ProviderRpcError, response?: ProviderMessageResponse) => {
287
+ if (error) {
288
+ reject(error)
289
+ } else if (response) {
290
+ resolve(response)
291
+ } else {
292
+ throw new Error('no valid response to return')
293
+ }
294
+ }
295
+
296
+ const idx = message.idx
297
+ if (!this.responseCallbacks.get(idx)) {
298
+ this.responseCallbacks.set(idx, responseCallback)
299
+ } else {
300
+ reject(new Error('duplicate message idx, should never happen'))
301
+ }
302
+
303
+ if (!this.isOpened()) {
304
+ logger.debug('pushing to pending requests', message)
305
+ this.pendingMessageRequests.push(message)
306
+ } else {
307
+ this.sendMessage(message)
308
+ }
309
+ })
310
+ }
311
+
312
+ sendMessage(message: ProviderMessage<any>) {
313
+ throw new Error('abstract method')
314
+ }
315
+
316
+ on<K extends keyof ProviderEventTypes>(event: K, fn: ProviderEventTypes[K]) {
317
+ this.events.on(event, fn as any)
318
+ }
319
+
320
+ once<K extends keyof ProviderEventTypes>(event: K, fn: ProviderEventTypes[K]) {
321
+ this.events.once(event, fn as any)
322
+ }
323
+
324
+ emit<K extends keyof ProviderEventTypes>(event: K, ...args: Parameters<ProviderEventTypes[K]>): boolean {
325
+ return this.events.emit(event, ...(args as any))
326
+ }
327
+
328
+ waitUntilOpened = async (openTimeout = PROVIDER_OPEN_TIMEOUT): Promise<WalletSession | undefined> => {
329
+ let opened = false
330
+ return Promise.race([
331
+ new Promise<WalletSession | undefined>((_, reject) => {
332
+ const timeout = setTimeout(() => {
333
+ clearTimeout(timeout)
334
+ // only emit close if the timeout wins the race
335
+ if (!opened) {
336
+ this.state = OpenState.CLOSED
337
+ this.events.emit('close', { code: 1005, message: 'opening wallet timed out' } as ProviderRpcError)
338
+ }
339
+ reject(new Error('opening wallet timed out'))
340
+ }, openTimeout)
341
+ }),
342
+ new Promise<WalletSession | undefined>(resolve => {
343
+ if (this.isOpened()) {
344
+ opened = true
345
+ resolve(this.openPayload?.session)
346
+ return
347
+ }
348
+ this.events.once('open', (openInfo: { session?: WalletSession }) => {
349
+ this.openPayload = openInfo
350
+ opened = true
351
+ resolve(openInfo.session)
352
+ })
353
+ })
354
+ ])
355
+ }
356
+
357
+ waitUntilConnected = async (): Promise<ConnectDetails> => {
358
+ await this.waitUntilOpened()
359
+
360
+ const connect = new Promise<ConnectDetails>(resolve => {
361
+ if (this.connectPayload) {
362
+ resolve(this.connectPayload)
363
+ return
364
+ }
365
+
366
+ this.events.once('connect', connectDetails => {
367
+ this.connectPayload = connectDetails
368
+ resolve(connectDetails)
369
+ })
370
+ })
371
+
372
+ const closeWallet = new Promise<ConnectDetails>((_, reject) => {
373
+ this.events.once('close', error => {
374
+ if (error) {
375
+ reject(new Error(`wallet closed due to ${JSON.stringify(error)}`))
376
+ } else {
377
+ reject(new Error(`user closed the wallet`))
378
+ }
379
+ })
380
+ })
381
+
382
+ return Promise.race<ConnectDetails>([connect, closeWallet])
383
+ }
384
+
385
+ protected close(error?: ProviderRpcError) {
386
+ if (this.state === OpenState.CLOSED) return
387
+
388
+ this.state = OpenState.CLOSED
389
+ this.confirmationOnly = false
390
+ this._sessionId = undefined
391
+ logger.info('closing wallet and flushing!')
392
+
393
+ // flush pending requests and return error to all callbacks
394
+ this.pendingMessageRequests.length = 0
395
+ this.responseCallbacks.forEach(responseCallback => {
396
+ responseCallback({
397
+ ...new Error('wallet closed'),
398
+ code: 4001
399
+ })
400
+ })
401
+ this.responseCallbacks.clear()
402
+
403
+ this.connectPayload = undefined
404
+ this.openPayload = undefined
405
+ this.accountsChangedPayload = undefined
406
+ this.networksPayload = undefined
407
+ this.walletContextPayload = undefined
408
+
409
+ this.events.emit('close', error)
410
+ }
411
+ }