UNPKG

@wagmi/core

Version:

VanillaJS library for Ethereum

530 lines 24.3 kB
import { ResourceUnavailableRpcError, SwitchChainError, UserRejectedRequestError, getAddress, numberToHex, withRetry, withTimeout, } from 'viem'; import { ChainNotConfiguredError } from '../errors/config.js'; import { ProviderNotFoundError } from '../errors/connector.js'; import { createConnector } from './createConnector.js'; injected.type = 'injected'; export function injected(parameters = {}) { const { shimDisconnect = true, unstable_shimAsyncInject } = parameters; function getTarget() { const target = parameters.target; if (typeof target === 'function') { const result = target(); if (result) return result; } if (typeof target === 'object') return target; if (typeof target === 'string') return { ...(targetMap[target] ?? { id: target, name: `${target[0].toUpperCase()}${target.slice(1)}`, provider: `is${target[0].toUpperCase()}${target.slice(1)}`, }), }; return { id: 'injected', name: 'Injected', provider(window) { return window?.ethereum; }, }; } let accountsChanged; let chainChanged; let connect; let disconnect; return createConnector((config) => ({ get icon() { return getTarget().icon; }, get id() { return getTarget().id; }, get name() { return getTarget().name; }, /** @deprecated */ get supportsSimulation() { return true; }, type: injected.type, async setup() { const provider = await this.getProvider(); // Only start listening for events if `target` is set, otherwise `injected()` will also receive events if (provider?.on && parameters.target) { if (!connect) { connect = this.onConnect.bind(this); provider.on('connect', connect); } // We shouldn't need to listen for `'accountsChanged'` here since the `'connect'` event should suffice (and wallet shouldn't be connected yet). // Some wallets, like MetaMask, do not implement the `'connect'` event and overload `'accountsChanged'` instead. if (!accountsChanged) { accountsChanged = this.onAccountsChanged.bind(this); provider.on('accountsChanged', accountsChanged); } } }, async connect({ chainId, isReconnecting } = {}) { const provider = await this.getProvider(); if (!provider) throw new ProviderNotFoundError(); let accounts = []; if (isReconnecting) accounts = await this.getAccounts().catch(() => []); else if (shimDisconnect) { // Attempt to show another prompt for selecting account if `shimDisconnect` flag is enabled try { const permissions = await provider.request({ method: 'wallet_requestPermissions', params: [{ eth_accounts: {} }], }); accounts = permissions[0]?.caveats?.[0]?.value?.map((x) => getAddress(x)); // `'wallet_requestPermissions'` can return a different order of accounts than `'eth_accounts'` // switch to `'eth_accounts'` ordering if more than one account is connected // https://github.com/wevm/wagmi/issues/4140 if (accounts.length > 0) { const sortedAccounts = await this.getAccounts(); accounts = sortedAccounts; } } catch (err) { const error = err; // Not all injected providers support `wallet_requestPermissions` (e.g. MetaMask iOS). // Only bubble up error if user rejects request if (error.code === UserRejectedRequestError.code) throw new UserRejectedRequestError(error); // Or prompt is already open if (error.code === ResourceUnavailableRpcError.code) throw error; } } try { if (!accounts?.length && !isReconnecting) { const requestedAccounts = await provider.request({ method: 'eth_requestAccounts', }); accounts = requestedAccounts.map((x) => getAddress(x)); } // Manage EIP-1193 event listeners // https://eips.ethereum.org/EIPS/eip-1193#events if (connect) { provider.removeListener('connect', connect); connect = undefined; } if (!accountsChanged) { accountsChanged = this.onAccountsChanged.bind(this); provider.on('accountsChanged', accountsChanged); } if (!chainChanged) { chainChanged = this.onChainChanged.bind(this); provider.on('chainChanged', chainChanged); } if (!disconnect) { disconnect = this.onDisconnect.bind(this); provider.on('disconnect', disconnect); } // Switch to chain if provided let currentChainId = await this.getChainId(); if (chainId && currentChainId !== chainId) { const chain = await this.switchChain({ chainId }).catch((error) => { if (error.code === UserRejectedRequestError.code) throw error; return { id: currentChainId }; }); currentChainId = chain?.id ?? currentChainId; } // Remove disconnected shim if it exists if (shimDisconnect) await config.storage?.removeItem(`${this.id}.disconnected`); // Add connected shim if no target exists if (!parameters.target) await config.storage?.setItem('injected.connected', true); return { accounts, chainId: currentChainId }; } catch (err) { const error = err; if (error.code === UserRejectedRequestError.code) throw new UserRejectedRequestError(error); if (error.code === ResourceUnavailableRpcError.code) throw new ResourceUnavailableRpcError(error); throw error; } }, async disconnect() { const provider = await this.getProvider(); if (!provider) throw new ProviderNotFoundError(); // Manage EIP-1193 event listeners if (chainChanged) { provider.removeListener('chainChanged', chainChanged); chainChanged = undefined; } if (disconnect) { provider.removeListener('disconnect', disconnect); disconnect = undefined; } if (!connect) { connect = this.onConnect.bind(this); provider.on('connect', connect); } // Experimental support for MetaMask disconnect // https://github.com/MetaMask/metamask-improvement-proposals/blob/main/MIPs/mip-2.md try { // Adding timeout as not all wallets support this method and can hang // https://github.com/wevm/wagmi/issues/4064 await withTimeout(() => // TODO: Remove explicit type for viem@3 provider.request({ // `'wallet_revokePermissions'` added in `viem@2.10.3` method: 'wallet_revokePermissions', params: [{ eth_accounts: {} }], }), { timeout: 100 }); } catch { } // Add shim signalling connector is disconnected if (shimDisconnect) { await config.storage?.setItem(`${this.id}.disconnected`, true); } if (!parameters.target) await config.storage?.removeItem('injected.connected'); }, async getAccounts() { const provider = await this.getProvider(); if (!provider) throw new ProviderNotFoundError(); const accounts = await provider.request({ method: 'eth_accounts' }); return accounts.map((x) => getAddress(x)); }, async getChainId() { const provider = await this.getProvider(); if (!provider) throw new ProviderNotFoundError(); const hexChainId = await provider.request({ method: 'eth_chainId' }); return Number(hexChainId); }, async getProvider() { if (typeof window === 'undefined') return undefined; let provider; const target = getTarget(); if (typeof target.provider === 'function') provider = target.provider(window); else if (typeof target.provider === 'string') provider = findProvider(window, target.provider); else provider = target.provider; // Some wallets do not conform to EIP-1193 (e.g. Trust Wallet) // https://github.com/wevm/wagmi/issues/3526#issuecomment-1912683002 if (provider && !provider.removeListener) { // Try using `off` handler if it exists, otherwise noop if ('off' in provider && typeof provider.off === 'function') provider.removeListener = provider.off; else provider.removeListener = () => { }; } return provider; }, async isAuthorized() { try { const isDisconnected = shimDisconnect && // If shim exists in storage, connector is disconnected (await config.storage?.getItem(`${this.id}.disconnected`)); if (isDisconnected) return false; // Don't allow injected connector to connect if no target is set and it hasn't already connected // (e.g. flag in storage is not set). This prevents a targetless injected connector from connecting // automatically whenever there is a targeted connector configured. if (!parameters.target) { const connected = await config.storage?.getItem('injected.connected'); if (!connected) return false; } const provider = await this.getProvider(); if (!provider) { if (unstable_shimAsyncInject !== undefined && unstable_shimAsyncInject !== false) { // If no provider is found, check for async injection // https://github.com/wevm/references/issues/167 // https://github.com/MetaMask/detect-provider const handleEthereum = async () => { if (typeof window !== 'undefined') window.removeEventListener('ethereum#initialized', handleEthereum); const provider = await this.getProvider(); return !!provider; }; const timeout = typeof unstable_shimAsyncInject === 'number' ? unstable_shimAsyncInject : 1_000; const res = await Promise.race([ ...(typeof window !== 'undefined' ? [ new Promise((resolve) => window.addEventListener('ethereum#initialized', () => resolve(handleEthereum()), { once: true })), ] : []), new Promise((resolve) => setTimeout(() => resolve(handleEthereum()), timeout)), ]); if (res) return true; } throw new ProviderNotFoundError(); } // Use retry strategy as some injected wallets (e.g. MetaMask) fail to // immediately resolve JSON-RPC requests on page load. const accounts = await withRetry(() => this.getAccounts()); return !!accounts.length; } catch { return false; } }, async switchChain({ addEthereumChainParameter, chainId }) { const provider = await this.getProvider(); if (!provider) throw new ProviderNotFoundError(); const chain = config.chains.find((x) => x.id === chainId); if (!chain) throw new SwitchChainError(new ChainNotConfiguredError()); const promise = new Promise((resolve) => { const listener = ((data) => { if ('chainId' in data && data.chainId === chainId) { config.emitter.off('change', listener); resolve(); } }); config.emitter.on('change', listener); }); try { await Promise.all([ provider .request({ method: 'wallet_switchEthereumChain', params: [{ chainId: numberToHex(chainId) }], }) // During `'wallet_switchEthereumChain'`, MetaMask makes a `'net_version'` RPC call to the target chain. // If this request fails, MetaMask does not emit the `'chainChanged'` event, but will still switch the chain. // To counter this behavior, we request and emit the current chain ID to confirm the chain switch either via // this callback or an externally emitted `'chainChanged'` event. // https://github.com/MetaMask/metamask-extension/issues/24247 .then(async () => { const currentChainId = await this.getChainId(); if (currentChainId === chainId) config.emitter.emit('change', { chainId }); }), promise, ]); return chain; } catch (err) { const error = err; // Indicates chain is not added to provider if (error.code === 4902 || // Unwrapping for MetaMask Mobile // https://github.com/MetaMask/metamask-mobile/issues/2944#issuecomment-976988719 error ?.data?.originalError?.code === 4902) { try { const { default: blockExplorer, ...blockExplorers } = chain.blockExplorers ?? {}; let blockExplorerUrls; if (addEthereumChainParameter?.blockExplorerUrls) blockExplorerUrls = addEthereumChainParameter.blockExplorerUrls; else if (blockExplorer) blockExplorerUrls = [ blockExplorer.url, ...Object.values(blockExplorers).map((x) => x.url), ]; let rpcUrls; if (addEthereumChainParameter?.rpcUrls?.length) rpcUrls = addEthereumChainParameter.rpcUrls; else rpcUrls = [chain.rpcUrls.default?.http[0] ?? '']; const addEthereumChain = { blockExplorerUrls, chainId: numberToHex(chainId), chainName: addEthereumChainParameter?.chainName ?? chain.name, iconUrls: addEthereumChainParameter?.iconUrls, nativeCurrency: addEthereumChainParameter?.nativeCurrency ?? chain.nativeCurrency, rpcUrls, }; await Promise.all([ provider .request({ method: 'wallet_addEthereumChain', params: [addEthereumChain], }) .then(async () => { const currentChainId = await this.getChainId(); if (currentChainId === chainId) config.emitter.emit('change', { chainId }); else throw new UserRejectedRequestError(new Error('User rejected switch after adding network.')); }), promise, ]); return chain; } catch (error) { throw new UserRejectedRequestError(error); } } if (error.code === UserRejectedRequestError.code) throw new UserRejectedRequestError(error); throw new SwitchChainError(error); } }, async onAccountsChanged(accounts) { // Disconnect if there are no accounts if (accounts.length === 0) this.onDisconnect(); // Connect if emitter is listening for connect event (e.g. is disconnected and connects through wallet interface) else if (config.emitter.listenerCount('connect')) { const chainId = (await this.getChainId()).toString(); this.onConnect({ chainId }); // Remove disconnected shim if it exists if (shimDisconnect) await config.storage?.removeItem(`${this.id}.disconnected`); } // Regular change event else config.emitter.emit('change', { accounts: accounts.map((x) => getAddress(x)), }); }, onChainChanged(chain) { const chainId = Number(chain); config.emitter.emit('change', { chainId }); }, async onConnect(connectInfo) { const accounts = await this.getAccounts(); if (accounts.length === 0) return; const chainId = Number(connectInfo.chainId); config.emitter.emit('connect', { accounts, chainId }); // Manage EIP-1193 event listeners const provider = await this.getProvider(); if (provider) { if (connect) { provider.removeListener('connect', connect); connect = undefined; } if (!accountsChanged) { accountsChanged = this.onAccountsChanged.bind(this); provider.on('accountsChanged', accountsChanged); } if (!chainChanged) { chainChanged = this.onChainChanged.bind(this); provider.on('chainChanged', chainChanged); } if (!disconnect) { disconnect = this.onDisconnect.bind(this); provider.on('disconnect', disconnect); } } }, async onDisconnect(error) { const provider = await this.getProvider(); // If MetaMask emits a `code: 1013` error, wait for reconnection before disconnecting // https://github.com/MetaMask/providers/pull/120 if (error && error.code === 1013) { if (provider && !!(await this.getAccounts()).length) return; } // No need to remove `${this.id}.disconnected` from storage because `onDisconnect` is typically // only called when the wallet is disconnected through the wallet's interface, meaning the wallet // actually disconnected and we don't need to simulate it. config.emitter.emit('disconnect'); // Manage EIP-1193 event listeners if (provider) { if (chainChanged) { provider.removeListener('chainChanged', chainChanged); chainChanged = undefined; } if (disconnect) { provider.removeListener('disconnect', disconnect); disconnect = undefined; } if (!connect) { connect = this.onConnect.bind(this); provider.on('connect', connect); } } }, })); } const targetMap = { coinbaseWallet: { id: 'coinbaseWallet', name: 'Coinbase Wallet', provider(window) { if (window?.coinbaseWalletExtension) return window.coinbaseWalletExtension; return findProvider(window, 'isCoinbaseWallet'); }, }, metaMask: { id: 'metaMask', name: 'MetaMask', provider(window) { return findProvider(window, (provider) => { if (!provider.isMetaMask) return false; // Brave tries to make itself look like MetaMask // Could also try RPC `web3_clientVersion` if following is unreliable if (provider.isBraveWallet && !provider._events && !provider._state) return false; // Other wallets that try to look like MetaMask const flags = [ 'isApexWallet', 'isAvalanche', 'isBitKeep', 'isBlockWallet', 'isKuCoinWallet', 'isMathWallet', 'isOkxWallet', 'isOKExWallet', 'isOneInchIOSWallet', 'isOneInchAndroidWallet', 'isOpera', 'isPhantom', 'isPortal', 'isRabby', 'isTokenPocket', 'isTokenary', 'isUniswapWallet', 'isZerion', ]; for (const flag of flags) if (provider[flag]) return false; return true; }); }, }, phantom: { id: 'phantom', name: 'Phantom', provider(window) { if (window?.phantom?.ethereum) return window.phantom?.ethereum; return findProvider(window, 'isPhantom'); }, }, }; function findProvider(window, select) { function isProvider(provider) { if (typeof select === 'function') return select(provider); if (typeof select === 'string') return provider[select]; return true; } const ethereum = window.ethereum; if (ethereum?.providers) return ethereum.providers.find((provider) => isProvider(provider)); if (ethereum && isProvider(ethereum)) return ethereum; return undefined; } //# sourceMappingURL=injected.js.map