-
Notifications
You must be signed in to change notification settings - Fork 10
/
index.ts
386 lines (306 loc) · 11.9 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
import EventEmitter from 'events'
import { create as createPayload, JsonRpcPayload } from './payload'
import type {
Callback,
Connection,
EthereumProvider,
EventHandler,
PendingPromise,
ProviderError,
RequestArguments,
JsonRpcResponse,
Payload
} from './types'
export declare namespace JsonRpc {
export { JsonRpcPayload as Payload, JsonRpcResponse as Response }
}
export type { EthereumProvider, RequestArguments, ProviderError }
class Provider extends EventEmitter implements EthereumProvider {
private readonly connection: Connection
private readonly eventHandlers: Record<string, EventHandler>
private readonly promises: Record<string, PendingPromise> = {}
private readonly attemptedSubscriptions: Set<string> = new Set()
private subscriptions: string[] = []
private networkVersion?: number
private manualChainId?: string
private providerChainId?: string
private checkConnectionRunning = false
private checkConnectionTimer?: NodeJS.Timer
private nextId = 1
connected = false
accounts: string[] = []
selectedAddress?: string = undefined
coinbase?: string = undefined
constructor (connection: Connection) {
super()
this.enable = this.enable.bind(this)
this.doSend = this.doSend.bind(this)
this.send = this.send.bind(this)
this.sendBatch = this.sendBatch.bind(this)
this.subscribe = this.subscribe.bind(this)
this.unsubscribe = this.unsubscribe.bind(this)
this.resumeSubscriptions = this.resumeSubscriptions.bind(this)
this.sendAsync = this.sendAsync.bind(this)
this.sendAsyncBatch = this.sendAsyncBatch.bind(this)
this.isConnected = this.isConnected.bind(this)
this.close = this.close.bind(this)
this.request = this.request.bind(this)
this.connection = connection
this.on('connect', this.resumeSubscriptions)
this.connection.on('connect', () => this.checkConnection(1000))
this.connection.on('close', () => {
this.connected = false
this.attemptedSubscriptions.clear()
this.emit('close')
this.emit('disconnect')
})
this.connection.on('payload', payload => {
const { id, method, error, result } = payload
if (typeof id !== 'undefined') {
if (this.promises[id]) { // Fulfill promise
const requestMethod = this.promises[id].method
if (requestMethod && ['eth_accounts', 'eth_requestAccounts'].includes(requestMethod)) {
const accounts = result || []
this.accounts = accounts
this.selectedAddress = accounts[0]
this.coinbase = accounts[0]
}
payload.error ? this.promises[id].reject(error) : this.promises[id].resolve(result)
delete this.promises[id]
}
} else if (method && method.indexOf('_subscription') > -1) { // Emit subscription result
// Events: connect, disconnect, chainChanged, chainsChanged, accountsChanged, assetsChanged, message
this.emit(payload.params.subscription, payload.params.result)
this.emit(method, payload.params) // Older EIP-1193
this.emit('message', { // Latest EIP-1193
type: payload.method,
data: {
subscription: payload.params.subscription,
result: payload.params.result
}
})
this.emit('data', payload) // Backwards Compatibility
}
})
this.on('newListener', event => {
if (Object.keys(this.eventHandlers).includes(event)) {
if (!this.attemptedSubscription(event) && this.connected) {
this.startSubscription(event)
if (event === 'networkChanged') {
console.warn('The networkChanged event is being deprecated, use chainChanged instead')
}
}
}
})
this.eventHandlers = {
networkChanged: netId => {
this.networkVersion = (typeof netId === 'string') ? parseInt(netId) : netId as number
this.emit('networkChanged', this.networkVersion)
},
chainChanged: chainId => {
this.providerChainId = chainId as string
if (!this.manualChainId) {
this.emit('chainChanged', chainId)
}
},
chainsChanged: chains => {
this.emit('chainsChanged', chains)
},
accountsChanged: (accounts) => {
this.selectedAddress = (accounts as string[])[0]
this.emit('accountsChanged', accounts)
},
assetsChanged: assets => {
this.emit('assetsChanged', assets)
}
}
}
get chainId () {
return this.manualChainId || this.providerChainId
}
async checkConnection (retryTimeout = 4000) {
if (this.checkConnectionRunning || this.connected) return
clearTimeout(this.checkConnectionTimer)
this.checkConnectionTimer = undefined
this.checkConnectionRunning = true
try {
this.networkVersion = await this.doSend('net_version', [], undefined, false)
this.providerChainId = await this.doSend('eth_chainId', [], undefined, false)
this.connected = true
} catch (e) {
this.checkConnectionTimer = setTimeout(() => this.checkConnection(), retryTimeout)
this.connected = false
} finally {
this.checkConnectionRunning = false
if (this.connected) {
this.emit('connect', { chainId: this.providerChainId })
}
}
}
private attemptedSubscription (event: string) {
return this.attemptedSubscriptions.has(event)
}
private setSubscriptionAttempted (event: string) {
this.attemptedSubscriptions.add(event)
}
async startSubscription (event: string) {
console.debug(`starting subscription for ${event} events`)
this.setSubscriptionAttempted(event)
try {
const eventId = await (this.subscribe('eth_subscribe', event)) as string
this.on(eventId, this.eventHandlers[event])
} catch (e) {
console.warn(`Unable to subscribe to ${event}`, e)
}
}
private resumeSubscriptions () {
Object.keys(this.eventHandlers).forEach(event => {
if (this.listenerCount(event) && !this.attemptedSubscription(event)) this.startSubscription(event)
})
}
async enable () {
const accounts: string[] = await this.doSend('eth_accounts')
if (accounts.length > 0) {
this.accounts = accounts
this.selectedAddress = accounts[0]
this.coinbase = accounts[0]
this.emit('enable')
return accounts
} else {
const err = new Error('User Denied Full Provider') as NodeJS.ErrnoException
err.code = '4001'
throw err
}
}
private doSend <T> (rawPayload: string | JsonRpcPayload, rawParams: readonly unknown[] = [], targetChain = this.manualChainId, waitForConnection = true): Promise<T> {
const sendFn = (resolve: (result: T) => void, reject: (err: Error) => void) => {
const method = (typeof rawPayload === 'object') ? rawPayload.method : rawPayload
const params = (typeof rawPayload === 'object') ? rawPayload.params : rawParams
const chainTarget = ((typeof rawPayload === 'object') && rawPayload.chainId) || targetChain
if (!method) {
return reject(new Error('Method is not a valid string.'))
}
try {
const payload = createPayload(method, params, this.nextId++, chainTarget)
this.promises[payload.id] = {
resolve: (result) => resolve(result as T),
reject,
method: payload.method
}
this.connection.send(payload)
} catch (e) {
reject(e as Error)
}
}
if (this.connected || !waitForConnection) {
return new Promise(sendFn)
}
return new Promise((resolve, reject) => {
const resolveSend = () => {
clearTimeout(disconnectTimer)
return resolve(new Promise(sendFn))
}
const disconnectTimer = setTimeout(() => {
this.off('connect', resolveSend)
reject(new Error('Not connected'))
}, 5000)
this.once('connect', resolveSend)
})
}
async send (methodOrPayload: string | JsonRpcPayload, callbackOrArgs: Callback<JsonRpcResponse> | unknown[]) { // Send can be clobbered, proxy sendPromise for backwards compatibility
if (
typeof methodOrPayload === 'string' &&
(!callbackOrArgs || Array.isArray(callbackOrArgs))
) {
const params = callbackOrArgs
return this.doSend(methodOrPayload, params)
}
if (
methodOrPayload &&
typeof methodOrPayload === 'object' &&
typeof callbackOrArgs === 'function'
) {
// a callback was passed to send(), forward everything to sendAsync()
const cb = callbackOrArgs as Callback<JsonRpcResponse>
return this.sendAsync(methodOrPayload, cb)
}
return this.request(methodOrPayload as JsonRpcPayload)
}
private sendBatch (requests: JsonRpcPayload[]): Promise<unknown[]> {
return Promise.all(requests.map(payload => {
return this.doSend(payload.method, payload.params)
}))
}
async subscribe (type: string, method: string, params = []) {
const id: string = await this.doSend(type, [method, ...params])
this.subscriptions.push(id)
return id
}
async unsubscribe (type: string, id: string) {
const success: boolean = await this.doSend<boolean>(type, [id])
if (success) {
this.subscriptions = this.subscriptions.filter(_id => _id !== id) // Remove subscription
this.removeAllListeners(id) // Remove listeners
return success
}
}
async sendAsync (rawPayload: JsonRpcPayload | JsonRpcPayload[], cb: Callback<JsonRpcResponse> | Callback<JsonRpcResponse[]>) { // Backwards Compatibility
if (!cb || typeof cb !== 'function') return new Error('Invalid or undefined callback provided to sendAsync')
if (!rawPayload) return cb(new Error('Invalid Payload'))
// sendAsync can be called with an array for batch requests used by web3.js 0.x
// this is not part of EIP-1193's backwards compatibility but we still want to support it
if (Array.isArray(rawPayload)) {
const payloads: JsonRpcPayload[] = rawPayload.map(p => ({ ...p, jsonrpc: '2.0' }))
const callback = cb as Callback<JsonRpcResponse[]>
return this.sendAsyncBatch(payloads, callback)
} else {
const payload: JsonRpcPayload = { ...(rawPayload as JsonRpcPayload), jsonrpc: '2.0' }
const callback = cb as Callback<JsonRpcResponse>
try {
const result = await this.doSend(payload.method, payload.params)
callback(null, { id: payload.id, jsonrpc: payload.jsonrpc, result })
} catch (e) {
callback(e as Error)
}
}
}
private async sendAsyncBatch (payloads: JsonRpcPayload[], cb: (err: Error | null, result?: JsonRpcResponse[]) => void) {
try {
const results = await this.sendBatch(payloads)
const result = results.map((entry, index) => {
return { id: payloads[index].id, jsonrpc: payloads[index].jsonrpc, result: entry }
})
cb(null, result)
} catch (e) {
cb(e as Error)
}
}
isConnected () { // Backwards Compatibility
return this.connected
}
close () {
if (this.connection && this.connection.close) this.connection.close()
this.off('connect', this.resumeSubscriptions)
this.connected = false
const error = new Error('Provider closed, subscription lost, please subscribe again.')
this.subscriptions.forEach(id => this.emit(id, error)) // Send Error objects to any open subscriptions
this.subscriptions = [] // Clear subscriptions
this.manualChainId = undefined
this.providerChainId = undefined
this.networkVersion = undefined
this.selectedAddress = undefined
this.coinbase = undefined
}
async request <T> (payload: Payload): Promise<T> {
return this.doSend<T>(payload.method, payload.params as unknown[], payload.chainId)
}
setChain (chainId: string | number) {
if (typeof chainId === 'number') chainId = '0x' + chainId.toString(16)
const chainChanged = (chainId !== this.chainId)
this.manualChainId = chainId
if (chainChanged) {
this.emit('chainChanged', this.chainId)
}
}
}
export default Provider