UNPKG

@web3-wallet/core

Version:
415 lines (414 loc) 15.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Connector = void 0; const createWalletStore_1 = require("./createWalletStore"); const types_1 = require("./types"); const utils_1 = require("./utils"); class Connector { /** * * @param walletName - {@link Connector#WalletName} * @param actions - {@link WalletStoreActions} */ constructor(options) { const { store, actions } = (0, createWalletStore_1.createWalletStoreAndActions)(); this.store = store; this.actions = actions; this.options = options; } /** * * ProviderNoFoundError should be thrown in the following cases * 1. detectProvider failed to retrieve the provider from a host environment. * 2. calling functions that requires a provider, but we have been unable to retrieve the provider */ // protected providerNotFoundError: ProviderNoFoundError; get providerNotFoundError() { return new types_1.ProviderNoFoundError(`${this.walletName} provider not found`); } /** * Detect provider in the host environment. * * @param providerFilter - providerFilter is provided the detected provider as it's input * and providerFilter returns a boolean to indicated wether the detected provider can be used. * 1. detectProvider should throw the ProviderNoFoundError if providerFilter returns false * 2. detectProvider should throw the ProviderNoFoundError if providerFilter returns false * * detectProvider is internally called by {@link Connector#lazyInitialize} * * @return Promise<p> - * 1. resolve with the provider if the detection succeeded. * 2. reject with an ProviderNotFoundError if it failed to retrieve the provider from the host environment. */ async detectProvider(providerFilter, options) { if (this.provider) return this.provider; const m = await Promise.resolve().then(() => __importStar(require('@web3-wallet/detect-provider'))); const injectedProvider = (await m.detectProvider(options ?? this.options?.detectProviderOptions)); if (!injectedProvider) throw this.providerNotFoundError; let provider = injectedProvider; providerFilter = providerFilter ?? this.options?.providerFilter ?? (() => true); /** * handle the case when e.g. metamask and coinbase wallet are both installed * */ if (injectedProvider.providers?.length) { provider = injectedProvider.providers?.find(providerFilter); } else { provider = provider && providerFilter(provider) ? provider : undefined; } if (!provider) { throw this.providerNotFoundError; } this.provider = provider; return provider; } /** * `lazyInitialize` does the following two things: * 1. triggers the provider detection process {@link Connector#detectProvider} * 2. Register event listeners to the provider {@link Connector#addEventListeners} * * `lazyInitialize` is internally called by the following public methods * - {@link Connector#connect} * - {@link Connector#autoConnect} * * @returns Promise<void> */ async lazyInitialize() { await this.detectProvider(); this.removeEventListeners = this.addEventListeners(); } /** * Try to connect to wallet. * * autoConnect never reject, it will always resolve. * * autoConnect only try to connect to wallet, if it don't need any further * user interaction with the wallet in the connecting process. * * @return Promise<boolean> - * 1. resolve with `true` if the connection succeeded. * 2. resolve with `false` if the connection failed. * */ async autoConnect() { const endConnection = this.actions.startConnection(); try { await this.lazyInitialize(); const [chainId, accounts] = await Promise.all([ this.requestChainId(), this.requestAccounts(), ]); if (!accounts.length) throw new Error('No accounts returned'); this.updateChainId(chainId); this.updateAccounts(accounts); } catch (e) { console.debug(`Could not auto connect`, e); return false; } finally { endConnection(); } return true; } /** * Initiates a connection. * * @param chain - If defined, indicates the desired chain to connect to. If the user is * already connected to this chain, no additional steps will be taken. Otherwise, the user will be prompted to switch * to the chain, if one of two conditions is met: either they already have it added in their extension, or the * argument is of type AddEthereumChainParameter, in which case the user will be prompted to add the chain with the * specified parameters first, before being prompted to switch. * * @returns Promise<void> */ async connect(chain) { const endConnection = this.actions.startConnection(); try { await this.lazyInitialize(); const [chainId, accounts] = await Promise.all([ this.requestChainId(), this.requestAccounts(), ]); const receivedChainId = (0, utils_1.parseChainId)(chainId); const desiredChainId = typeof chain === 'number' ? chain : chain?.chainId; /** * there's no desired chain, or it's equal to the received chain */ if (!desiredChainId || receivedChainId === desiredChainId) { this.updateChainId(receivedChainId); this.updateAccounts(accounts); /** * The connection process completed successfully, we can stop from here. */ return; } /** * the desiredChainId does match the receivedChainId, try to switch chain */ try { await this.switchChain(desiredChainId); this.updateChainId(desiredChainId); this.updateAccounts(accounts); } catch (err) { const error = err; /** * switch chain failed, try to add chain */ const shouldTryToAddChain = (0, types_1.isAddChainParameter)(chain); // const shouldTryToAddChain = // isAddChainParameter(chain) && // (error.code === 4902 || // error.message.code === 4902 || // error.code === -32603); /** * don't know how to handle the error, throw the error again * and stop from here, the whole connection process failed. */ if (!this.addChain || !shouldTryToAddChain) throw error; /** * try to add a new chain to wallet */ await this.addChain(chain); /** * switch to the added chainId again */ await this.switchChain(chain.chainId); } } finally { endConnection(); } } /** * Disconnect wallet * * Wallet connector implementors should override this method if the wallet supports * force disconnect. * * What is force disconnect? * - force disconnect will actually disconnect the wallet. * - non-disconnect only reset the wallet store to it's initial state. * * For some wallets, MetaMask for example, there're not ways to force disconnect MetaMask. * For some wallets, Walletconnect for example, we are able to force disconnect Walletconnect. * @param _force - wether to force disconnect to wallet, default is false. * */ async disconnect(_force) { this.actions.resetState(); } /** * Add an asset to the wallet assets list * * @param asset - {@link WatchAssetParameters} * @return Promise<void> */ async watchAsset(asset) { if (!this.provider) throw this.providerNotFoundError; const success = await this.provider.request({ method: 'wallet_watchAsset', params: { type: 'ERC20', options: asset, }, }); if (!success) throw new Error(`Failed to watch ${asset.symbol}`); } /** * Update the wallet store with new the chainId * * @param chainId - the chainId to update * @return void */ updateChainId(chainId) { this.actions.update({ chainId: (0, utils_1.parseChainId)(chainId), }); } /** * Update the wallet store with new the new accounts * * @param accounts * @return void */ updateAccounts(accounts) { this.actions.update({ accounts }); } /** * Wallet connect listener * * @param info - the connect info * @return void */ onConnect(info) { this.updateChainId(info.chainId); } /** * Wallet disconnect listener * * @param error - the disconnect ProviderRpcError * @return void */ onDisconnect(_) { this.actions.resetState(); } /** * Wallet chainId change listener * * @param chainId - the new chainId * @return void */ onChainChanged(chainId) { this.updateChainId(chainId); } /** * wallet account change listener */ onAccountsChanged(accounts) { this.updateAccounts(accounts); } /** * Register event listeners to the provider * * @return removeEventListeners - a function to remove the registered event listeners {@link Connector#removeEventListeners} */ addEventListeners() { if (!this.provider) return; // return if event listeners are already added if (this.removeEventListeners) return; const onConnect = this.onConnect.bind(this); const onDisconnect = this.onDisconnect.bind(this); const onChainChanged = this.onChainChanged.bind(this); const onAccountsChanged = this.onAccountsChanged.bind(this); if (typeof this.provider.on === 'function') { this.provider.on('connect', onConnect); this.provider.on('disconnect', onDisconnect); this.provider.on('chainChanged', onChainChanged); this.provider.on('accountsChanged', onAccountsChanged); } else { this.provider.addListener('connect', onConnect); this.provider.addListener('disconnect', onDisconnect); this.provider.addListener('chainChanged', onChainChanged); this.provider.addListener('accountsChanged', onAccountsChanged); } return () => { if (!this.provider) return; if (typeof this.provider.off === 'function') { this.provider.off('connect', onConnect); this.provider.off('disconnect', onDisconnect); this.provider.off('chainChanged', onChainChanged); this.provider.off('accountsChanged', onAccountsChanged); } if (typeof this.provider.removeListener === 'function') { this.provider.removeListener('connect', onConnect); this.provider.removeListener('disconnect', onDisconnect); this.provider.removeListener('chainChanged', onChainChanged); this.provider.removeListener('accountsChanged', onAccountsChanged); } }; } /** * Switch network * * - {@link SwitchEthereumChainParameter} * * @param chainId - the if the the chain to switch to * @returns Promise<void> */ async switchChain(chainId) { if (!this.provider) throw this.providerNotFoundError; await this.provider.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: (0, utils_1.toHexChainId)(chainId) }], }); } /** * Add a new network/chain to wallet * * @param - {@link AddEthereumChainParameter} * @returns Promise<void> */ async addChain(addChainParameter) { if (!this.provider) throw this.providerNotFoundError; await this.provider.request({ method: 'wallet_addEthereumChain', params: [ { ...addChainParameter, chainId: (0, utils_1.toHexChainId)(addChainParameter.chainId), }, ], }); } /** * Fetch wallet accounts via the wallet provider * * @return Promise<string[]> - the fetched accounts */ async requestAccounts() { if (!this.provider) throw this.providerNotFoundError; try { const accounts = await this.provider.request({ method: 'eth_requestAccounts', }); return accounts; } catch (error) { console.debug(`Failed to request accounts with 'eth_requestAccounts', try to fallback to 'eth_accounts'`); const accounts = await this.provider.request({ method: 'eth_accounts', }); return accounts; } } /** * Fetch wallet chainId via the wallet provider * * @return Promise<string> - the fetched chainId */ async requestChainId() { if (!this.provider) throw this.providerNotFoundError; return await this.provider.request({ method: 'eth_chainId' }); } } exports.Connector = Connector;