UNPKG

@wagmi/connectors

Version:

Collection of connectors for Wagmi

398 lines 18.8 kB
import { ChainNotConfiguredError, ProviderNotFoundError, createConnector, extractRpcUrls, } from '@wagmi/core'; import { ResourceUnavailableRpcError, SwitchChainError, UserRejectedRequestError, getAddress, hexToNumber, numberToHex, withRetry, withTimeout, } from 'viem'; metaMask.type = 'metaMask'; export function metaMask(parameters = {}) { let sdk; let provider; let providerPromise; let accountsChanged; let chainChanged; let connect; let displayUri; let disconnect; return createConnector((config) => ({ id: 'metaMaskSDK', name: 'MetaMask', rdns: ['io.metamask', 'io.metamask.mobile'], type: metaMask.type, async setup() { const provider = await this.getProvider(); if (provider?.on) { 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 (!displayUri) { displayUri = this.onDisplayUri; provider.on('display_uri', displayUri); } let accounts = []; if (isReconnecting) accounts = await this.getAccounts().catch(() => []); try { let signResponse; let connectWithResponse; if (!accounts?.length) { if (parameters.connectAndSign || parameters.connectWith) { if (parameters.connectAndSign) signResponse = await sdk.connectAndSign({ msg: parameters.connectAndSign, }); else if (parameters.connectWith) connectWithResponse = await sdk.connectWith({ method: parameters.connectWith.method, params: parameters.connectWith.params, }); accounts = await this.getAccounts(); } else { const requestedAccounts = (await sdk.connect()); accounts = requestedAccounts.map((x) => getAddress(x)); } } // 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; } if (displayUri) { provider.removeListener('display_uri', displayUri); displayUri = undefined; } if (signResponse) provider.emit('connectAndSign', { accounts, chainId: currentChainId, signResponse, }); else if (connectWithResponse) provider.emit('connectWith', { accounts, chainId: currentChainId, connectWithResponse, }); // 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); } 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(); // 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); } await sdk.terminate(); }, async getAccounts() { const provider = await this.getProvider(); const accounts = (await provider.request({ method: 'eth_accounts', })); return accounts.map((x) => getAddress(x)); }, async getChainId() { const provider = await this.getProvider(); const chainId = provider.getChainId() || (await provider?.request({ method: 'eth_chainId' })); return Number(chainId); }, async getProvider() { async function initProvider() { // Unwrapping import for Vite compatibility. // See: https://github.com/vitejs/vite/issues/9703 const MetaMaskSDK = await (async () => { const { default: SDK } = await import('@metamask/sdk'); if (typeof SDK !== 'function' && typeof SDK.default === 'function') return SDK.default; return SDK; })(); const readonlyRPCMap = {}; for (const chain of config.chains) readonlyRPCMap[numberToHex(chain.id)] = extractRpcUrls({ chain, transports: config.transports, })?.[0]; sdk = new MetaMaskSDK({ _source: 'wagmi', forceDeleteProvider: false, forceInjectProvider: false, injectProvider: false, // Workaround cast since MetaMask SDK does not support `'exactOptionalPropertyTypes'` ...parameters, readonlyRPCMap, dappMetadata: { ...parameters.dappMetadata, // Test if name and url are set AND not empty name: parameters.dappMetadata?.name ? parameters.dappMetadata?.name : 'wagmi', url: parameters.dappMetadata?.url ? parameters.dappMetadata?.url : typeof window !== 'undefined' ? window.location.origin : 'https://wagmi.sh', }, useDeeplink: parameters.useDeeplink ?? true, }); const result = await sdk.init(); // On initial load, sometimes `sdk.getProvider` does not return provider. // https://github.com/wevm/wagmi/issues/4367 // Use result of `init` call if available. const provider = (() => { if (result?.activeProvider) return result.activeProvider; return sdk.getProvider(); })(); if (!provider) throw new ProviderNotFoundError(); return provider; } if (!provider) { if (!providerPromise) providerPromise = initProvider(); provider = await providerPromise; } return provider; }, async isAuthorized() { try { // MetaMask mobile provider sometimes fails to immediately resolve // JSON-RPC requests on page load const timeout = 200; const accounts = await withRetry(() => withTimeout(() => this.getAccounts(), { timeout }), { delay: timeout + 1, retryCount: 3, }); return !!accounts.length; } catch { return false; } }, async switchChain({ addEthereumChainParameter, chainId }) { const provider = await this.getProvider(); const chain = config.chains.find((x) => x.id === chainId); if (!chain) throw new SwitchChainError(new ChainNotConfiguredError()); try { await 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 await waitForChainIdToSync(); await sendAndWaitForChangeEvent(chainId); return chain; } catch (err) { const error = err; if (error.code === UserRejectedRequestError.code) throw new UserRejectedRequestError(error); // 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 { await provider.request({ method: 'wallet_addEthereumChain', params: [ { blockExplorerUrls: (() => { const { default: blockExplorer, ...blockExplorers } = chain.blockExplorers ?? {}; if (addEthereumChainParameter?.blockExplorerUrls) return addEthereumChainParameter.blockExplorerUrls; if (blockExplorer) return [ blockExplorer.url, ...Object.values(blockExplorers).map((x) => x.url), ]; return; })(), chainId: numberToHex(chainId), chainName: addEthereumChainParameter?.chainName ?? chain.name, iconUrls: addEthereumChainParameter?.iconUrls, nativeCurrency: addEthereumChainParameter?.nativeCurrency ?? chain.nativeCurrency, rpcUrls: (() => { if (addEthereumChainParameter?.rpcUrls?.length) return addEthereumChainParameter.rpcUrls; return [chain.rpcUrls.default?.http[0] ?? '']; })(), }, ], }); await waitForChainIdToSync(); await sendAndWaitForChangeEvent(chainId); return chain; } catch (err) { const error = err; if (error.code === UserRejectedRequestError.code) throw new UserRejectedRequestError(error); throw new SwitchChainError(error); } } throw new SwitchChainError(error); } async function waitForChainIdToSync() { // On mobile, there is a race condition between the result of `'wallet_addEthereumChain'` and `'eth_chainId'`. // To avoid this, we wait for `'eth_chainId'` to return the expected chain ID with a retry loop. await withRetry(async () => { const value = hexToNumber( // `'eth_chainId'` is cached by the MetaMask SDK side to avoid unnecessary deeplinks (await provider.request({ method: 'eth_chainId' }))); // `value` doesn't match expected `chainId`, throw to trigger retry if (value !== chainId) throw new Error('User rejected switch after adding network.'); return value; }, { delay: 50, retryCount: 20, // android device encryption is slower }); } async function sendAndWaitForChangeEvent(chainId) { await new Promise((resolve) => { const listener = ((data) => { if ('chainId' in data && data.chainId === chainId) { config.emitter.off('change', listener); resolve(); } }); config.emitter.on('change', listener); config.emitter.emit('change', { chainId }); }); } }, async onAccountsChanged(accounts) { // Disconnect if there are no accounts if (accounts.length === 0) { // ... and using browser extension if (sdk.isExtensionActive()) this.onDisconnect(); // FIXME(upstream): Mobile app sometimes emits invalid `accountsChanged` event with empty accounts array else return; } // 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 }); } // 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 }); const provider = await this.getProvider(); 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; } config.emitter.emit('disconnect'); // 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); } }, onDisplayUri(uri) { config.emitter.emit('message', { type: 'display_uri', data: uri }); }, })); } //# sourceMappingURL=metaMask.js.map