import { fromEventPattern, Observable } from 'rxjs' import { filter, takeUntil, take, share, switchMap } from 'rxjs/operators' import partition from 'lodash.partition' import { providers, utils } from 'ethers' import { weiToEth } from '@web3-onboard/common' import { disconnectWallet$ } from './streams.js' import { updateAccount, updateWallet } from './store/actions.js' import { validEnsChain } from './utils.js' import disconnect from './disconnect.js' import { state } from './store/index.js' import { getBNMulitChainSdk } from './services.js' import { configuration } from './configuration.js' import type { ChainId, EIP1102Request, EIP1193Provider, ProviderAccounts, Chain, AccountsListener, ChainListener, SelectAccountsRequest } from '@web3-onboard/common' import type { Account, Address, Balances, Ens, WalletPermission, WalletState } from './types.js' import type { Uns } from '@web3-onboard/unstoppable-resolution' import { updateSecondaryTokens } from './update-balances' export const ethersProviders: { [key: string]: providers.StaticJsonRpcProvider } = {} export function getProvider(chain: Chain): providers.StaticJsonRpcProvider { if (!chain) return null if (!ethersProviders[chain.rpcUrl]) { ethersProviders[chain.rpcUrl] = new providers.StaticJsonRpcProvider( chain.providerConnectionInfo && chain.providerConnectionInfo.url ? chain.providerConnectionInfo : chain.rpcUrl ) } return ethersProviders[chain.rpcUrl] } export function requestAccounts( provider: EIP1193Provider ): Promise { const args = { method: 'eth_requestAccounts' } as EIP1102Request return provider.request(args) } export function selectAccounts( provider: EIP1193Provider ): Promise { const args = { method: 'eth_selectAccounts' } as SelectAccountsRequest return provider.request(args) } export function getChainId(provider: EIP1193Provider): Promise { return provider.request({ method: 'eth_chainId' }) as Promise } export function listenAccountsChanged(args: { provider: EIP1193Provider disconnected$: Observable }): Observable { const { provider, disconnected$ } = args const addHandler = (handler: AccountsListener) => { provider.on('accountsChanged', handler) } const removeHandler = (handler: AccountsListener) => { provider.removeListener('accountsChanged', handler) } return fromEventPattern(addHandler, removeHandler).pipe( takeUntil(disconnected$) ) } export function listenChainChanged(args: { provider: EIP1193Provider disconnected$: Observable }): Observable { const { provider, disconnected$ } = args const addHandler = (handler: ChainListener) => { provider.on('chainChanged', handler) } const removeHandler = (handler: ChainListener) => { provider.removeListener('chainChanged', handler) } return fromEventPattern(addHandler, removeHandler).pipe( takeUntil(disconnected$) ) } export function trackWallet( provider: EIP1193Provider, label: WalletState['label'] ): void { const disconnected$ = disconnectWallet$.pipe( filter(wallet => wallet === label), take(1) ) const accountsChanged$ = listenAccountsChanged({ provider, disconnected$ }).pipe(share()) // when account changed, set it to first account and subscribe to events accountsChanged$.subscribe(async ([address]) => { // sync accounts with internal state // in the case of an account has been manually disconnected try { await syncWalletConnectedAccounts(label) } catch (error) { console.warn( 'Web3Onboard: Error whilst trying to sync connected accounts:', error ) } // no address, then no account connected, so disconnect wallet // this could happen if user locks wallet, // or if disconnects app from wallet if (!address) { disconnect({ label }) return } const { wallets } = state.get() const { accounts } = wallets.find(wallet => wallet.label === label) const [[existingAccount], restAccounts] = partition( accounts, account => account.address === address ) // update accounts without ens/uns and balance first updateWallet(label, { accounts: [ existingAccount || { address: address, ens: null, uns: null, balance: null }, ...restAccounts ] }) // if not existing account and notifications, // then subscribe to transaction events if (state.get().notify.enabled && !existingAccount) { const sdk = await getBNMulitChainSdk() if (sdk) { const wallet = state .get() .wallets.find(wallet => wallet.label === label) try { sdk.subscribe({ id: address, chainId: wallet.chains[0].id, type: 'account' }) } catch (error) { // unsupported network for transaction events } } } }) // also when accounts change, update Balance and ENS/UNS accountsChanged$ .pipe( switchMap(async ([address]) => { if (!address) return const { wallets, chains } = state.get() const primaryWallet = wallets.find(wallet => wallet.label === label) const { chains: walletChains, accounts } = primaryWallet const [connectedWalletChain] = walletChains const chain = chains.find( ({ namespace, id }) => namespace === 'evm' && id === connectedWalletChain.id ) const balanceProm = getBalance(address, chain) const secondaryTokenBal = updateSecondaryTokens( primaryWallet, address, chain ) const account = accounts.find(account => account.address === address) const ensChain = chains.find( ({ id }) => id === validEnsChain(connectedWalletChain.id) ) const ensProm = account && account.ens ? Promise.resolve(account.ens) : ensChain ? getEns(address, ensChain) : Promise.resolve(null) const unsProm = account && account.uns ? Promise.resolve(account.uns) : getUns(address, chain) return Promise.all([ Promise.resolve(address), balanceProm, ensProm, unsProm, secondaryTokenBal ]) }) ) .subscribe(res => { if (!res) return const [address, balance, ens, uns, secondaryTokens] = res updateAccount(label, address, { balance, ens, uns, secondaryTokens }) }) const chainChanged$ = listenChainChanged({ provider, disconnected$ }).pipe( share() ) // Update chain on wallet when chainId changed chainChanged$.subscribe(async chainId => { const { wallets } = state.get() const { chains, accounts } = wallets.find(wallet => wallet.label === label) const [connectedWalletChain] = chains if (chainId === connectedWalletChain.id) return if (state.get().notify.enabled) { const sdk = await getBNMulitChainSdk() if (sdk) { const wallet = state .get() .wallets.find(wallet => wallet.label === label) // Unsubscribe with timeout of 60 seconds // to allow for any currently inflight transactions wallet.accounts.forEach(({ address }) => { sdk.unsubscribe({ id: address, chainId: wallet.chains[0].id, timeout: 60000 }) }) // resubscribe for new chainId wallet.accounts.forEach(({ address }) => { try { sdk.subscribe({ id: address, chainId: chainId, type: 'account' }) } catch (error) { // unsupported network for transaction events } }) } } const resetAccounts = accounts.map( ({ address }) => ({ address, ens: null, uns: null, balance: null } as Account) ) updateWallet(label, { chains: [{ namespace: 'evm', id: chainId }], accounts: resetAccounts }) }) // when chain changes get ens/uns and balance for each account for wallet chainChanged$ .pipe( switchMap(async chainId => { const { wallets, chains } = state.get() const primaryWallet = wallets.find(wallet => wallet.label === label) const { accounts } = primaryWallet const chain = chains.find( ({ namespace, id }) => namespace === 'evm' && id === chainId ) return Promise.all( accounts.map(async ({ address }) => { const balanceProm = getBalance(address, chain) const secondaryTokenBal = updateSecondaryTokens( primaryWallet, address, chain ) const ensChain = chains.find( ({ id }) => id === validEnsChain(chainId) ) const ensProm = ensChain ? getEns(address, ensChain) : Promise.resolve(null) const unsProm = validEnsChain(chainId) ? getUns(address, ensChain) : Promise.resolve(null) const [balance, ens, uns, secondaryTokens] = await Promise.all([ balanceProm, ensProm, unsProm, secondaryTokenBal ]) return { address, balance, ens, uns, secondaryTokens } }) ) }) ) .subscribe(updatedAccounts => { updatedAccounts && updateWallet(label, { accounts: updatedAccounts }) }) disconnected$.subscribe(() => { provider.disconnect && provider.disconnect() }) } export async function getEns( address: Address, chain: Chain ): Promise { // chain we don't recognize and don't have a rpcUrl for requests if (!chain) return null const provider = getProvider(chain) try { const name = await provider.lookupAddress(address) let ens = null if (name) { const resolver = await provider.getResolver(name) if (resolver) { const [contentHash, avatar] = await Promise.all([ resolver.getContentHash(), resolver.getAvatar() ]) const getText = resolver.getText.bind(resolver) ens = { name, avatar, contentHash, getText } } } return ens } catch (error) { console.error(error) return null } } export async function getUns( address: Address, chain: Chain ): Promise { const { unstoppableResolution } = configuration // check if address is valid ETH address before attempting to resolve // chain we don't recognize and don't have a rpcUrl for requests if (!unstoppableResolution || !utils.isAddress(address) || !chain) return null try { return await unstoppableResolution(address) } catch (error) { console.error(error) return null } } export async function getBalance( address: string, chain: Chain ): Promise { // chain we don't recognize and don't have a rpcUrl for requests if (!chain) return null const { wallets } = state.get() try { const wallet = wallets.find(wallet => !!wallet.provider) const provider = wallet.provider const balanceHex = await provider.request({ method: 'eth_getBalance', params: [address, 'latest'] }) return balanceHex ? { [chain.token || 'eth']: weiToEth(balanceHex) } : null } catch (error) { console.error(error) return null } } export function switchChain( provider: EIP1193Provider, chainId: ChainId ): Promise { return provider.request({ method: 'wallet_switchEthereumChain', params: [{ chainId }] }) } export function addNewChain( provider: EIP1193Provider, chain: Chain ): Promise { return provider.request({ method: 'wallet_addEthereumChain', params: [ { chainId: chain.id, chainName: chain.label, nativeCurrency: { name: chain.label, symbol: chain.token, decimals: 18 }, rpcUrls: [chain.publicRpcUrl || chain.rpcUrl], blockExplorerUrls: chain.blockExplorerUrl ? [chain.blockExplorerUrl] : undefined } ] }) } export function updateChainRPC( provider: EIP1193Provider, chain: Chain, rpcUrl: string ): Promise { return provider.request({ method: 'wallet_addEthereumChain', params: [ { chainId: chain.id, chainName: chain.label, nativeCurrency: { name: chain.label, symbol: chain.token, decimals: 18 }, rpcUrls: [rpcUrl], blockExplorerUrls: chain.blockExplorerUrl ? [chain.blockExplorerUrl] : undefined } ] }) } export async function getPermissions( provider: EIP1193Provider ): Promise { try { const permissions = (await provider.request({ method: 'wallet_getPermissions' })) as WalletPermission[] return Array.isArray(permissions) ? permissions : [] } catch (error) { return [] } } export async function syncWalletConnectedAccounts( label: WalletState['label'] ): Promise { const wallet = state.get().wallets.find(wallet => wallet.label === label) const permissions = await getPermissions(wallet.provider) const accountsPermissions = permissions.find( ({ parentCapability }) => parentCapability === 'eth_accounts' ) if (accountsPermissions) { const { value: connectedAccounts } = accountsPermissions.caveats.find( ({ type }) => type === 'restrictReturnedAccounts' ) || { value: null } if (connectedAccounts) { const syncedAccounts = wallet.accounts.filter(({ address }) => connectedAccounts.includes(address) ) updateWallet(wallet.label, { ...wallet, accounts: syncedAccounts }) } } }