import { REQUIRED_METHODS } from '@walletconnect/ethereum-provider' import { isHexString } from './index.js' import type { EthereumProviderOptions } from '@walletconnect/ethereum-provider/dist/types/EthereumProvider' import type { JQueryStyleEventEmitter } from 'rxjs/internal/observable/fromEvent' import type { EthereumProvider } from '@walletconnect/ethereum-provider' import type { WalletConnectOptions } from './types.js' import type { CoreTypes } from '@walletconnect/types' import type { Chain, ProviderAccounts, WalletInit, EIP1193Provider } from '@web3-onboard/common' // 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: WalletConnectOptions): WalletInit { 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: unknown 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 } = await import( '@walletconnect/ethereum-provider' ) const { Subject, fromEvent } = await import('rxjs') const { takeUntil, take } = await import('rxjs/operators') const getMetaData = (): CoreTypes.Metadata | undefined => { 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: CoreTypes.Metadata = { 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: number[] = 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: number[] = 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: Record, { id, rpcUrl }) => { rpcMap[parseInt(id, 16)] = rpcUrl || '' return rpcMap }, {}), metadata: getMetaData(), qrModalOptions: qrModalOptions } as EthereumProviderOptions) const emitter = new EventEmitter() class EthProvider { public request: EIP1193Provider['request'] public connector: InstanceType public chains: Chain[] public disconnect: EIP1193Provider['disconnect'] // @ts-ignore public emit: typeof EventEmitter['emit'] // @ts-ignore public on: typeof EventEmitter['on'] // @ts-ignore public removeListener: typeof EventEmitter['removeListener'] private disconnected$: InstanceType constructor({ connector, chains }: { connector: InstanceType chains: Chain[] }) { 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 as JQueryStyleEventEmitter, 'chainChanged', (payload: number) => 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 as JQueryStyleEventEmitter, 'session_delete', (payload: string) => 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 as JQueryStyleEventEmitter, 'display_uri', (payload: string) => 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 as JQueryStyleEventEmitter< any, { chainId: number } >, 'connect', (payload: { chainId: number | string }) => 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] as { chainId?: number } 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