import './Dashboard.css' import { CheckCircleTwoTone, CloseCircleTwoTone, DeleteOutlined, SyncOutlined, WalletOutlined, } from '@ant-design/icons' import { useWeb3React } from '@web3-react/core' import { Avatar, Badge, Button, Col, Input, Modal, Row, Skeleton, Table, Tooltip, Typography, } from 'antd' import { Content } from 'antd/lib/layout/layout' import BigNumber from 'bignumber.js' import { ethers } from 'ethers' import React, { useEffect, useState } from 'react' import { useMetatags } from '../hooks/useMetatags' import { getTokenBalancesForChainsFromDebank } from '../services/balances' import { readWallets } from '../services/localStorage' import { useStomt } from '../services/stomt' import { isWalletDeactivated } from '../services/utils' import { Amounts, ChainId, ChainKey, chainKeysToObject, Coin, CoinKey, ColomnType, DataType, defaultCoins, getChainById, SummaryAmounts, supportedChains, TokenAmount, Wallet, WalletSummary, } from '../types' import ConnectButton from './web3/ConnectButton' const visibleChains: ChainId[] = [ ChainId.ETH, ChainId.POL, ChainId.BSC, ChainId.DAI, ChainId.FTM, ChainId.OKT, ChainId.AVA, ChainId.ARB, ChainId.HEC, ChainId.MOR, ChainId.CEL, ChainId.OPT, ChainId.CRO, ChainId.BOB, ChainId.MOO, ChainId.MAM, ChainId.FUS, ChainId.ONE, ] const emptySummaryAmounts: SummaryAmounts = { amount_usd: new BigNumber(0), percentage_of_portfolio: new BigNumber(0), } const emptyWallet = { address: '0x..', loading: true, portfolio: chainKeysToObject([]), } let coins = defaultCoins.filter((coin) => coin.key !== CoinKey.TEST) // individual render functions function renderAmounts( amounts: Amounts = { amount_coin: new BigNumber(-1), amount_usd: new BigNumber(-1) }, ) { if (amounts.amount_coin.eq(-1)) { // loading return (
) } else if (amounts.amount_coin.isZero()) { // empty return (
00.0000
00.00
) } else { // default return (
{amounts.amount_coin.toFixed(4)}
{amounts.amount_usd.toFixed(2)} USD
) } } function renderCoin(coin: Coin) { return (
{coin.key} ) : ( ) }>
) } function renderSummary(summary: SummaryAmounts) { summary.amount_usd = new BigNumber(summary.amount_usd) // Ugly fix return (
{summary.amount_usd.toFixed(2)} USD
{summary.percentage_of_portfolio.shiftedBy(2).toFixed(2)} % of portfolio
) } const showGasModal = (gas: ChainKey) => { switch (gas) { case ChainKey.ETH: Modal.info({ title: 'Gas Info for ethereum chain', content: (

Find out how to get ETH:

Where to buy ETH (by ethereum.org)
), }) break case ChainKey.POL: Modal.info({ title: 'Gas Info for Polygon/Matic chain', content: (

You can get some free MATIC using those faucets. It should be enough to exchange or move your funds.

), }) break case ChainKey.BSC: Modal.info({ title: 'Gas Info for Binance Smart Chain', content: (

You need to buy BNB.

e.g. on{' '} binance.com
), }) break case ChainKey.DAI: Modal.info({ title: 'Gas Info for Gnosis chain', content: (

You can get some free DAI using those faucets. It should be enough to exchange or move your funds.

), }) break case ChainKey.FTM: Modal.info({ title: 'Gas Info for FTM chain', content: (

Find out how to get ETH:

Where to buy ETH (by fantom.foundation)
), }) break case ChainKey.OKT: Modal.info({ title: 'Gas Info for OKExChain chain', content: (

Find out how to get OKT:

Where to buy OKT (by okex.com)
), }) break case ChainKey.AVA: Modal.info({ title: 'Gas Info for Avalanche chain', content: (

Find out how to get AVAX:

Where to buy AVAX (by avax.network)
), }) break case ChainKey.RIN: Modal.info({ title: 'Gas Info for Rinkeby Testnet', content: (

Find out how to get ETH:

Official Faucet
), }) break case ChainKey.GOR: Modal.info({ title: 'Gas Info for Goerli Testnet', content: (

Find out how to get ETH:

Faucet
), }) break } } const tooltipsEmpty: { [ChainKey: string]: JSX.Element } = {} const tooltips: { [ChainKey: string]: any } = {} for (const chainId of visibleChains) { const chain = getChainById(chainId) tooltips[chain.key] = ( <> The ${chain.name} chain requires ${chain.coin} to pay for gas. ) tooltipsEmpty[chain.key] = ( <> The ${chain.name} requires ${chain.coin} to pay for gas. Without it you won't be able to do anything on this chain. ) } // render formatters function renderGas(wallet: Wallet, chainKey: ChainKey, coinName: CoinKey) { const coin = coins.find((coin) => coin.key === coinName) const isChainUsed = wallet.portfolio[chainKey]?.length > 0 const inPortfolio = wallet.portfolio[chainKey]?.find( (tokenAmount) => tokenAmount.address === '0x0000000000000000000000000000000000000000' || tokenAmount.address === coin?.chains[chainKey]?.address, ) const amounts: Amounts = inPortfolio ? parsePortfolioToAmount(inPortfolio) : { amount_coin: new BigNumber(0), amount_usd: new BigNumber(0) } const tooltipEmpty = tooltipsEmpty[chainKey] const tooltip = tooltips[chainKey] return (
{amounts.amount_coin.eq(-1) ? ( {coinName}:{' '} ) : amounts.amount_coin.isZero() ? ( {coinName}: - ) : ( {coinName}: {amounts.amount_coin.toFixed(4)} )}
) } const renderWalletColumnTitle = ( address: string, syncHandler: Function, deleteHandler: Function, ) => { return ( {`${address.substr(0, 4)}...${address.substr(-4, 4)}`} syncHandler()} /> deleteHandler()} /> ) } //Helpers const parsePortfolioToAmount = (inPortfolio: TokenAmount) => { const amount = new BigNumber(inPortfolio.amount) const subAmounts: Amounts = { amount_coin: amount, amount_usd: amount.times(inPortfolio.priceUSD || '0'), } return subAmounts } const baseWidth = 150 const buildColumnForWallet = (wallet: Wallet, syncHandler: Function, deleteHandler: Function) => { const walletColumn: ColomnType = { title: renderWalletColumnTitle(wallet.address, syncHandler, deleteHandler), dataIndex: wallet.address, children: supportedChains .filter((chain) => visibleChains.includes(chain.id)) .map((chain) => { return { title: chain.name, children: [ { title: renderGas(wallet, chain.key, chain.coin), dataIndex: `${wallet.address}_${chain.key}`, width: baseWidth, render: renderAmounts, }, ], } }), } return walletColumn } const coinColumn: ColomnType = { title: 'Coins', fixed: 'left', dataIndex: 'coin', render: renderCoin, width: 60, } const portfolioColumn: ColomnType = { title: 'Portfolio', width: baseWidth + 10, dataIndex: 'portfolio', render: renderAmounts, } const initialColumns = [ coinColumn, portfolioColumn, buildColumnForWallet( emptyWallet, () => {}, () => {}, ), { title: , dataIndex: '', width: 100, }, ] const initialRows: Array = coins.map((coin) => { return { key: coin.key, coin: coin as Coin, portfolio: { amount_coin: new BigNumber(-1), amount_usd: new BigNumber(-1) }, ...chainKeysToObject({ amount_coin: new BigNumber(-1), amount_usd: new BigNumber(-1) }), } }) const calculateWalletSummary = (wallet: Wallet, totalSumUsd: BigNumber) => { var summary: WalletSummary = { wallet: wallet.address, chains: chainKeysToObject({ amount_usd: new BigNumber(0), percentage_of_portfolio: new BigNumber(0), }), } visibleChains.forEach((chainId) => { const chain = getChainById(chainId) wallet.portfolio[chain.key]?.forEach((tokenAmount) => { summary.chains[chain.key].amount_usd = new BigNumber(summary.chains[chain.key].amount_usd) // chainKeysToObject retruns string summary.chains[chain.key].amount_usd = tokenAmount.priceUSD ? summary.chains[chain.key].amount_usd.plus( new BigNumber(tokenAmount.amount).times(new BigNumber(tokenAmount.priceUSD)), ) : summary.chains[chain.key].amount_usd }) summary.chains[chain.key].percentage_of_portfolio = wallet.portfolio[chain.key] ?.reduce( (sum, current) => current.priceUSD ? sum.plus(new BigNumber(current.amount).times(new BigNumber(current.priceUSD))) : sum, new BigNumber(0), ) .div(totalSumUsd) }) return summary } // actual component const Dashboard = () => { useMetatags({ title: 'LI.FI - Dashboard', }) useStomt('dashboard') const [registeredWallets, setRegisteredWallets] = useState>(() => readWallets().map((address) => ({ address: address, loading: false, portfolio: chainKeysToObject([]), })), ) const web3 = useWeb3React() const [columns, setColumns] = useState>(initialColumns) const [walletModalVisible, setWalletModalVisible] = useState(false) const [walletModalAddress, setWalletModalAddress] = useState('') const [walletModalLoading, setWalletModalLoading] = useState(false) const [data, setData] = useState>(initialRows) // fixes react warning // https://stackoverflow.com/a/63144665 const [isSubscribed, setIsSubscribed] = useState(true) const deleteWallet = (wallet: Wallet) => { setRegisteredWallets((registeredWallets) => registeredWallets.filter((item) => item.address !== wallet.address), ) } // update registeredWallets on activate or deactivate useEffect(() => { if (isWalletDeactivated(web3.account)) return setRegisteredWallets( readWallets().map((address) => ({ address: address, loading: false, portfolio: chainKeysToObject([]), })), ) }, [web3.account]) const buildWalletColumns = () => { var walletColumns: Array = [] registeredWallets.forEach((wallet) => { const col = buildColumnForWallet( wallet, () => updateWalletPortfolio(wallet), () => deleteWallet(wallet), ) walletColumns.push(col) }) return walletColumns } const buildColumns = (initialBuild: boolean = false) => { const columns = [coinColumn, portfolioColumn] if (initialBuild) { buildColumnForWallet( emptyWallet, () => {}, () => {}, ) } else { buildWalletColumns().map((column) => columns.push(column)) } columns.push({ title: ( ), dataIndex: '', width: 100, }) return columns } //builders const buildRows = () => { const generatedRows: Array = [] coins.forEach((coin) => { // row object const coinRow: DataType = { key: coin.key, coin: coin as Coin, portfolio: { amount_coin: new BigNumber(0), amount_usd: new BigNumber(0), }, } registeredWallets.forEach((wallet) => { for (const chainId of visibleChains) { const chain = getChainById(chainId) const emptyAmounts: Amounts = { amount_coin: wallet.loading ? new BigNumber(-1) : new BigNumber(0), amount_usd: wallet.loading ? new BigNumber(-1) : new BigNumber(0), } const inPortfolio = wallet.portfolio[chain.key].find( (e) => e.address === coin.chains[chain.id]?.address, ) const cellContent: Amounts = inPortfolio ? parsePortfolioToAmount(inPortfolio) : emptyAmounts coinRow[`${wallet.address}_${chain.key}`] = cellContent if (cellContent.amount_coin.gt(0)) { coinRow.portfolio.amount_coin = coinRow.portfolio.amount_coin.plus( cellContent.amount_coin, ) coinRow.portfolio.amount_usd = coinRow.portfolio.amount_usd.plus(cellContent.amount_usd) } } // for each chain }) // for each wallet generatedRows.push(coinRow) }) // for each coin // sort generatedRows.sort((a, b) => b.portfolio.amount_coin.minus(a.portfolio.amount_coin).toNumber()) // DESC token generatedRows.sort((a, b) => b.portfolio.amount_usd.minus(a.portfolio.amount_usd).toNumber()) // DESC usd return generatedRows } const updateWalletPortfolio = async (wallet: Wallet) => { wallet.loading = true setData(buildRows) // set loading state const portfolio: { [ChainKey: string]: TokenAmount[] } = await getTokenBalancesForChainsFromDebank(wallet.address) for (const chainId of visibleChains) { const chain = getChainById(chainId) const chainPortfolio = portfolio[chain.id] wallet.portfolio[chain.key] = chainPortfolio // add new coins chainPortfolio.forEach((token) => { const exists = coins.find( (existingCoin) => existingCoin.chains[chain.id]?.address === token.address, ) if (!exists) { let symbolExists = coins.find((existingCoin) => existingCoin.key === token.symbol) let symbol = token.symbol if (symbolExists) { let symbol_id = 0 while (symbolExists) { symbol_id += 1 // eslint-disable-next-line no-loop-func symbolExists = coins.find( // eslint-disable-next-line no-loop-func (existingCoin) => existingCoin.key === token.symbol + '_' + symbol_id, ) } symbol += '_' + symbol_id } let newCoin: Coin = { key: symbol as CoinKey, name: token.name, logoURI: token.logoURI || '', chains: chainKeysToObject(token), verified: false, } coins.push(newCoin) } }) } wallet.loading = false if (isSubscribed) { setRegisteredWallets( registeredWallets.map((old) => (old.address === wallet.address ? wallet : old)), ) setColumns(buildColumns(false)) setData(buildRows) } } const updateEntirePortfolio = () => { registeredWallets.forEach(async (wallet) => { await updateWalletPortfolio(wallet) }) } useEffect(() => { setIsSubscribed(true) return () => { setIsSubscribed(false) } }, []) useEffect(() => { if (!registeredWallets.length) { setWalletModalVisible(true) setColumns(initialColumns) setData(initialRows) } else { updateEntirePortfolio() setColumns(buildColumns()) setData(buildRows) } // eslint-disable-next-line }, [registeredWallets.length]) const addWallet = (address: string) => { const newWallet = { address: address, loading: true, portfolio: chainKeysToObject([]), } setRegisteredWallets((registeredWallets) => [...registeredWallets, newWallet]) } // Add Wallet Modal Handlers const resolveEnsName = async (name: string) => { const ethereum = new ethers.providers.JsonRpcProvider(process.env.REACT_APP_RPC_URL_MAINNET) return ethereum.resolveName(name) } const handleWalletModalAdd = async () => { setWalletModalLoading(true) let address = getModalAddressSuggestion() if (address.endsWith('.eth')) { address = (await resolveEnsName(address)) || '' } if (ethers.utils.isAddress(address)) { setWalletModalVisible(false) setWalletModalAddress(' ') addWallet(address) } setWalletModalLoading(false) } const handleWalletModalClose = () => { if (!registeredWallets.length) { setWalletModalVisible(true) } else { setWalletModalVisible(false) } } const getModalAddressSuggestion = () => { const addedWallets = registeredWallets.map((wallet) => wallet.address) const web3Account = web3.account && addedWallets.indexOf(web3.account) === -1 ? web3.account : '' return walletModalAddress || web3Account } let summaryIndex = 0 return (
( SUM {renderSummary( !registeredWallets.length ? emptySummaryAmounts : ({ amount_usd: data.reduce( (sum, curr) => sum.plus(curr.portfolio.amount_usd), new BigNumber(0), ), percentage_of_portfolio: new BigNumber(1), } as SummaryAmounts), )} {registeredWallets.map((wallet: Wallet) => { const total = data.reduce( (sum, curr) => sum.plus(curr.portfolio.amount_usd), new BigNumber(0), ) const summary = calculateWalletSummary(wallet, total) return supportedChains .filter((chain) => visibleChains.includes(chain.id)) .map((chain) => { return ( {wallet.loading ? renderSummary({ amount_usd: new BigNumber(0), percentage_of_portfolio: new BigNumber(0), }) : renderSummary(summary.chains[chain.key])} ) }) })} )} /> Close ) : ( '' ), , ]}>
{!web3.account ? ( ) : ( )}
Temporarily inspect a wallet address / ens domain: } onChange={(event) => setWalletModalAddress(event.target.value)} disabled={walletModalLoading} style={{ borderRadius: 6, }} />
) } export default Dashboard