Skip to content

Commit

Permalink
Move remote-specific initialization logic to StreamProvider
Browse files Browse the repository at this point in the history
  • Loading branch information
rekmarks committed Jun 5, 2022
1 parent 7ac97e1 commit cf3be4b
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 69 deletions.
74 changes: 39 additions & 35 deletions src/BaseProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,14 @@ export interface BaseProviderState {
}

/**
* An abstract class implementing the EIP-1193 interface. The class is abstract
* because the internal `_rpcEngine` needs to be wired up with a middleware that
* hands off JSON-RPC requests.
* An abstract class implementing the EIP-1193 interface. Implementers must:
*
* 1. At initialization, push a middleware to the internal `_rpcEngine` that
* hands off requests to the server and receives responses in return.
* 2. At initialization, retrieve initial state and call
* {@link BaseProvider._initialize} **once**.
* 3. Ensure that the provider's state is synchronized with the wallet.
* 4. Ensure that notifications are received and emitted as appropriate.
*/
export default abstract class BaseProvider extends SafeEventEmitter {
protected readonly _log: ConsoleLike;
Expand Down Expand Up @@ -101,16 +106,16 @@ export default abstract class BaseProvider extends SafeEventEmitter {

this.setMaxListeners(maxEventListeners);

// private state
// Private state
this._state = {
...BaseProvider._defaultState,
};

// public state
// Public state
this.selectedAddress = null;
this.chainId = null;

// bind functions to prevent consumers from making unbound calls
// Bind functions to prevent consumers from making unbound calls
this._handleAccountsChanged = this._handleAccountsChanged.bind(this);
this._handleConnect = this._handleConnect.bind(this);
this._handleChainChanged = this._handleChainChanged.bind(this);
Expand All @@ -119,20 +124,19 @@ export default abstract class BaseProvider extends SafeEventEmitter {
this._rpcRequest = this._rpcRequest.bind(this);
this.request = this.request.bind(this);

// setup own event listeners

// EIP-1193 connect
// EIP-1193 connect, emitted in _initialize
this.on('connect', () => {
this._state.isConnected = true;
});

// handle RPC requests via dapp-side rpc engine
// Handle RPC requests via dapp-side RPC engine.
//
// ATTN: Implementers must push middleware that hands off requests to
// the server.
const rpcEngine = new JsonRpcEngine();
rpcEngine.push(createIdRemapMiddleware());
rpcEngine.push(createErrorMiddleware(this._log));
this._rpcEngine = rpcEngine;

this._initializeState();
}

//====================
Expand Down Expand Up @@ -197,37 +201,37 @@ export default abstract class BaseProvider extends SafeEventEmitter {
//====================

/**
* Constructor helper.
* Populates initial state by calling 'metamask_getProviderState' and emits
* necessary events.
* Sets initial state if provided and marks this provider as initialized.
* Throws if called more than once.
*
* @param initialState - The provider's initial state.
* @emits BaseProvider#_initialized
*/
private async _initializeState() {
try {
const { accounts, chainId, isUnlocked, networkVersion } =
(await this.request({
method: 'metamask_getProviderState',
})) as {
accounts: string[];
chainId: string;
isUnlocked: boolean;
networkVersion: string;
};
protected _initialize(initialState?: {
accounts: string[];
chainId: string;
isUnlocked: boolean;
networkVersion: string;
}) {
if (this._state.initialized === true) {
throw new Error('Provider already initialized.');
}

// indicate that we've connected, for EIP-1193 compliance
if (initialState) {
const { accounts, chainId, isUnlocked, networkVersion } = initialState;

// EIP-1193 connect
this.emit('connect', { chainId });

this._handleChainChanged({ chainId, networkVersion });
this._handleUnlockStateChanged({ accounts, isUnlocked });
this._handleAccountsChanged(accounts);
} catch (error) {
this._log.error(
'MetaMask: Failed to get initial state. Please report this bug.',
error,
);
} finally {
this._state.initialized = true;
this.emit('_initialized');
}

// Mark provider as initialized regardless of whether initial state was
// retrieved.
this._state.initialized = true;
this.emit('_initialized');
}

/**
Expand Down
75 changes: 41 additions & 34 deletions src/StreamProvider.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import type { Duplex } from 'stream';
import type { EventEmitter } from 'events';
import ObjectMultiplex from '@metamask/object-multiplex';
import SafeEventEmitter from '@metamask/safe-event-emitter';
import { duplex as isDuplex } from 'is-stream';
import type { JsonRpcMiddleware } from 'json-rpc-engine';
import { createStreamMiddleware } from 'json-rpc-middleware-stream';
import pump from 'pump';
import messages from './messages';
import { ConsoleLike, EMITTED_NOTIFICATIONS } from './utils';
import { EMITTED_NOTIFICATIONS } from './utils';
import BaseProvider, { BaseProviderOptions } from './BaseProvider';

export interface StreamProviderOptions extends BaseProviderOptions {
Expand Down Expand Up @@ -56,7 +55,7 @@ export default class StreamProvider extends BaseProvider {
// Bind functions to prevent consumers from making unbound calls
this._handleStreamDisconnect = this._handleStreamDisconnect.bind(this);

// setup connectionStream multiplexing
// Set up connectionStream multiplexing
const mux = new ObjectMultiplex();
pump(
connectionStream,
Expand All @@ -65,8 +64,7 @@ export default class StreamProvider extends BaseProvider {
this._handleStreamDisconnect.bind(this, 'MetaMask'),
);

// setup RPC connection

// Set up RPC connection
this._jsonRpcConnection = createStreamMiddleware();
pump(
this._jsonRpcConnection.stream,
Expand All @@ -78,7 +76,10 @@ export default class StreamProvider extends BaseProvider {
// Wire up the JsonRpcEngine to the JSON-RPC connection stream
this._rpcEngine.push(this._jsonRpcConnection.middleware);

// handle JSON-RPC notifications
// Set initial state
this._initializeAsync();

// Handle JSON-RPC notifications
this._jsonRpcConnection.events.on('notification', (payload) => {
const { method, params } = payload;
if (method === 'metamask_accountsChanged') {
Expand All @@ -105,37 +106,43 @@ export default class StreamProvider extends BaseProvider {
//====================

/**
* Called when connection is lost to critical streams.
* Constructor helper. Calls `metamask_getProviderState` and passes the result
* to {@link BaseProvider._initialize}. Logs an error if getting initial state
* fails.
*/
private async _initializeAsync() {
let initialState: Parameters<BaseProvider['_initialize']>[0];

try {
initialState = (await this.request({
method: 'metamask_getProviderState',
})) as Parameters<BaseProvider['_initialize']>[0];
} catch (error) {
this._log.error(
'MetaMask: Failed to get initial state. Please report this bug.',
error,
);
}
this._initialize(initialState);
}

/**
* Called when connection is lost to critical streams. Emits an 'error' event
* from the provider with the error message and stack if present.
*
* @emits MetamaskInpageProvider#disconnect
*/
protected _handleStreamDisconnect(streamName: string, error: Error) {
logStreamDisconnectWarning(this._log, streamName, error, this);
this._handleDisconnect(false, error ? error.message : undefined);
}
}
private _handleStreamDisconnect(streamName: string, error: Error) {
let warningMsg = `MetaMask: Lost connection to "${streamName}".`;
if (error?.stack) {
warningMsg += `\n${error.stack}`;
}

/**
* Logs a stream disconnection error. Emits an 'error' if given an
* EventEmitter that has listeners for the 'error' event.
*
* @param log - The logging API to use.
* @param remoteLabel - The label of the disconnected stream.
* @param error - The associated error to log.
* @param emitter - The logging API to use.
*/
function logStreamDisconnectWarning(
log: ConsoleLike,
remoteLabel: string,
error: Error,
emitter: EventEmitter,
): void {
let warningMsg = `MetaMask: Lost connection to "${remoteLabel}".`;
if (error?.stack) {
warningMsg += `\n${error.stack}`;
}
log.warn(warningMsg);
if (emitter && emitter.listenerCount('error') > 0) {
emitter.emit('error', warningMsg);
this._log.warn(warningMsg);
if (this.listenerCount('error') > 0) {
this.emit('error', warningMsg);
}

this._handleDisconnect(false, error ? error.message : undefined);
}
}

0 comments on commit cf3be4b

Please sign in to comment.