UNPKG

@web3auth/ws-embed

Version:

Embed script

289 lines (285 loc) 9.77 kB
'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;