import React, { PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useWallet as useSolanaAdapterReactWallet } from '@solana/wallet-adapter-react' import { OnboardAPI, WalletState } from '@web3-onboard/core' import { ethers } from 'ethers' import { instantiateOnboard } from '../../core' import { ChainType, Chains, PolkadotExtensionState, ProviderConfigs, SolanaWalletAdapterState } from '../../core/interfaces' import { usePolkadotConnector } from '../../store/polkadot-connector/hook' import { useSolanaConnector } from '../../store/solana-connector/hook' import { WalletContext, WalletContextProps, useChains } from '../../store/wallet' import { clearConnectedWalletCache, constructPolkadotApi, getConnectedWalletCache, getPolkadotExtensionStateByExtensionLabel, getSolanaWalletAdapterStateByAdapterName, setConnectedWalletCache, switchEvmChain, userCanceledError } from '../../utils' import { PolkadotConnector } from '../PolkadotConnector/PolkadotConnector' import { PolkadotConnectorProvider } from '../PolkadotConnector/PolkadotConnectorProvider' import { SolanaConnector } from '../SolanaConnector/SolanaConnector' import { SolanaConnectorProvider } from '../SolanaConnector/SolanaConnectorProvider' import { ChainProvider } from './ChainProvider' interface WalletProviderProps { appName?: string prefixCls?: string theme?: 'light' | 'dark' providerConfigs: ProviderConfigs chains: Chains } export const WalletProvider: React.FC> = (props) => { const { children, chains, providerConfigs, ...restProps } = props return ( {children} ) } interface WalletProviderImplProps { appName?: string prefixCls?: string theme?: 'light' | 'dark' providerConfigs: ProviderConfigs } const WalletProviderImpl: React.FC> = ({ children, appName, prefixCls = 'ata-', theme, providerConfigs }) => { const { chainId, setChainId, chains } = useChains() const onboardAPI = useRef() const polkadotConnector = usePolkadotConnector() const solanaConnector = useSolanaConnector() const _solanaWallet = useSolanaAdapterReactWallet() const { wallets: solanaAdapterReactWallets, select: solanaAdapterReactSelect, connect: solanaAdapterReactConnect, disconnect: solanaAdapterReactDisconnect } = _solanaWallet const solanaWallet = useMemo(() => { const chain = chains.find((item) => item.chainId === chainId) if (chain && chain.type === ChainType.Solana) { return _solanaWallet } }, [chainId, chains, _solanaWallet]) const [account, setAccount] = useState() const [ethersProvider, setEthersProvider] = useState() const [polkadotApi, setPolkadotApi] = useState() const [walletState, setWalletState] = useState() const [loading, setLoading] = useState(true) const [reconnected, setReconnected] = useState(false) const connectPolkadotApi = useCallback( async (chainId) => { const api = await constructPolkadotApi({ chainId, chains }) if (api) { await api.connect() } setPolkadotApi(api) }, [chains, setPolkadotApi] ) const clearState = useCallback(() => { onboardAPI.current = undefined setAccount(undefined) setChainId(undefined) setEthersProvider(undefined) setPolkadotApi(undefined) setWalletState(undefined) }, [setAccount, setChainId, setEthersProvider, setPolkadotApi, setWalletState]) const clearEVMState = useCallback(() => { onboardAPI.current = undefined setEthersProvider(undefined) }, [setEthersProvider]) const clearPolkadotState = useCallback(() => { setPolkadotApi(undefined) }, [setPolkadotApi]) async function recoverState(options: { chainId: string wallets?: WalletState[] polkadotExtensionState?: PolkadotExtensionState solanaWalletAdapterState?: SolanaWalletAdapterState }) { const { chainId, wallets, polkadotExtensionState, solanaWalletAdapterState } = options if (wallets) { const firstWallet = wallets[0] if (firstWallet) { const { label, icon, provider } = firstWallet setConnectedWalletCache({ providerLabel: label, chainId }) const ethersProvider = new ethers.providers.Web3Provider(provider) await ethersProvider.send('eth_requestAccounts', []) const chain = chains.find((item) => item.chainId === chainId) if (chain) { await switchEvmChain({ ethersProvider, chain }) } const accounts = await ethersProvider.listAccounts() const account = accounts && accounts.length ? accounts[0] : undefined setAccount(account) setChainId(chainId) setEthersProvider(ethersProvider) setWalletState({ label, icon, evmProvider: provider, accounts }) } } else if (polkadotExtensionState) { const { label, icon, injected, accounts } = polkadotExtensionState setConnectedWalletCache({ providerLabel: label, chainId }) const account = accounts && accounts.length ? accounts[0] : undefined await connectPolkadotApi(chainId) setAccount(account) setChainId(chainId) setWalletState({ label, icon, polkadotInjected: injected, accounts }) } else if (solanaWalletAdapterState) { const { label, icon, adapter, accounts } = solanaWalletAdapterState setConnectedWalletCache({ providerLabel: label, chainId }) const account = accounts && accounts.length ? accounts[0] : undefined setAccount(account) setChainId(chainId) setWalletState({ label, icon, solanaWalletAdapter: adapter, accounts }) } } const reconnect = useCallback(async () => { try { const connectedWalletCache = getConnectedWalletCache() if (connectedWalletCache != null) { const chain = chains.find((item) => item.chainId === connectedWalletCache.chainId) if (!chain) { return } setLoading(true) const { type, chainId } = chain if (type === ChainType.EVM) { if (!onboardAPI.current) { onboardAPI.current = onboardAPI.current || instantiateOnboard(providerConfigs, chains) } if (onboardAPI.current) { const wallets = await onboardAPI.current.connectWallet({ autoSelect: { label: connectedWalletCache.providerLabel, disableModals: true } }) await recoverState({ chainId: chainId, wallets }) } } else if (type === ChainType.Polkadot) { // need to wait until the extension inject in to browser await new Promise((resolve) => { setTimeout(() => { resolve() }, 1 * 500) }) const polkadotExtensionState = await getPolkadotExtensionStateByExtensionLabel({ appName, label: connectedWalletCache.providerLabel, providerConfigs }) await recoverState({ chainId, polkadotExtensionState }) } else if (type === ChainType.Solana) { const solanaWalletAdapterState = await getSolanaWalletAdapterStateByAdapterName({ name: connectedWalletCache.providerLabel, wallets: solanaAdapterReactWallets, select: solanaAdapterReactSelect, connect: solanaAdapterReactConnect }) await recoverState({ chainId, solanaWalletAdapterState }) } } } finally { setLoading(false) } }, [ solanaAdapterReactWallets, solanaAdapterReactSelect, solanaAdapterReactConnect, providerConfigs, chains, appName ]) const disconnectEVM = useCallback(async () => { if (onboardAPI.current) { const [primaryWallet] = onboardAPI.current.state.get().wallets if (primaryWallet) { await onboardAPI.current.disconnectWallet({ label: primaryWallet.label }) } } }, []) const disconnectPolkadot = useCallback(async () => { if (polkadotApi != null) { await polkadotApi.disconnect() } }, [polkadotApi]) const disconnectSolana = useCallback(async () => { await solanaAdapterReactDisconnect() }, [solanaAdapterReactDisconnect]) const disconnect = useCallback(async () => { const chain = chains.find((item) => item.chainId === chainId) if (!chain) { return } const { type } = chain if (type === ChainType.EVM) { await disconnectEVM() } else if (type === ChainType.Polkadot) { await disconnectPolkadot() } else if (type === ChainType.Solana) { await disconnectSolana() } clearState() clearConnectedWalletCache() }, [clearState, disconnectEVM, disconnectPolkadot, chains, chainId]) const switchChain = async (chainId: string) => { const chain = chains.find((item) => item.chainId === chainId) if (!chain) { throw new Error('Chain is not existed') } const { type } = chain if (type === ChainType.EVM) { if (!ethersProvider || polkadotApi || walletState?.solanaWalletAdapter) { await connect(chainId) await disconnectPolkadot() await disconnectSolana() clearPolkadotState() return } await switchEvmChain({ ethersProvider, chain }) setChainId(chainId) } else if (type === ChainType.Polkadot) { if (!polkadotApi || ethersProvider || walletState?.solanaWalletAdapter) { await connect(chainId) await disconnectEVM() await disconnectSolana() clearEVMState() return } await connectPolkadotApi(chainId) setChainId(chainId) } else if (type === ChainType.Solana) { if (!walletState || !walletState.solanaWalletAdapter || ethersProvider || polkadotApi) { await connect(chainId) await disconnectEVM() await disconnectPolkadot() clearEVMState() clearPolkadotState() return } setChainId(chainId) } } const connect = useCallback( async (chainId: string) => { // eslint-disable-next-line no-useless-catch const chain = chains.find((item) => item.chainId === chainId) if (!chain) { throw new Error('Chain is not existed') } setLoading(true) const { type } = chain try { if (type === ChainType.EVM) { if (!onboardAPI.current) { onboardAPI.current = instantiateOnboard(providerConfigs, chains) } if (onboardAPI.current) { const wallets = await onboardAPI.current.connectWallet() if (wallets.length === 0) { throw userCanceledError } await recoverState({ chainId, wallets }) } } else if (type === ChainType.Polkadot) { const extensionState = await polkadotConnector.connect(providerConfigs) await recoverState({ chainId, polkadotExtensionState: extensionState }) } else if (type === ChainType.Solana) { const walletAdapterState = await solanaConnector.connect() await recoverState({ chainId, solanaWalletAdapterState: walletAdapterState }) } } finally { setLoading(false) } }, [setLoading, providerConfigs, chains, polkadotConnector] ) useEffect(() => { if (!reconnected) { setReconnected(true) reconnect() } }, [reconnected, reconnect]) useEffect(() => { function onChainChanged(chainId: number | string) { setChainId(`${chainId}`) } function onAccountsChanged(accounts: string[]) { if (!accounts[0]) { return disconnect() } setAccount(accounts[0]) } if (ethersProvider) { ethersProvider.on('chainChanged', onChainChanged) ethersProvider.on('accountsChanged', onAccountsChanged) } return () => { if (ethersProvider) { ethersProvider.off('chainChanged', onChainChanged) ethersProvider.off('accountsChanged', onAccountsChanged) } } }, [account, disconnect]) return ( {children} ) }