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 (
)
} else {
// default
return (
{amounts.amount_coin.toFixed(4)}
{amounts.amount_usd.toFixed(2)} USD
)
}
}
function renderCoin(coin: Coin) {
return (
)
}
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: (
),
})
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: (
),
})
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: (
),
})
break
case ChainKey.OKT:
Modal.info({
title: 'Gas Info for OKExChain chain',
content: (
),
})
break
case ChainKey.AVA:
Modal.info({
title: 'Gas Info for Avalanche chain',
content: (
),
})
break
case ChainKey.RIN:
Modal.info({
title: 'Gas Info for Rinkeby Testnet',
content: (
),
})
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