@web3auth/ws-embed
Version:
Embed script
289 lines (285 loc) • 9.77 kB
JavaScript
'use strict';
var _objectSpread = require('@babel/runtime/helpers/objectSpread2');
var _defineProperty = require('@babel/runtime/helpers/defineProperty');
var baseControllers = require('@toruslabs/base-controllers');
var auth = require('@web3auth/auth');
var dequal = require('fast-deep-equal');
var baseProvider = require('./baseProvider.js');
var constants = require('./constants.js');
var loglevel = require('./loglevel.js');
var messages = require('./messages.js');
var utils = require('./utils.js');
/**
* @param connectionStream - A Node.js duplex stream
* @param opts - An options bag
*/
class TorusInPageProvider extends baseProvider {
constructor(connectionStream, {
maxEventListeners = 100,
jsonRpcStreamName = "provider"
}) {
super(connectionStream, {
maxEventListeners,
jsonRpcStreamName
});
// private state
/**
* The chain ID of the currently connected EVM chain.
* See [chainId.network]{@link https://chainid.network} for more information.
*/
_defineProperty(this, "chainId", void 0);
/**
* The user's currently selected EVM address.
* If null, Torus is either locked or the user has not permitted any
* addresses to be viewed.
*/
_defineProperty(this, "selectedAddress", void 0);
_defineProperty(this, "tryWindowHandle", void 0);
this.state = _objectSpread({}, TorusInPageProvider.defaultState);
// public state
this.selectedAddress = null;
this.chainId = null;
this.handleAccountsChanged = this.handleAccountsChanged.bind(this);
this.handleChainChanged = this.handleChainChanged.bind(this);
this.handleUnlockStateChanged = this.handleUnlockStateChanged.bind(this);
// setup own event listeners
// EIP-1193 connect
this.on("connect", () => {
this.state.isConnected = true;
});
const jsonRpcNotificationHandler = payload => {
const {
method,
params
} = payload;
if (method === baseControllers.PROVIDER_NOTIFICATIONS.ACCOUNTS_CHANGED) {
this.handleAccountsChanged(params);
} else if (method === baseControllers.PROVIDER_NOTIFICATIONS.UNLOCK_STATE_CHANGED) {
this.handleUnlockStateChanged(params);
} else if (method === baseControllers.PROVIDER_NOTIFICATIONS.CHAIN_CHANGED) {
this.handleChainChanged(params);
} else if (utils.EMITTED_NOTIFICATIONS.includes(method)) {
this.emit("data", payload);
this.emit("notification", params.result);
this.emit("message", {
type: method,
data: params
});
}
};
// json rpc notification listener
this.jsonRpcConnectionEvents.on("notification", jsonRpcNotificationHandler);
}
/**
* Returns whether the inpage provider is connected to Torus.
*/
isConnected() {
return this.state.isConnected;
}
// Private Methods
//= ===================
/**
* Constructor helper.
* Populates initial state by calling 'wallet_getProviderState' and emits
* necessary events.
*/
async initializeState() {
try {
const {
accounts,
chainId,
isUnlocked
} = await this.request({
method: baseControllers.PROVIDER_JRPC_METHODS.GET_PROVIDER_STATE,
params: []
});
// indicate that we've connected, for EIP-1193 compliance
this.emit("connect", {
chainId
});
this.handleChainChanged({
chainId
});
this.handleUnlockStateChanged({
accounts,
isUnlocked
});
this.handleAccountsChanged(accounts);
} catch (error) {
loglevel.error("WsEmbed: Failed to get initial state. Please report this bug.", error);
} finally {
loglevel.info("initialized provider state");
this.state.initialized = true;
}
}
/**
* Internal RPC method. Forwards requests to background via the RPC engine.
* Also remap ids inbound and outbound
*/
rpcRequest(payload, callback, isInternal = false) {
let cb = callback;
const _payload = payload;
if (!Array.isArray(_payload)) {
if (!_payload.jsonrpc) {
_payload.jsonrpc = "2.0";
}
if ([constants.EVM_METHOD_TYPES.GET_ACCOUNTS, constants.EVM_METHOD_TYPES.ETH_REQUEST_ACCOUNTS, constants.SOLANA_METHOD_TYPES.SOLANA_REQUEST_ACCOUNTS, constants.SOLANA_METHOD_TYPES.GET_ACCOUNTS].includes(_payload.method)) {
// handle accounts changing
cb = (err, res) => {
this.handleAccountsChanged(res.result || [], _payload.method === "eth_accounts", isInternal);
callback(err, res);
};
} else if (_payload.method === "wallet_getProviderState") {
this.rpcEngine.handle(payload, cb);
return;
}
}
this.tryWindowHandle(_payload, cb);
}
/**
* When the provider becomes connected, updates internal state and emits
* required events. Idempotent.
*
* @param chainId - The ID of the newly connected chain.
* emits TorusInpageProvider#connect
*/
handleConnect(chainId) {
if (!this.state.isConnected) {
this.state.isConnected = true;
this.emit("connect", {
chainId
});
loglevel.debug(messages.info.connected(chainId));
}
}
/**
* When the provider becomes disconnected, updates internal state and emits
* required events. Idempotent with respect to the isRecoverable parameter.
*
* Error codes per the CloseEvent status codes as required by EIP-1193:
* https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes
*
* @param isRecoverable - Whether the disconnection is recoverable.
* @param errorMessage - A custom error message.
* emits TorusInpageProvider#disconnect
*/
handleDisconnect(isRecoverable, errorMessage) {
if (this.state.isConnected || !this.state.isPermanentlyDisconnected && !isRecoverable) {
this.state.isConnected = false;
let error;
if (isRecoverable) {
error = new auth.EthereumProviderError(1013,
// Try again later
errorMessage || messages.errors.disconnected());
loglevel.debug(error);
} else {
error = new auth.EthereumProviderError(1011,
// Internal error
errorMessage || messages.errors.permanentlyDisconnected());
loglevel.error(error);
this.chainId = null;
this.state.accounts = null;
this.selectedAddress = null;
this.state.isUnlocked = false;
this.state.isPermanentlyDisconnected = true;
}
this.emit("disconnect", error);
}
}
/**
* Called when accounts may have changed.
*/
handleAccountsChanged(accounts, isEthAccounts = false, isInternal = false) {
// defensive programming
let finalAccounts = accounts;
if (!Array.isArray(finalAccounts)) {
loglevel.error("WsEmbed: Received non-array accounts parameter. Please report this bug.", finalAccounts);
finalAccounts = [];
}
for (const account of accounts) {
if (typeof account !== "string") {
loglevel.error("WsEmbed: Received non-string account. Please report this bug.", accounts);
finalAccounts = [];
break;
}
}
// emit accountsChanged if anything about the accounts array has changed
if (!dequal(this.state.accounts, finalAccounts)) {
// we should always have the correct accounts even before eth_accounts
// returns, except in cases where isInternal is true
if (isEthAccounts && Array.isArray(this.state.accounts) && this.state.accounts.length > 0 && !isInternal) {
loglevel.error('WsEmbed: "eth_accounts" unexpectedly updated accounts. Please report this bug.', finalAccounts);
}
this.state.accounts = finalAccounts;
this.emit("accountsChanged", finalAccounts);
}
// handle selectedAddress
if (this.selectedAddress !== finalAccounts[0]) {
this.selectedAddress = finalAccounts[0] || null;
}
}
/**
* Upon receipt of a new chainId and networkVersion, emits corresponding
* events and sets relevant public state.
* Does nothing if neither the chainId nor the networkVersion are different
* from existing values.
*
* emits TorusInpageProvider#chainChanged
* @param networkInfo - An object with network info.
*/
handleChainChanged({
chainId
} = {}) {
if (!chainId) {
loglevel.error("WsEmbed: Received invalid network parameters. Please report this bug.", {
chainId
});
return;
}
if (chainId === "loading") {
this.handleDisconnect(true);
} else {
this.handleConnect(chainId);
if (chainId !== this.chainId) {
this.chainId = chainId;
if (this.state.initialized) {
this.emit("chainChanged", this.chainId);
}
}
}
}
/**
* Upon receipt of a new isUnlocked state, sets relevant public state.
* Calls the accounts changed handler with the received accounts, or an empty
* array.
*
* Does nothing if the received value is equal to the existing value.
* There are no lock/unlock events.
*
* @param opts - Options bag.
*/
handleUnlockStateChanged({
accounts,
isUnlocked
} = {}) {
if (typeof isUnlocked !== "boolean") {
loglevel.error("WsEmbed: Received invalid isUnlocked parameter. Please report this bug.", {
isUnlocked
});
return;
}
if (isUnlocked !== this.state.isUnlocked) {
this.state.isUnlocked = isUnlocked;
this.handleAccountsChanged(accounts || []);
}
}
}
_defineProperty(TorusInPageProvider, "defaultState", {
accounts: null,
isConnected: false,
isUnlocked: false,
initialized: false,
isPermanentlyDisconnected: false,
hasEmittedConnection: false
});
module.exports = TorusInPageProvider;