import { goerli, mainnet, polygon, polygonMumbai, sepolia } from '@wagmi/chains' import type { Chain, Connector, GetAccountResult, SwitchNetworkResult } from '@wagmi/core' import { configureChains, createConfig, createStorage, getWalletClient } from '@wagmi/core' import { MetaMaskConnector } from '@wagmi/connectors/metaMask' import { ClientCtrl, ConfigCtrl, ExplorerCtrl, ModalCtrl, OptionsCtrl } from '@web3modal/core' import { EthereumClient, w3mConnectors, w3mProvider } from '@web3modal/ethereum' import type EthereumProvider from '@walletconnect/ethereum-provider' import type { IProvider } from '@walletconnect/universal-provider' import type { Authc } from '../authc' import { CustomPostMessage } from './CustomPostMessage' import type { AuthcWalletOptions } from './types' import { createLogoutIframe, createWalletLoginIframe, formatUniversalUrl } from './utils' import { AUTHC_CUSTOM_WALLET } from './constants' export const defaultChains = [mainnet, polygon, polygonMumbai, goerli, sepolia] let wagmiClientInstance: ReturnType export const createWeb3Modal = (config: AuthcWalletOptions) => { const { projectId, storageKey = 'authc.wallet.wagmi', wagmiClientCache = false } = config // 1. Define constants const chains: Chain[] = config.chains ?? defaultChains // 2. Configure wagmi client if (!wagmiClientInstance || !wagmiClientCache) { const { publicClient, webSocketPublicClient } = configureChains(chains, [w3mProvider({ projectId: projectId || '' })]) wagmiClientInstance = createConfig({ autoConnect: true, connectors: [ new MetaMaskConnector({ chains, options: { shimDisconnect: true, }, }) as any, ...w3mConnectors({ chains, projectId, }), ], publicClient, storage: createStorage({ key: storageKey, storage: window.localStorage, }), webSocketPublicClient: webSocketPublicClient as any, }) // 3. Create ethereum and modal clients const ethereumClient = new EthereumClient(wagmiClientInstance, chains) ClientCtrl.setEthereumClient(ethereumClient) ConfigCtrl.setConfig({ projectId, ...config }) } } export class CustomWalletLogin extends CustomPostMessage { private closeWatch: (() => void) | null = null walletLoginPromise: Promise __resolve: (value: any) => void __reject: (err: any) => void CUSTOM_WALLET_KEY = AUTHC_CUSTOM_WALLET authc: Authc constructor(private walletOption: AuthcWalletOptions) { super(() => this.customIframe, walletOption.authc ? 'authcWalletLogin' : 'walletLogin') if (walletOption.storageKey) this.CUSTOM_WALLET_KEY += `${walletOption.storageKey}.${AUTHC_CUSTOM_WALLET}` createWeb3Modal(walletOption) if (walletOption.customWallets?.length) { this.authc = walletOption.authc this.init() this.watchCustomLogin() } } get isCustomWallet() { return !!this.customIframe } get walletName() { return localStorage.getItem(this.CUSTOM_WALLET_KEY) || undefined } get customIframe() { return document.querySelector('iframe[data-authc-wallet]') } async removeIframe() { this.customIframe?.remove() this.closeWatch?.() this.closeWatch = null } private onLoadLoginIframeError() { this.cancelLogin() this.__reject(new Error('Load wallet website failed, Please check your network, or try again later')) } toggleIframeVisible = (state: boolean) => { if (this.customIframe) { if (state) { this.customIframe.style.zIndex = '5000' this.customIframe.style.pointerEvents = 'auto' } else { if (this.customIframe) { this.customIframe.style.zIndex = '-1' this.customIframe.style.pointerEvents = 'none' } } } } getWalletConnectUri() { return new Promise((resolve, reject) => { const chain = this.options.getSelectedChain() || this.config.state.defaultChain this.client().connectWalletConnect((uri) => { resolve(uri) }, chain?.id).catch(reject) }) } createLoginPromise() { this.walletLoginPromise = new Promise((resolve, reject) => { this.__resolve = resolve this.__reject = reject }) return this.walletLoginPromise } reconnectWalletCustom() { if (this.customIframe) return this.walletLoginPromise this.createLoginPromise() const wallet = this.getCustomWallet(this.walletName!) if (wallet) { createWalletLoginIframe( `${wallet.links.universal}?redirectUrl=${encodeURIComponent('/view/wc')}&locale=${this.walletOption.locale?.()}`, { onerror: this.onLoadLoginIframeError, }, ) } return this.walletLoginPromise } async connectWalletCustom(name?: string) { if (!name) name = (this.config.state as AuthcWalletOptions).customWallets?.[0].name this.createLoginPromise() if (this.customIframe) { try { this.replace('/login') } catch {} return this.walletLoginPromise } const wallet = this.getCustomWallet(name) if (!wallet) { this.__reject(new Error(`Cannot find '${name}' wallet, please check you have added it to the customWallets`)) return this.walletLoginPromise } localStorage.setItem(this.CUSTOM_WALLET_KEY, wallet.name) createWalletLoginIframe( `${formatUniversalUrl(wallet.links.universal, await this.getWalletConnectUri(), wallet.name)}&locale=${this.walletOption.locale?.()}`, { onerror: this.onLoadLoginIframeError, }, ) return this.walletLoginPromise } getCustomWallet = (name: string) => { return (this.config.state as AuthcWalletOptions).customWallets?.find(wallet => wallet.name.toLowerCase() === name?.toLowerCase()) } async watchCustomLogin() { this.client().watchAccount((account) => { const wallet = this.getCustomWallet(this.walletName) if (!wallet) return if (account.isConnecting || account.isReconnecting) { this.watchCustomWallet() } else if (account.isConnected) { this.watchCustomWallet() this.getSignerProvider().then((provider) => { if (this.walletName && !wallet) return this.cancelLogin() if (!wallet) return const session = (provider as any)?.session || (provider as any)?.connector?.session if (!session) return const walletUrl = (session.peerMeta || session.peer?.metadata)?.url if (wallet.links.universal.includes(walletUrl)) { if (!this.customIframe) this.reconnectWalletCustom() this.checkWalletAddress(account.address) } else if (this.walletName) { this.cancelLogin() } }) if (this.initialized) { // 如果没有就补充一个 if (!this.walletLoginPromise) this.createLoginPromise() this.__resolve(account) } else { this.initializedCallbackQueue.push(async () => { // 如果没有就补充一个 if (!this.walletLoginPromise) this.createLoginPromise() this.__resolve?.(account) }) } } else if (account.isDisconnected && (this.customIframe)) { if (!this.walletOption.unlockOnDemand) this.cancelLogin() } }) } watchCustomWallet() { if (this.closeWatch) return this.closeWatch = this.receiveMessage(async (type, data) => { if (type === 'lifecycle') { if (data.name === 'reconstruct') { if (this.client().getAccount().isConnected) this.client().disconnect() if (!this.walletOption.unlockOnDemand && this.client().getAccount().status !== 'disconnected') this.replace('/reconstruct') } } else if (type === 'loginFailed') { this.__reject?.(new Error(data?.errorMessage || data?.error?.toString() || 'Login failed')) this.cancelLogin() } else if (type === 'modal') { this.toggleIframeVisible(!!data.state) } else if (type === 'wcUri') { this.getWalletConnectUri().then((uri) => { this.postMessage({ type: 'wcUri', uri, code: data.code, }) }) } else if (type.toString() === '401' && this.client().getAccount().isConnected) { if (data.url) { const url = new URL(data.url) await this.logoutWithIframe(url.toString()) } this.cancelLogin() } else if (type === 'logout') { await this.cancelLogin() } }) } watchAccount(callback: ( account: GetAccountResult & { /** * is custom wallet login? */ isCustomWallet: boolean errMessage: string }) => void, ...args: any[]) { return this.client().watchAccount(async (data) => { let errMessage = '' if (data.isConnected) { try { await this.walletLoginPromise } catch (err) { errMessage = err.message } } callback({ ...data, isCustomWallet: this.isCustomWallet, errMessage, }) }, ...args) } // 内部的 disconnect async cancelLogin() { this.initialized = false this.initializedCallbackQueue = [] this.removeIframe() // disconnect need isCustomWallet value const disconnected = await this.client().disconnect() localStorage.removeItem(this.CUSTOM_WALLET_KEY) return disconnected } disconnect() { if (this.isCustomWallet) { // 等待 iframe 退出后发出401 return this.logout() } else { return this.client().disconnect() } } async getWalletClient(_chainId?: number) { const chainId = _chainId || await this.client().getNetwork()?.chain?.id const client = await getWalletClient({ chainId, }) return client ?? wagmiClientInstance.publicClient } async getSignerProvider() { const connector = this.client().getConnectorById('walletConnect') as Connector return connector.getProvider().then(res => res as EthereumProvider) } async getSignClient() { return this.getSignerProvider().then((provider) => { return provider.signer.rpcProviders.eip155 as IProvider }) } async switchNetwork(params: { chainId: number }): Promise { if (this.isCustomWallet) { const signClient = await this.getSignClient() const signer = (await this.getSignerProvider()).signer const session = signer.session const namespaces = signer.namespaces const namespace = signClient.namespace const accounts = namespace.accounts const hasChain = accounts.find((account: any) => account.includes(`eip155:${params.chainId}`)) const addresses = accounts.map((a: string) => a.split(':')[2]) namespaces.eip155 = namespace if (!hasChain) { namespace.accounts = accounts.concat(addresses.map((ad: string) => `eip155:${params.chainId}:${ad}`)) namespace.chains = namespace.chains.concat(`eip155:${params.chainId}`) await signClient.client.update({ topic: session.topic, namespaces: namespaces as any, }) } } return this.client().switchNetwork(params) } protected logoutWithIframe(url: string) { return createLogoutIframe(url) } get modal() { return ModalCtrl } get config() { return ConfigCtrl } get client(): () => EthereumClient { return ClientCtrl.client } get explorer() { return ExplorerCtrl } get options() { return OptionsCtrl as typeof import('@web3modal/core').OptionsCtrl } }