UNPKG

@web3-onboard/walletconnect

Version:

WalletConnect SDK module for connecting to Web3-Onboard. Web3-Onboard makes it simple to connect Ethereum hardware and software wallets to your dapp. Features standardised spec compliant web3 providers for all supported wallets, framework agnostic modern

254 lines (253 loc) 14 kB
import { isHexString } from './index.js'; // methods that require user interaction const methods = [ 'eth_sendTransaction', 'eth_signTransaction', 'personal_sign', 'eth_sign', 'eth_signTypedData', 'eth_signTypedData_v4', 'wallet_addEthereumChain', 'wallet_switchEthereumChain' ]; function walletConnect(options) { if (!options.projectId) { throw new Error('WalletConnect requires a projectId. Please visit https://cloud.walletconnect.com to get one.'); } if (!options.dappUrl) { console.warn(`It is strongly recommended to supply a dappUrl to the WalletConnect init object as it is required by some wallets (i.e. MetaMask) to allow connection.`); } const { projectId, handleUri, requiredChains, optionalChains, qrModalOptions, additionalRequiredMethods, additionalOptionalMethods, dappUrl } = options; let instance; return () => { return { label: 'WalletConnect', getIcon: async () => (await import('./icon.js')).default, getInterface: async ({ chains, EventEmitter, appMetadata }) => { const { ProviderRpcError, ProviderRpcErrorCode } = await import('@web3-onboard/common'); const { default: EthereumProvider, REQUIRED_METHODS } = await import('@walletconnect/ethereum-provider'); const { Subject, fromEvent } = await import('rxjs'); const { takeUntil, take } = await import('rxjs/operators'); const getMetaData = () => { if (!appMetadata) return undefined; const url = dappUrl || appMetadata.explore || ''; !url && !url.length && console.warn(`It is strongly recommended to supply a dappUrl as it is required by some wallets (i.e. MetaMask) to allow connection.`); const wcMetaData = { name: appMetadata.name, description: appMetadata.description || '', url, icons: [] }; if (appMetadata.icon !== undefined && appMetadata.icon.length) { wcMetaData.icons = [appMetadata.icon]; } if (appMetadata.logo !== undefined && appMetadata.logo.length) { wcMetaData.icons = wcMetaData.icons.length ? [...wcMetaData.icons, appMetadata.logo] : [appMetadata.logo]; } return wcMetaData; }; // default to mainnet const requiredChainsParsed = Array.isArray(requiredChains) && requiredChains.length && requiredChains.every(num => !isNaN(num)) ? // @ts-ignore // Required as WC package does not support hex numbers requiredChains.map(chainID => parseInt(chainID)) : []; // Defaults to the chains provided within the web3-onboard init chain property const optionalChainsParsed = Array.isArray(optionalChains) && optionalChains.length && optionalChains.every(num => !isNaN(num)) ? // @ts-ignore // Required as WC package does not support hex numbers optionalChains.map(chainID => parseInt(chainID)) : chains.map(({ id }) => parseInt(id, 16)); const requiredMethodsSet = new Set(additionalRequiredMethods && Array.isArray(additionalRequiredMethods) ? [...additionalRequiredMethods, ...REQUIRED_METHODS] : REQUIRED_METHODS); const requiredMethods = Array.from(requiredMethodsSet); const optionalMethods = additionalOptionalMethods && Array.isArray(additionalOptionalMethods) ? [...additionalOptionalMethods, ...methods] : methods; const connector = await EthereumProvider.init({ projectId, chains: requiredChainsParsed, // default to mainnet methods: requiredMethods, optionalChains: optionalChainsParsed, optionalMethods, showQrModal: true, rpcMap: chains .map(({ id, rpcUrl }) => ({ id, rpcUrl })) .reduce((rpcMap, { id, rpcUrl }) => { rpcMap[parseInt(id, 16)] = rpcUrl || ''; return rpcMap; }, {}), metadata: getMetaData(), qrModalOptions: qrModalOptions }); const emitter = new EventEmitter(); class EthProvider { constructor({ connector, chains }) { this.emit = emitter.emit.bind(emitter); this.on = emitter.on.bind(emitter); this.removeListener = emitter.removeListener.bind(emitter); this.connector = connector; this.chains = chains; this.disconnected$ = new Subject(); // listen for accountsChanged fromEvent(this.connector, 'accountsChanged', payload => payload) .pipe(takeUntil(this.disconnected$)) .subscribe({ next: payload => { const accounts = Array.isArray(payload) ? payload : [payload]; this.emit('accountsChanged', accounts); }, error: console.warn }); // listen for chainChanged fromEvent(this.connector, 'chainChanged', (payload) => payload) .pipe(takeUntil(this.disconnected$)) .subscribe({ next: chainId => { const hexChainId = isHexString(chainId) ? chainId : `0x${chainId.toString(16)}`; this.emit('chainChanged', hexChainId); }, error: console.warn }); // listen for disconnect event fromEvent(this.connector, 'session_delete', (payload) => payload) .pipe(takeUntil(this.disconnected$)) .subscribe({ next: () => { this.emit('accountsChanged', []); this.disconnected$.next(true); typeof localStorage !== 'undefined' && localStorage.removeItem('walletconnect'); }, error: console.warn }); this.disconnect = () => { if (this.connector.session) { this.connector.disconnect(); instance = null; } }; if (options && handleUri) { // listen for uri event fromEvent(this.connector, 'display_uri', (payload) => payload) .pipe(takeUntil(this.disconnected$)) .subscribe(async (uri) => { try { handleUri && (await handleUri(uri)); } catch (error) { throw `An error occurred when handling the URI. Error: ${error}`; } }); } const checkForSession = () => { const session = this.connector.session; instance = session; if (session) { this.emit('accountsChanged', this.connector.accounts); this.emit('chainChanged', this.connector.chainId); } }; checkForSession(); this.request = async ({ method, params }) => { if (method === 'eth_chainId') { return isHexString(this.connector.chainId) ? this.connector.chainId : `0x${this.connector.chainId.toString(16)}`; } if (method === 'eth_requestAccounts') { return new Promise(async (resolve, reject) => { // Subscribe to connection events fromEvent(this.connector, 'connect', (payload) => payload) .pipe(take(1)) .subscribe({ next: ({ chainId }) => { this.emit('accountsChanged', this.connector.accounts); const hexChainId = isHexString(chainId) ? chainId : `0x${chainId.toString(16)}`; this.emit('chainChanged', hexChainId); resolve(this.connector.accounts); }, error: reject }); // Check if connection is already established if (!this.connector.session) { // create new session await this.connector.connect().catch(err => { console.error('err creating new session: ', err); reject(new ProviderRpcError({ code: 4001, message: 'User rejected the request.' })); }); } else { // update ethereum provider to load accounts & chainId const accounts = this.connector.accounts; const chainId = this.connector.chainId; instance = this.connector.session; const hexChainId = `0x${chainId.toString(16)}`; this.emit('chainChanged', hexChainId); return resolve(accounts); } }); } if (method === 'eth_selectAccounts') { throw new ProviderRpcError({ code: ProviderRpcErrorCode.UNSUPPORTED_METHOD, message: `The Provider does not support the requested method: ${method}` }); } if (method == 'wallet_switchEthereumChain') { if (!params) { throw new ProviderRpcError({ code: ProviderRpcErrorCode.INVALID_PARAMS, message: `The Provider requires a chainId to be passed in as an argument` }); } const chainIdObj = params[0]; if (!chainIdObj.hasOwnProperty('chainId') || typeof chainIdObj['chainId'] === 'undefined') { throw new ProviderRpcError({ code: ProviderRpcErrorCode.INVALID_PARAMS, message: `The Provider requires a chainId to be passed in as an argument` }); } return this.connector.request({ method: 'wallet_switchEthereumChain', params: [ { chainId: chainIdObj.chainId } ] }); } return this.connector.request({ method, params }); }; } } return { provider: new EthProvider({ chains, connector }), instance }; } }; }; } export default walletConnect;