@tomo-inc/inject-providers 0.0.1

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.
@@ -0,0 +1,890 @@
1
+ import { EventEmitter } from "events";
2
+
3
+ import {
4
+ createIdRemapMiddleware,
5
+ JsonRpcEngine,
6
+ JsonRpcRequest,
7
+ JsonRpcResponse,
8
+ JsonRpcSuccess,
9
+ } from "json-rpc-engine";
10
+ import { createStreamMiddleware } from "json-rpc-middleware-stream";
11
+
12
+ import { getDappInfo } from "../utils/index";
13
+ import messages from "./messages";
14
+
15
+ import { EthereumRpcError, ethErrors } from "eth-rpc-errors";
16
+ import { toHex } from "viem";
17
+ import {
18
+ ConsoleLike,
19
+ createErrorMiddleware,
20
+ EMITTED_NOTIFICATIONS,
21
+ getRpcPromiseCallback,
22
+ logStreamDisconnectWarning,
23
+ Maybe,
24
+ NOOP,
25
+ } from "./utils";
26
+
27
+ import { ChainTypes, IProductInfo } from "../types";
28
+ import {
29
+ InitializeProviderOptions,
30
+ InpageProviderOptions,
31
+ InternalState,
32
+ RequestArguments,
33
+ SendSyncJsonRpcRequest,
34
+ UnvalidatedJsonRpcRequest,
35
+ WarningEventName,
36
+ } from "./type";
37
+
38
+ const chainType = ChainTypes.EVM;
39
+
40
+ export class EvmProvider extends EventEmitter {
41
+ private readonly _log: ConsoleLike;
42
+
43
+ private _state: InternalState;
44
+
45
+ private _rpcEngine: JsonRpcEngine;
46
+
47
+ public sendRequest: (chainType: string, data: any) => void;
48
+
49
+ public onResponse: any;
50
+
51
+ /**
52
+ * The chain ID of the currently connected Ethereum chain.
53
+ * See [chainId.network]{@link https://chainid.network} for more information.
54
+ */
55
+ public chainId: string | null;
56
+
57
+ /**
58
+ * The network ID of the currently connected Ethereum chain.
59
+ */
60
+ public networkVersion: string | null;
61
+
62
+ /**
63
+ * The user's currently selected Ethereum address.
64
+ * If null, is either locked or the user has not permitted any
65
+ * addresses to be viewed.
66
+ */
67
+ public selectedAddress: string | null;
68
+
69
+ /**
70
+ * Indicating that this provider is a provider.
71
+ */
72
+
73
+ public name: string;
74
+ public icon: string;
75
+
76
+ /**
77
+ * @param connectionStream - A Node.js duplex stream
78
+ * @param options - An options bag
79
+ * @param options.jsonRpcStreamName - The name of the internal JSON-RPC stream.
80
+ * @param options.logger - The logging API to use. Default: console
81
+ * @param options.maxEventListeners - The maximum number of event
82
+ * listeners. Default: 100
83
+ * @param options.shouldSendMetadata - Whether the provider should
84
+ * send page metadata. Default: true
85
+ */
86
+ constructor(
87
+ productInfo: IProductInfo,
88
+ {
89
+ logger = console,
90
+ maxEventListeners = 100,
91
+ shouldSendMetadata = true,
92
+ sendRequest,
93
+ onResponse,
94
+ }: InpageProviderOptions,
95
+ ) {
96
+ if (typeof maxEventListeners !== "number" || typeof shouldSendMetadata !== "boolean") {
97
+ throw new Error(messages.errors.invalidOptions(maxEventListeners, shouldSendMetadata));
98
+ }
99
+
100
+ validateLoggerObject(logger);
101
+
102
+ super();
103
+
104
+ this._log = logger;
105
+ this.name = productInfo.name;
106
+ this.icon = productInfo.icon;
107
+ this.sendRequest = sendRequest;
108
+ this.onResponse = onResponse;
109
+
110
+ this.setMaxListeners(maxEventListeners);
111
+
112
+ // private state
113
+ this._state = {
114
+ sentWarnings: {
115
+ // methods
116
+ enable: false,
117
+ experimentalMethods: false,
118
+ send: false,
119
+ // events
120
+ events: {
121
+ close: false,
122
+ data: false,
123
+ networkChanged: false,
124
+ notification: false,
125
+ },
126
+ },
127
+ accounts: null,
128
+ isConnected: false,
129
+ isUnlocked: false,
130
+ initialized: true,
131
+ isPermanentlyDisconnected: false,
132
+ };
133
+
134
+ // public state
135
+ this.selectedAddress = null;
136
+ this.networkVersion = null;
137
+ this.chainId = null;
138
+
139
+ // bind functions (to prevent consumers from making unbound calls)
140
+ this._handleAccountsChanged = this._handleAccountsChanged.bind(this);
141
+ this._handleConnect = this._handleConnect.bind(this);
142
+ this._handleChainChanged = this._handleChainChanged.bind(this);
143
+ this._handleDisconnect = this._handleDisconnect.bind(this);
144
+ this._handleStreamDisconnect = this._handleStreamDisconnect.bind(this);
145
+ this._handleUnlockStateChanged = this._handleUnlockStateChanged.bind(this);
146
+ this._sendSync = this._sendSync.bind(this);
147
+ this._rpcRequest = this._rpcRequest.bind(this);
148
+ this._warnOfDeprecation = this._warnOfDeprecation.bind(this);
149
+ this.enable = this.enable.bind(this);
150
+ this.connect = this.connect.bind(this);
151
+ this.disconnect = this.disconnect.bind(this);
152
+ this.request = this.request.bind(this);
153
+ this.send = this.send.bind(this);
154
+ this.sendAsync = this.sendAsync.bind(this);
155
+
156
+ // EIP-1193 connect
157
+ this.on("connect", () => {
158
+ this._state.isConnected = true;
159
+ });
160
+
161
+ const jsonRpcConnection = createStreamMiddleware();
162
+
163
+ // handle RPC requests via dapp-side rpc engine
164
+ const rpcEngine = new JsonRpcEngine();
165
+ rpcEngine.push(createIdRemapMiddleware());
166
+ rpcEngine.push(createErrorMiddleware(this._log));
167
+ rpcEngine.push(jsonRpcConnection.middleware as any);
168
+ this._rpcEngine = rpcEngine;
169
+
170
+ this._initializeState();
171
+
172
+ // handle JSON-RPC notifications
173
+ jsonRpcConnection.events.on("notification", (payload) => {
174
+ const { method, params } = payload;
175
+
176
+ if (method === "wallet_accountsChanged") {
177
+ this._handleAccountsChanged(params);
178
+ } else if (method === "wallet_unlockStateChanged") {
179
+ this._handleUnlockStateChanged(params);
180
+ } else if (method === "wallet_chainChanged") {
181
+ this._handleChainChanged(params);
182
+ } else if (EMITTED_NOTIFICATIONS.includes(method)) {
183
+ // deprecated
184
+ // emitted here because that was the original order
185
+ this.emit("data", payload);
186
+
187
+ this.emit("message", {
188
+ type: method,
189
+ data: params,
190
+ });
191
+
192
+ // deprecated
193
+ this.emit("notification", payload.params.result);
194
+ } else if (method === "WALLET_STREAM_FAILURE") {
195
+ console.error(messages.errors.permanentlyDisconnected());
196
+ }
197
+ });
198
+
199
+ // miscellanea
200
+ }
201
+
202
+ //====================
203
+ // Public Methods
204
+ //====================
205
+
206
+ /**
207
+ * Returns whether the provider can process RPC requests.
208
+ */
209
+ isConnected(): boolean {
210
+ return this._state.isConnected;
211
+ }
212
+
213
+ /**
214
+ * Submits an RPC request for the given method, with the given params.
215
+ * Resolves with the result of the method call, or rejects on error.
216
+ *
217
+ * @param args - The RPC request arguments.
218
+ * @param args.method - The RPC method name.
219
+ * @param args.params - The parameters for the RPC method.
220
+ * @returns A Promise that resolves with the result of the RPC method,
221
+ * or rejects if an error is encountered.
222
+ */
223
+ async request<T>(args: RequestArguments): Promise<Maybe<T>> {
224
+ if (!args || typeof args !== "object" || Array.isArray(args)) {
225
+ throw ethErrors.rpc.invalidRequest({
226
+ message: messages.errors.invalidRequestArgs(),
227
+ data: args,
228
+ });
229
+ }
230
+
231
+ const { method, params } = args;
232
+
233
+ if (typeof method !== "string" || method.length === 0) {
234
+ throw ethErrors.rpc.invalidRequest({
235
+ message: messages.errors.invalidRequestMethod(),
236
+ data: args,
237
+ });
238
+ }
239
+
240
+ if (method === "wallet_addEthereumChain") {
241
+ if (!Array.isArray(params) || params.length === 0) {
242
+ throw ethErrors.rpc.invalidRequest({
243
+ message: messages.errors.invalidRequestParams(),
244
+ data: args,
245
+ });
246
+ }
247
+ const network = params[0] as any;
248
+ if (!network || !network.chainName || !network.rpcUrls || !network.chainId || !network.nativeCurrency) {
249
+ throw ethErrors.rpc.invalidRequest({
250
+ message: messages.errors.invalidRequestParams(),
251
+ data: args,
252
+ });
253
+ }
254
+ }
255
+
256
+ if (method === "personal_sign") {
257
+ if (!Array.isArray(params) || params.length !== 2 || !params[0] || !params[1]) {
258
+ throw ethErrors.rpc.invalidRequest({
259
+ message: messages.errors.invalidRequestParams(),
260
+ data: args,
261
+ });
262
+ }
263
+ }
264
+
265
+ if (method === "wallet_watchAsset") {
266
+ const { type, options } = params as {
267
+ type: string;
268
+ options: {
269
+ address: string;
270
+ symbol: string;
271
+ decimals: number;
272
+ };
273
+ };
274
+ const isNotCompelete = !options || !options.address || !options.symbol || !options.decimals;
275
+ if (!type || isNotCompelete) {
276
+ throw ethErrors.rpc.invalidRequest({
277
+ message: messages.errors.invalidRequestParams(),
278
+ data: args,
279
+ });
280
+ }
281
+ }
282
+
283
+ if (params !== undefined && !Array.isArray(params) && (typeof params !== "object" || params === null)) {
284
+ throw ethErrors.rpc.invalidRequest({
285
+ message: messages.errors.invalidRequestParams(),
286
+ data: args,
287
+ });
288
+ }
289
+
290
+ const connectFuns: Record<string, boolean> = {
291
+ enable: true,
292
+ connect: true,
293
+ eth_accounts: true,
294
+ eth_requestAccounts: true,
295
+ };
296
+ // if (connectFuns[args.method] || args.method === "_wallet_getProviderState" || args.method === "disconnect") {
297
+ const dappInfo = await getDappInfo();
298
+ args = { ...args, ...{ dappInfo } };
299
+ // }
300
+
301
+ this.sendRequest(chainType, args);
302
+
303
+ return this.onResponse(args).then((res: any) => {
304
+ const { data, method } = res || {};
305
+
306
+ if (method === "_wallet_getProviderState") {
307
+ this._handleAccountsChanged(data?.accounts || [], true);
308
+ }
309
+ if (method === "wallet_revokePermissions") {
310
+ this._handleAccountsChanged([], true);
311
+ }
312
+ if (method in connectFuns && connectFuns[method]) {
313
+ this._handleAccountsChanged(data || [], true);
314
+ }
315
+
316
+ if (method === "wallet_switchEthereumChain" || method === "eth_chainId") {
317
+ this._handleChainChanged({ chainId: data, isConnected: true });
318
+ }
319
+
320
+ return data;
321
+ });
322
+ }
323
+
324
+ /**
325
+ * Submits an RPC request per the given JSON-RPC request object.
326
+ *
327
+ * @param payload - The RPC request object.
328
+ * @param cb - The callback function.
329
+ */
330
+ sendAsync(
331
+ payload: JsonRpcRequest<unknown>,
332
+ callback: (error: Error | null, result?: JsonRpcResponse<unknown>) => void,
333
+ ): void {
334
+ this._rpcRequest(payload, callback);
335
+ }
336
+
337
+ /**
338
+ * We override the following event methods so that we can warn consumers
339
+ * about deprecated events:
340
+ * addListener, on, once, prependListener, prependOnceListener
341
+ */
342
+
343
+ addListener(eventName: string, listener: (...args: unknown[]) => void) {
344
+ this._warnOfDeprecation(eventName);
345
+ return super.addListener(eventName, listener);
346
+ }
347
+
348
+ on(eventName: string, listener: (...args: unknown[]) => void) {
349
+ this._warnOfDeprecation(eventName);
350
+ return super.on(eventName, listener);
351
+ }
352
+
353
+ once(eventName: string, listener: (...args: unknown[]) => void) {
354
+ this._warnOfDeprecation(eventName);
355
+ return super.once(eventName, listener);
356
+ }
357
+
358
+ prependListener(eventName: string, listener: (...args: unknown[]) => void) {
359
+ this._warnOfDeprecation(eventName);
360
+ return super.prependListener(eventName, listener);
361
+ }
362
+
363
+ prependOnceListener(eventName: string, listener: (...args: unknown[]) => void) {
364
+ this._warnOfDeprecation(eventName);
365
+ return super.prependOnceListener(eventName, listener);
366
+ }
367
+
368
+ //====================
369
+ // Private Methods
370
+ //====================
371
+
372
+ /**
373
+ * Constructor helper.
374
+ * Populates initial state by calling 'getProviderState' and emits
375
+ * necessary events.
376
+ */
377
+ private async _initializeState() {
378
+ try {
379
+ const {
380
+ accounts = [],
381
+ chainId = "0x01",
382
+ isUnlocked = false,
383
+ isConnected = false,
384
+ } = (await this.request({
385
+ method: "wallet_getProviderState",
386
+ })) as {
387
+ accounts: string[];
388
+ chainId: string;
389
+ isUnlocked: boolean;
390
+ isConnected: boolean;
391
+ };
392
+
393
+ // indicate that we've connected, for EIP-1193 compliance
394
+ this.emit("connect", { chainId });
395
+
396
+ this._handleChainChanged({ chainId, isConnected });
397
+ this._handleUnlockStateChanged({ accounts, isUnlocked });
398
+ this._handleAccountsChanged(accounts);
399
+
400
+ window.addEventListener("message", (event: MessageEvent<any>) => {
401
+ const { data, type, method } = event.data;
402
+ if (type === "subscribeWalletEvents" && method) {
403
+ this.subscribeWalletEventsCallback({ data, method });
404
+ }
405
+ });
406
+ } catch (error) {
407
+ this._log.error(`${this.name}: Failed to get initial state.`, error);
408
+ } finally {
409
+ this._state.initialized = true;
410
+ this.emit("_initialized");
411
+ }
412
+
413
+ this.keepAlive();
414
+ }
415
+
416
+ private subscribeWalletEventsCallback = ({ method, data }: { method: string; data: any }) => {
417
+ if (method === "accountsChanged") {
418
+ const addresses = data?.[chainType] || [];
419
+ const accounts = addresses.map(({ address }: { address: string }) => address);
420
+ this._handleAccountsChanged(accounts, true);
421
+ }
422
+ if (method === "chainChanged") {
423
+ const { chainId, type }: { chainId: string; type: string } = data;
424
+ if (type.indexOf(`${chainType}:`) === 0) {
425
+ const chainIdHex = toHex(parseInt(chainId));
426
+ this._handleChainChanged({ chainId: chainIdHex, isConnected: true });
427
+ }
428
+ }
429
+ };
430
+
431
+ private keepAlive = () => {
432
+ this.request({
433
+ method: "keepAlive",
434
+ params: {},
435
+ }).then(() => {
436
+ setTimeout(() => {
437
+ this.keepAlive();
438
+ }, 1000);
439
+ });
440
+ };
441
+
442
+ /**
443
+ * Internal RPC method. Forwards requests to background via the RPC engine.
444
+ * Also remap ids inbound and outbound.
445
+ *
446
+ * @param payload - The RPC request object.
447
+ * @param callback - The consumer's callback.
448
+ */
449
+ private _rpcRequest(
450
+ payload: UnvalidatedJsonRpcRequest | UnvalidatedJsonRpcRequest[],
451
+ callback: (...args: any[]) => void,
452
+ ) {
453
+ let cb = callback;
454
+
455
+ if (!Array.isArray(payload)) {
456
+ if (!payload.jsonrpc) {
457
+ payload.jsonrpc = "2.0";
458
+ }
459
+
460
+ if (payload.method === "eth_accounts" || payload.method === "eth_requestAccounts") {
461
+ // handle accounts changing
462
+ cb = (err: Error, res: JsonRpcSuccess<string[]>) => {
463
+ this._handleAccountsChanged(res.result || [], payload.method === "eth_accounts");
464
+ callback(err, res);
465
+ };
466
+ }
467
+ return this._rpcEngine.handle(payload as JsonRpcRequest<unknown>, cb);
468
+ }
469
+ return this._rpcEngine.handle(payload as JsonRpcRequest<unknown>[], cb);
470
+ }
471
+
472
+ /**
473
+ * When the provider becomes connected, updates internal state and emits
474
+ * required events. Idempotent.
475
+ *
476
+ * @param chainId - The ID of the newly connected chain.
477
+ * @emits InpageProvider#connect
478
+ */
479
+ private _handleConnect(chainId: string) {
480
+ if (!this._state.isConnected) {
481
+ this._state.isConnected = true;
482
+ this.emit("connect", { chainId });
483
+ // this._log.debug(messages.info.connected(chainId));
484
+ }
485
+ }
486
+
487
+ /**
488
+ * When the provider becomes disconnected, updates internal state and emits
489
+ * required events. Idempotent with respect to the isRecoverable parameter.
490
+ *
491
+ * Error codes per the CloseEvent status codes as required by EIP-1193:
492
+ * https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes
493
+ *
494
+ * @param isRecoverable - Whether the disconnection is recoverable.
495
+ * @param errorMessage - A custom error message.
496
+ * @emits InpageProvider#disconnect
497
+ */
498
+ private _handleDisconnect(isRecoverable: boolean, errorMessage?: string) {
499
+ if (this._state.isConnected || (!this._state.isPermanentlyDisconnected && !isRecoverable)) {
500
+ this._state.isConnected = false;
501
+
502
+ let error;
503
+ if (isRecoverable) {
504
+ error = new EthereumRpcError(
505
+ 1013, // Try again later
506
+ errorMessage || messages.errors.disconnected(),
507
+ );
508
+ // this._log.debug(error);
509
+ } else {
510
+ error = new EthereumRpcError(
511
+ 1011, // Internal error
512
+ errorMessage || messages.errors.permanentlyDisconnected(),
513
+ );
514
+ // this._log.error(error);
515
+ this.chainId = null;
516
+ this.networkVersion = null;
517
+ this._state.accounts = null;
518
+ this.selectedAddress = null;
519
+ this._state.isUnlocked = false;
520
+ this._state.isPermanentlyDisconnected = true;
521
+ }
522
+
523
+ this.emit("disconnect", error);
524
+ this.emit("close", error); // deprecated
525
+ }
526
+ }
527
+
528
+ /**
529
+ * Called when connection is lost to critical streams.
530
+ *
531
+ * @emits InpageProvider#disconnect
532
+ */
533
+ private _handleStreamDisconnect(streamName: string, error: Error) {
534
+ logStreamDisconnectWarning(this._log, streamName, error, this);
535
+ this._handleDisconnect(false, error ? error?.message : undefined);
536
+ }
537
+
538
+ /**
539
+ * Upon receipt of a new chainId and networkVersion, emits corresponding
540
+ * events and sets relevant public state.
541
+ * Does nothing if neither the chainId nor the networkVersion are different
542
+ * from existing values.
543
+ *
544
+ * @emits InpageProvider#chainChanged
545
+ * @param networkInfo - An object with network info.
546
+ * @param networkInfo.chainId - The latest chain ID.
547
+ * @param networkInfo.networkVersion - The latest network ID.
548
+ */
549
+ private _handleChainChanged({ chainId, isConnected }: { chainId?: string; isConnected?: boolean } = {}) {
550
+ if (!chainId || typeof chainId !== "string" || !chainId.startsWith("0x")) {
551
+ // this._log.error(`${this.name}: Received invalid network parameters.`, {
552
+ // chainId,
553
+ // });
554
+ return;
555
+ }
556
+
557
+ if (!isConnected) {
558
+ this._handleDisconnect(true);
559
+ } else {
560
+ this._handleConnect(chainId);
561
+
562
+ if (chainId !== this.chainId) {
563
+ this.chainId = chainId;
564
+ if (this._state.initialized) {
565
+ this.emit("chainChanged", this.chainId);
566
+ }
567
+ }
568
+ }
569
+ }
570
+
571
+ /**
572
+ * Called when accounts may have changed. Diffs the new accounts value with
573
+ * the current one, updates all state as necessary, and emits the
574
+ * accountsChanged event.
575
+ *
576
+ * @param accounts - The new accounts value.
577
+ * @param isEthAccounts - Whether the accounts value was returned by
578
+ * a call to eth_accounts.
579
+ */
580
+ private _handleAccountsChanged(accounts: unknown[], isEthAccounts = false): void {
581
+ let _accounts = accounts;
582
+
583
+ if (!Array.isArray(accounts)) {
584
+ this._log.error(`${this.name}: Received invalid accounts parameter.`, accounts);
585
+ _accounts = [];
586
+ }
587
+
588
+ for (const account of accounts) {
589
+ if (typeof account !== "string") {
590
+ this._log.error(`${this.name}: Received non-string account.`, accounts);
591
+ _accounts = [];
592
+ break;
593
+ }
594
+ }
595
+
596
+ // emit accountsChanged if anything about the accounts array has changed
597
+ if (this._state.accounts !== _accounts) {
598
+ // we should always have the correct accounts even before eth_accounts
599
+ // returns
600
+ if (isEthAccounts && this._state.accounts !== null) {
601
+ // this._log.error(
602
+ // `${this.name}: 'eth_accounts' unexpectedly updated accounts. Please report this bug.`,
603
+ // _accounts,
604
+ // );
605
+ }
606
+
607
+ this._state.accounts = _accounts as string[];
608
+
609
+ // handle selectedAddress
610
+ if (this.selectedAddress !== _accounts[0]) {
611
+ this.selectedAddress = (_accounts[0] as string) || null;
612
+ }
613
+
614
+ // finally, after all state has been updated, emit the event
615
+ if (this._state.initialized) {
616
+ this.emit("accountsChanged", _accounts);
617
+ }
618
+ }
619
+ }
620
+
621
+ /**
622
+ * Upon receipt of a new isUnlocked state, sets relevant public state.
623
+ * Calls the accounts changed handler with the received accounts, or an empty
624
+ * array.
625
+ *
626
+ * Does nothing if the received value is equal to the existing value.
627
+ * There are no lock/unlock events.
628
+ *
629
+ * @param opts - Options bag.
630
+ * @param opts.accounts - The exposed accounts, if any.
631
+ * @param opts.isUnlocked - The latest isUnlocked value.
632
+ */
633
+ private _handleUnlockStateChanged({ accounts, isUnlocked }: { accounts?: string[]; isUnlocked?: boolean } = {}) {
634
+ if (typeof isUnlocked !== "boolean") {
635
+ this._log.error(`${this.name}: Received invalid isUnlocked parameter.`);
636
+ return;
637
+ }
638
+
639
+ if (isUnlocked !== this._state.isUnlocked) {
640
+ this._state.isUnlocked = isUnlocked;
641
+ this._handleAccountsChanged(accounts || []);
642
+ }
643
+ }
644
+
645
+ /**
646
+ * Warns of deprecation for the given event, if applicable.
647
+ */
648
+ private _warnOfDeprecation(eventName: string): void {
649
+ const events = this._state.sentWarnings.events;
650
+ if (events[eventName as WarningEventName] === false) {
651
+ this._log.warn(messages.warnings.events[eventName as WarningEventName]);
652
+ events[eventName as WarningEventName] = true;
653
+ }
654
+ }
655
+
656
+ //====================
657
+ // Deprecated Methods
658
+ //====================
659
+
660
+ /**
661
+ * Equivalent to: ethereum.request('eth_requestAccounts')
662
+ *
663
+ * @deprecated Use request({ method: 'eth_requestAccounts' }) instead.
664
+ * @returns A promise that resolves to an array of addresses.
665
+ */
666
+ enable(): Promise<string[]> {
667
+ return this.connect();
668
+ }
669
+
670
+ setConnectedStatus({ connected, address }: { connected: boolean; address: string[] }) {
671
+ this._state.isConnected = connected;
672
+ this._state.accounts = address;
673
+ }
674
+
675
+ connect(): Promise<string[]> {
676
+ if (!this._state.sentWarnings.enable) {
677
+ this._log.warn(messages.warnings.enableDeprecation);
678
+ this._state.sentWarnings.enable = true;
679
+ }
680
+ return new Promise<string[]>((resolve, reject) => {
681
+ if (this._state.isConnected) {
682
+ resolve(this._state.accounts as string[]);
683
+ }
684
+ try {
685
+ this.request<string[]>({
686
+ method: "eth_requestAccounts",
687
+ }).then((accounts) => {
688
+ if (accounts) {
689
+ this.emit("connect", {});
690
+ resolve(accounts as string[]);
691
+ } else {
692
+ reject(new Error("No accounts returned"));
693
+ }
694
+ });
695
+ } catch (error) {
696
+ reject(error);
697
+ }
698
+ });
699
+ }
700
+
701
+ disconnect(): Promise<boolean> {
702
+ return new Promise<boolean>((resolve, reject) => {
703
+ try {
704
+ (async () => {
705
+ const { isConnected } = (await this.request({
706
+ method: "wallet_revokePermissions",
707
+ })) as {
708
+ isConnected: boolean;
709
+ };
710
+ if (!isConnected) {
711
+ this._handleDisconnect(true);
712
+ this.emit("disconnect", null);
713
+ }
714
+ resolve(!isConnected);
715
+ })();
716
+ } catch (error) {
717
+ reject(error);
718
+ }
719
+ });
720
+ }
721
+
722
+ /**
723
+ * Submits an RPC request for the given method, with the given params.
724
+ *
725
+ * @deprecated Use "request" instead.
726
+ * @param method - The method to request.
727
+ * @param params - Any params for the method.
728
+ * @returns A Promise that resolves with the JSON-RPC response object for the
729
+ * request.
730
+ */
731
+ send<T>(method: string, params?: T[]): Promise<JsonRpcResponse<T>>;
732
+
733
+ /**
734
+ * Submits an RPC request per the given JSON-RPC request object.
735
+ *
736
+ * @deprecated Use "request" instead.
737
+ * @param payload - A JSON-RPC request object.
738
+ * @param callback - An error-first callback that will receive the JSON-RPC
739
+ * response object.
740
+ */
741
+ send<T>(payload: JsonRpcRequest<unknown>, callback: (error: Error | null, result?: JsonRpcResponse<T>) => void): void;
742
+
743
+ /**
744
+ * Accepts a JSON-RPC request object, and synchronously returns the cached result
745
+ * for the given method. Only supports 4 specific RPC methods.
746
+ *
747
+ * @deprecated Use "request" instead.
748
+ * @param payload - A JSON-RPC request object.
749
+ * @returns A JSON-RPC response object.
750
+ */
751
+ send<T>(payload: SendSyncJsonRpcRequest): JsonRpcResponse<T>;
752
+
753
+ send(methodOrPayload: unknown, callbackOrArgs?: unknown): unknown {
754
+ if (!this._state.sentWarnings.send) {
755
+ this._log.warn(messages.warnings.sendDeprecation);
756
+ this._state.sentWarnings.send = true;
757
+ }
758
+
759
+ if (typeof methodOrPayload === "string" && (!callbackOrArgs || Array.isArray(callbackOrArgs))) {
760
+ return new Promise((resolve, reject) => {
761
+ try {
762
+ this._rpcRequest(
763
+ { method: methodOrPayload, params: callbackOrArgs },
764
+ getRpcPromiseCallback(resolve, reject, false),
765
+ );
766
+ } catch (error) {
767
+ reject(error);
768
+ }
769
+ });
770
+ } else if (methodOrPayload && typeof methodOrPayload === "object" && typeof callbackOrArgs === "function") {
771
+ return this._rpcRequest(
772
+ methodOrPayload as JsonRpcRequest<unknown>,
773
+ callbackOrArgs as (...args: unknown[]) => void,
774
+ );
775
+ }
776
+ return this._sendSync(methodOrPayload as SendSyncJsonRpcRequest);
777
+ }
778
+
779
+ /**
780
+ * Internal backwards compatibility method, used in send.
781
+ *
782
+ * @deprecated
783
+ */
784
+ private _sendSync(payload: SendSyncJsonRpcRequest) {
785
+ let result;
786
+ switch (payload.method) {
787
+ case "eth_accounts":
788
+ result = this.selectedAddress ? [this.selectedAddress] : [];
789
+ break;
790
+
791
+ case "eth_coinbase":
792
+ result = this.selectedAddress || null;
793
+ break;
794
+
795
+ case "eth_uninstallFilter":
796
+ this._rpcRequest(payload, NOOP);
797
+ result = true;
798
+ break;
799
+
800
+ case "net_version":
801
+ result = this.networkVersion || null;
802
+ break;
803
+
804
+ default:
805
+ throw new Error(messages.errors.unsupportedSync(payload.method));
806
+ }
807
+
808
+ return {
809
+ id: payload.id,
810
+ jsonrpc: payload.jsonrpc,
811
+ result,
812
+ };
813
+ }
814
+ }
815
+
816
+ function validateLoggerObject(logger: ConsoleLike): void {
817
+ if (logger !== console) {
818
+ if (typeof logger === "object") {
819
+ const methodKeys = ["log", "warn", "error", "debug", "info", "trace"];
820
+ for (const key of methodKeys) {
821
+ if (typeof logger[key as keyof ConsoleLike] !== "function") {
822
+ throw new Error(messages.errors.invalidLoggerMethod(key));
823
+ }
824
+ }
825
+ return;
826
+ }
827
+ throw new Error(messages.errors.invalidLoggerObject());
828
+ }
829
+ }
830
+
831
+ /**
832
+ * Initializes a InpageProvider and (optionally) assigns it as window.ethereum.
833
+ *
834
+ * @param options - An options bag.
835
+ * @param options.connectionStream - A Node.js stream.
836
+ * @param options.jsonRpcStreamName - The name of the internal JSON-RPC stream.
837
+ * @param options.maxEventListeners - The maximum number of event listeners.
838
+ * @param options.shouldSendMetadata - Whether the provider should send page metadata.
839
+ * @param options.shouldSetOnWindow - Whether the provider should be set as window.ethereum.
840
+ * @param options.shouldShimWeb3 - Whether a window.web3 shim should be injected.
841
+ * @returns The initialized provider (whether set or not).
842
+ */
843
+ export function initializeEvmProvider(
844
+ productInfo: IProductInfo,
845
+ {
846
+ jsonRpcStreamName,
847
+ logger = console,
848
+ maxEventListeners = 100,
849
+ shouldSendMetadata = true,
850
+ shouldSetOnWindow = true,
851
+ shouldShimWeb3 = false,
852
+ sendRequest,
853
+ onResponse,
854
+ }: InitializeProviderOptions,
855
+ ): EvmProvider {
856
+ let provider = new EvmProvider(productInfo, {
857
+ jsonRpcStreamName,
858
+ logger,
859
+ maxEventListeners,
860
+ shouldSendMetadata,
861
+ sendRequest,
862
+ onResponse,
863
+ });
864
+
865
+ provider = new Proxy(provider, {
866
+ // some common libraries, e.g. web3@1.x, mess with our API
867
+ deleteProperty: () => true,
868
+ });
869
+
870
+ if (shouldSetOnWindow) {
871
+ setGlobalProvider(provider);
872
+ }
873
+
874
+ if (shouldShimWeb3) {
875
+ // shimWeb3(provider, logger);
876
+ }
877
+
878
+ return provider;
879
+ }
880
+
881
+ /**
882
+ * Sets the given provider instance as window.ethereum and dispatches the
883
+ * 'ethereum#initialized' event on window.
884
+ *
885
+ * @param providerInstance - The provider instance.
886
+ */
887
+ export function setGlobalProvider(providerInstance: EvmProvider): void {
888
+ (window as Record<string, any>).ethereum = providerInstance;
889
+ window.dispatchEvent(new Event("ethereum#initialized"));
890
+ }