UNPKG

saepenatus

Version:

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 javascript UI with code splitting, CSS customization, mul

504 lines (428 loc) 13 kB
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 { Resolution } from '@unstoppabledomains/resolution' import type { ChainId, EIP1102Request, EIP1193Provider, ProviderAccounts, Chain, AccountsListener, ChainListener, SelectAccountsRequest } from '@web3-onboard/common' import type { Account, Address, Balances, Ens, Uns, WalletPermission, WalletState } from './types.js' 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<ProviderAccounts> { const args = { method: 'eth_requestAccounts' } as EIP1102Request return provider.request(args) } export function selectAccounts( provider: EIP1193Provider ): Promise<ProviderAccounts> { const args = { method: 'eth_selectAccounts' } as SelectAccountsRequest return provider.request(args) } export function getChainId(provider: EIP1193Provider): Promise<string> { return provider.request({ method: 'eth_chainId' }) as Promise<string> } export function listenAccountsChanged(args: { provider: EIP1193Provider disconnected$: Observable<string> }): Observable<ProviderAccounts> { const { provider, disconnected$ } = args const addHandler = (handler: AccountsListener) => { provider.on('accountsChanged', handler) } const removeHandler = (handler: AccountsListener) => { provider.removeListener('accountsChanged', handler) } return fromEventPattern<ProviderAccounts>(addHandler, removeHandler).pipe( takeUntil(disconnected$) ) } export function listenChainChanged(args: { provider: EIP1193Provider disconnected$: Observable<string> }): Observable<ChainId> { const { provider, disconnected$ } = args const addHandler = (handler: ChainListener) => { provider.on('chainChanged', handler) } const removeHandler = (handler: ChainListener) => { provider.removeListener('chainChanged', handler) } return fromEventPattern<ChainId>(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 { chains: walletChains, accounts } = wallets.find( wallet => wallet.label === label ) const [connectedWalletChain] = walletChains const chain = chains.find( ({ namespace, id }) => namespace === 'evm' && id === connectedWalletChain.id ) const balanceProm = getBalance(address, chain) const account = accounts.find(account => account.address === address) const ensProm = account && account.ens ? Promise.resolve(account.ens) : validEnsChain(connectedWalletChain.id) ? getEns(address, chain) : Promise.resolve(null) const unsProm = account && account.uns ? Promise.resolve(account.uns) : getUns(address, chain) return Promise.all([ Promise.resolve(address), balanceProm, ensProm, unsProm ]) }) ) .subscribe(res => { if (!res) return const [address, balance, ens, uns] = res updateAccount(label, address, { balance, ens, uns }) }) 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 { accounts } = wallets.find(wallet => wallet.label === label) const chain = chains.find( ({ namespace, id }) => namespace === 'evm' && id === chainId ) return Promise.all( accounts.map(async ({ address }) => { const balanceProm = getBalance(address, chain) const ensProm = validEnsChain(chainId) ? getEns(address, chain) : Promise.resolve(null) const unsProm = validEnsChain(chainId) ? getUns(address, chain) : Promise.resolve(null) const [balance, ens, uns] = await Promise.all([ balanceProm, ensProm, unsProm ]) return { address, balance, ens, uns } }) ) }) ) .subscribe(updatedAccounts => { updatedAccounts && updateWallet(label, { accounts: updatedAccounts }) }) disconnected$.subscribe(() => { provider.disconnect && provider.disconnect() }) } export async function getEns( address: Address, chain: Chain ): Promise<Ens | null> { // 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<Uns | null> { // 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 (!utils.isAddress(address) || !chain) return null const resolutionInstance = new Resolution() try { const name = await resolutionInstance.reverse(address) let uns = null if (name) { uns = { name } } return uns } catch (error) { console.error(error) return null } } export async function getBalance( address: string, chain: Chain ): Promise<Balances | null> { // 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<unknown> { return provider.request({ method: 'wallet_switchEthereumChain', params: [{ chainId }] }) } export function addNewChain( provider: EIP1193Provider, chain: Chain ): Promise<unknown> { 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 async function getPermissions( provider: EIP1193Provider ): Promise<WalletPermission[]> { 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<void> { 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 }) } } }