From 5677627487e575a5921617790dd059159884b320 Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Fri, 19 Jan 2024 10:25:47 +0100 Subject: [PATCH] Show Tinlake portfolio (#1873) --- .../InvestRedeemTinlakeProvider.tsx | 2 +- .../Portfolio/CardPortfolioValue.tsx | 13 ++- .../src/components/Portfolio/Holdings.tsx | 90 +++++++++++-------- centrifuge-app/src/config.ts | 2 + centrifuge-app/src/pages/Portfolio/index.tsx | 4 +- centrifuge-app/src/utils/address.ts | 8 ++ .../src/utils/tinlake/currencies.ts | 8 ++ .../src/utils/tinlake/useTinlakeBalances.ts | 54 +++++++---- centrifuge-js/src/index.ts | 1 + 9 files changed, 118 insertions(+), 64 deletions(-) create mode 100644 centrifuge-app/src/utils/address.ts diff --git a/centrifuge-app/src/components/InvestRedeem/InvestRedeemTinlakeProvider.tsx b/centrifuge-app/src/components/InvestRedeem/InvestRedeemTinlakeProvider.tsx index ee960ef8a0..f663c5f629 100644 --- a/centrifuge-app/src/components/InvestRedeem/InvestRedeemTinlakeProvider.tsx +++ b/centrifuge-app/src/components/InvestRedeem/InvestRedeemTinlakeProvider.tsx @@ -28,7 +28,7 @@ export function InvestRedeemTinlakeProvider({ poolId, trancheId, children }: Pro if (!tranche) throw new Error(`Token not found. Pool id: ${poolId}, token id: ${trancheId}`) const { data: investment, refetch: refetchInvestment } = useTinlakeInvestments(poolId, address) - const { data: balances, refetch: refetchBalances, isLoading: isBalancesLoading } = useTinlakeBalances() + const { data: balances, refetch: refetchBalances, isLoading: isBalancesLoading } = useTinlakeBalances(address) const { data: nativeBalance, refetch: refetchBalance, isLoading: isBalanceLoading } = useNativeBalance() const { data: permissions, isLoading: isPermissionsLoading } = useTinlakePermissions(poolId, address) const trancheInvestment = investment?.[seniority] diff --git a/centrifuge-app/src/components/Portfolio/CardPortfolioValue.tsx b/centrifuge-app/src/components/Portfolio/CardPortfolioValue.tsx index e1c2403d04..06c05d10ae 100644 --- a/centrifuge-app/src/components/Portfolio/CardPortfolioValue.tsx +++ b/centrifuge-app/src/components/Portfolio/CardPortfolioValue.tsx @@ -5,8 +5,8 @@ import { config } from '../../config' import { Dec } from '../../utils/Decimal' import { formatBalance } from '../../utils/formatting' import { useTransactionsByAddress } from '../../utils/usePools' +import { useHoldings } from './Holdings' import { PortfolioValue } from './PortfolioValue' -import { usePortfolioTokens } from './usePortfolio' const RangeFilterButton = styled(Stack)` &:hover { @@ -22,17 +22,14 @@ const rangeFilters = [ ] as const export function CardPortfolioValue({ address }: { address?: string }) { - const portfolioTokens = usePortfolioTokens(address) + const tokens = useHoldings(address) const transactions = useTransactionsByAddress(address) const { colors } = useTheme() const [range, setRange] = React.useState<(typeof rangeFilters)[number]>({ value: 'ytd', label: 'Year to date' }) - const currentPortfolioValue = portfolioTokens.reduce( - (sum, token) => sum.add(token.position.mul(token.tokenPrice.toDecimal())), - Dec(0) - ) + const currentPortfolioValue = tokens.reduce((sum, token) => sum.add(token.position.mul(token.tokenPrice)), Dec(0)) const balanceProps = { as: 'strong', @@ -78,7 +75,7 @@ export function CardPortfolioValue({ address }: { address?: string }) { - {transactions?.investorTransactions.length === 0 || !address ? null : ( + {address && transactions?.investorTransactions.length ? ( <> @@ -106,7 +103,7 @@ export function CardPortfolioValue({ address }: { address?: string }) { - )} + ) : null} ) diff --git a/centrifuge-app/src/components/Portfolio/Holdings.tsx b/centrifuge-app/src/components/Portfolio/Holdings.tsx index 92074f532c..4f80000a34 100644 --- a/centrifuge-app/src/components/Portfolio/Holdings.tsx +++ b/centrifuge-app/src/components/Portfolio/Holdings.tsx @@ -1,4 +1,4 @@ -import { Token, TokenBalance } from '@centrifuge/centrifuge-js' +import { Token } from '@centrifuge/centrifuge-js' import { formatBalance, useBalances, useCentrifuge, useWallet } from '@centrifuge/centrifuge-react' import { AnchorButton, @@ -13,6 +13,7 @@ import { Text, Thumbnail, } from '@centrifuge/fabric' +import Decimal from 'decimal.js-light' import React from 'react' import { useHistory, useLocation } from 'react-router-dom' import { useTheme } from 'styled-components' @@ -21,12 +22,14 @@ import ethLogo from '../../assets/images/ethereum.svg' import centLogo from '../../assets/images/logoCentrifuge.svg' import usdcLogo from '../../assets/images/usdc-logo.svg' import usdtLogo from '../../assets/images/usdt-logo.svg' +import { isEvmAddress, isSubstrateAddress } from '../../utils/address' import { Dec } from '../../utils/Decimal' import { formatBalanceAbbreviated } from '../../utils/formatting' import { useTinlakeBalances } from '../../utils/tinlake/useTinlakeBalances' +import { useTinlakePools } from '../../utils/tinlake/useTinlakePools' import { useCFGTokenPrice } from '../../utils/useCFGTokenPrice' import { usePoolCurrencies } from '../../utils/useCurrencies' -import { usePool, usePoolMetadata, usePools } from '../../utils/usePools' +import { usePool, usePoolMetadata } from '../../utils/usePools' import { Column, DataTable, SortableTableHeader } from '../DataTable' import { Eththumbnail } from '../EthThumbnail' import { InvestRedeemDrawer } from '../InvestRedeem/InvestRedeemDrawer' @@ -39,12 +42,12 @@ type Row = { currency: Token['currency'] poolId: string trancheId: string - marketValue: TokenBalance - position: TokenBalance - tokenPrice: TokenBalance + marketValue: Decimal + position: Decimal + tokenPrice: Decimal canInvestRedeem: boolean - address: string - connectedNetwork: string + address?: string + connectedNetwork?: string } const columns: Column[] = [ @@ -132,51 +135,42 @@ const columns: Column[] = [ }, ] -export function Holdings({ canInvestRedeem = true, address }: { canInvestRedeem?: boolean; address?: string }) { - const centBalances = useBalances(address) +export function useHoldings(address?: string, canInvestRedeem = true) { + const { data: tinlakeBalances } = useTinlakeBalances(address && isEvmAddress(address) ? address : undefined) + const centBalances = useBalances(address && isSubstrateAddress(address) ? address : undefined) const wallet = useWallet() - const { data: tinlakeBalances } = useTinlakeBalances() - const pools = usePools() + const tinlakePools = useTinlakePools() const portfolioTokens = usePortfolioTokens(address) const currencies = usePoolCurrencies() - const { search, pathname } = useLocation() - const history = useHistory() - const params = new URLSearchParams(search) - const openSendDrawer = params.get('send') - const openReceiveDrawer = params.get('receive') - const openInvestDrawer = params.get('invest') - const openRedeemDrawer = params.get('redeem') - - const [investPoolId, investTrancheId] = openInvestDrawer?.split('-') || [] - const [redeemPoolId, redeemTrancheId] = openRedeemDrawer?.split('-') || [] - const CFGPrice = useCFGTokenPrice() - const tokens = [ + const tokens: Row[] = [ ...portfolioTokens.map((token) => ({ ...token, tokenPrice: token.tokenPrice.toDecimal() || Dec(0), canInvestRedeem, })), - ...(tinlakeBalances?.tranches.filter((tranche) => !tranche.balance.isZero) || []).map((balance) => { - const pool = pools?.find((pool) => pool.id === balance.poolId) + ...(tinlakeBalances?.tranches.filter((tranche) => !tranche.balance.isZero()) || []).map((balance) => { + const pool = tinlakePools.data?.pools?.find((pool) => pool.id === balance.poolId) const tranche = pool?.tranches.find((tranche) => tranche.id === balance.trancheId) + if (!tranche) return null as never return { - position: balance.balance, - marketValue: tranche?.tokenPrice ? balance.balance.toDecimal().mul(tranche?.tokenPrice.toDecimal()) : Dec(0), - tokenPrice: tranche?.tokenPrice?.toDecimal() || Dec(0), + position: balance.balance.toDecimal(), + marketValue: tranche.tokenPrice ? balance.balance.toDecimal().mul(tranche?.tokenPrice.toDecimal()) : Dec(0), + tokenPrice: tranche.tokenPrice?.toDecimal() || Dec(0), trancheId: balance.trancheId, poolId: balance.poolId, - currency: tranche?.currency, + currency: tranche.currency, canInvestRedeem, connectedNetwork: wallet.connectedNetworkName, } }), ...(tinlakeBalances?.currencies.filter((currency) => currency.balance.gtn(0)) || []).map((currency) => { + const tokenPrice = currency.currency.symbol === 'wCFG' ? CFGPrice ?? 0 : 1 return { - position: currency.balance, - marketValue: currency.balance.toDecimal().mul(Dec(1)), - tokenPrice: Dec(1), + position: currency.balance.toDecimal(), + marketValue: currency.balance.toDecimal().mul(Dec(tokenPrice)), + tokenPrice: Dec(tokenPrice), trancheId: '', poolId: '', currency: currency.currency, @@ -188,13 +182,14 @@ export function Holdings({ canInvestRedeem = true, address }: { canInvestRedeem? ?.filter((currency) => currency.balance.gtn(0)) .map((currency) => { const token = currencies?.find((curr) => curr.symbol === currency.currency.symbol) + if (!token) return null as never return { currency: token, poolId: '', trancheId: '', - position: currency.balance.toDecimal() || Dec(0), + position: currency.balance.toDecimal(), tokenPrice: Dec(1), - marketValue: currency.balance.toDecimal() || Dec(0), + marketValue: currency.balance.toDecimal(), canInvestRedeem: false, connectedNetwork: wallet.connectedNetworkName, } @@ -204,22 +199,41 @@ export function Holdings({ canInvestRedeem = true, address }: { canInvestRedeem? { currency: { ...centBalances?.native.currency, - name: centBalances?.native.currency.symbol, + symbol: centBalances?.native.currency.symbol ?? 'CFG', + name: centBalances?.native.currency.symbol ?? 'CFG', + decimals: centBalances?.native.currency.decimals ?? 18, key: 'centrifuge', isPoolCurrency: false, isPermissioned: false, }, poolId: '', trancheId: '', - position: centBalances?.native.balance, + position: centBalances?.native.balance.toDecimal() || Dec(0), tokenPrice: CFGPrice ? Dec(CFGPrice) : Dec(0), - marketValue: CFGPrice ? centBalances?.native.balance.toDecimal().mul(CFGPrice) : Dec(0), + marketValue: CFGPrice ? centBalances?.native.balance.toDecimal().mul(CFGPrice) ?? Dec(0) : Dec(0), canInvestRedeem: false, connectedNetwork: wallet.connectedNetworkName, }, ] : []), - ] + ].filter(Boolean) + + return tokens +} + +export function Holdings({ canInvestRedeem = true, address }: { canInvestRedeem?: boolean; address?: string }) { + const { search, pathname } = useLocation() + const history = useHistory() + const params = new URLSearchParams(search) + const openSendDrawer = params.get('send') + const openReceiveDrawer = params.get('receive') + const openInvestDrawer = params.get('invest') + const openRedeemDrawer = params.get('redeem') + + const [investPoolId, investTrancheId] = openInvestDrawer?.split('-') || [] + const [redeemPoolId, redeemTrancheId] = openRedeemDrawer?.split('-') || [] + + const tokens = useHoldings(address, canInvestRedeem) return address && tokens.length ? ( <> diff --git a/centrifuge-app/src/config.ts b/centrifuge-app/src/config.ts index 769824ae3a..7606c4d744 100644 --- a/centrifuge-app/src/config.ts +++ b/centrifuge-app/src/config.ts @@ -130,6 +130,7 @@ const CENTRIFUGE: EnvironmentConfig = { const ethNetwork = import.meta.env.REACT_APP_TINLAKE_NETWORK || 'mainnet' const goerliConfig = { + chainId: 5, rpcUrl: 'https://goerli.infura.io/v3/f9ba987e8cb34418bb53cdbd4d8321b5', poolRegistryAddress: '0x5ba1e12693dc8f9c48aad8770482f4739beed696', tinlakeUrl: 'https://goerli.staging.tinlake.cntrfg.com/', @@ -137,6 +138,7 @@ const goerliConfig = { blockExplorerUrl: 'https://goerli.etherscan.io', } const mainnetConfig = { + chainId: 1, rpcUrl: 'https://mainnet.infura.io/v3/ed5e0e19bcbc427cbf8f661736d44516', poolRegistryAddress: '0x5ba1e12693dc8f9c48aad8770482f4739beed696', tinlakeUrl: 'https://tinlake.centrifuge.io', diff --git a/centrifuge-app/src/pages/Portfolio/index.tsx b/centrifuge-app/src/pages/Portfolio/index.tsx index 751716812b..9aaf2955a3 100644 --- a/centrifuge-app/src/pages/Portfolio/index.tsx +++ b/centrifuge-app/src/pages/Portfolio/index.tsx @@ -22,7 +22,7 @@ export default function PortfolioPage() { function Portfolio() { const address = useAddress() const transactions = useTransactionsByAddress(address) - const { showNetworks } = useWallet() + const { showNetworks, connectedNetwork } = useWallet() return ( <> @@ -38,7 +38,7 @@ function Portfolio() { - {transactions?.investorTransactions.length === 0 ? ( + {transactions?.investorTransactions.length === 0 && connectedNetwork === 'centrifuge' ? ( diff --git a/centrifuge-app/src/utils/address.ts b/centrifuge-app/src/utils/address.ts new file mode 100644 index 0000000000..54a9e9371a --- /dev/null +++ b/centrifuge-app/src/utils/address.ts @@ -0,0 +1,8 @@ +import { isAddress as isEvmAddress } from '@ethersproject/address' +import { isAddress } from '@polkadot/util-crypto' + +export function isSubstrateAddress(address: string) { + return isAddress(address) && !isEvmAddress(address) +} + +export { isEvmAddress } diff --git a/centrifuge-app/src/utils/tinlake/currencies.ts b/centrifuge-app/src/utils/tinlake/currencies.ts index 5547455cc0..b5f0615a16 100644 --- a/centrifuge-app/src/utils/tinlake/currencies.ts +++ b/centrifuge-app/src/utils/tinlake/currencies.ts @@ -7,4 +7,12 @@ export const currencies = { isPoolCurrency: false, isPermissioned: false, }, + wCFG: { + decimals: 18, + name: 'Wrapped CFG', + symbol: 'wCFG', + key: 'wCFG', + isPoolCurrency: false, + isPermissioned: false, + }, } diff --git a/centrifuge-app/src/utils/tinlake/useTinlakeBalances.ts b/centrifuge-app/src/utils/tinlake/useTinlakeBalances.ts index 4150ec2b3a..1fbeea070a 100644 --- a/centrifuge-app/src/utils/tinlake/useTinlakeBalances.ts +++ b/centrifuge-app/src/utils/tinlake/useTinlakeBalances.ts @@ -1,21 +1,38 @@ -import { AccountCurrencyBalance, AccountTokenBalance, CurrencyBalance, TokenBalance } from '@centrifuge/centrifuge-js' +import { + AccountCurrencyBalance, + AccountTokenBalance, + CurrencyBalance, + evmMulticall, + EvmMulticallCall, + TokenBalance, +} from '@centrifuge/centrifuge-js' +import { useWallet } from '@centrifuge/centrifuge-react' import { BigNumber } from '@ethersproject/bignumber' +import { JsonRpcProvider } from '@ethersproject/providers' import { useQuery } from 'react-query' -import { useAddress } from '../useAddress' +import { ethConfig } from '../../config' import { currencies } from './currencies' -import { Call, multicall } from './multicall' import { TinlakePool, useTinlakePools } from './useTinlakePools' export function useTinlakeBalances(address?: string) { - const addr = useAddress('evm') || address + const { + evm: { getProvider }, + } = useWallet() const { data } = useTinlakePools() - return useQuery(['tinlakeBalances', addr, !!data?.pools], () => getBalances(data?.pools!, addr!), { - enabled: !!addr && !!data?.pools, - }) + return useQuery( + ['tinlakeBalances', address, !!data?.pools], + () => getBalances(data?.pools!, address!, getProvider(ethConfig.chainId)), + { + enabled: !!address && !!data?.pools, + retry: false, + } + ) } -async function getBalances(pools: TinlakePool[], address: string) { - const calls: Call[] = [] +const WCFG_ADDRESS = '0xc221b7e65ffc80de234bbb6667abdd46593d34f0' + +async function getBalances(pools: TinlakePool[], address: string, provider: JsonRpcProvider) { + const calls: EvmMulticallCall[] = [] const toTokenBalance = (val: BigNumber) => new TokenBalance(val.toString(), 18) const toCurrencyBalance = (val: BigNumber) => new CurrencyBalance(val.toString(), 18) @@ -25,12 +42,12 @@ async function getBalances(pools: TinlakePool[], address: string) { calls.push( { target: pool.addresses.JUNIOR_TOKEN, - call: ['balanceOf(address)(uint256)', address], + call: ['function balanceOf(address) view returns (uint256)', address], returns: [[`tokens.${pool.id}.junior`, toTokenBalance]], }, { target: pool.addresses.SENIOR_TOKEN, - call: ['balanceOf(address)(uint256)', address], + call: ['function balanceOf(address) view returns (uint256)', address], returns: [[`tokens.${pool.id}.senior`, toTokenBalance]], } ) @@ -38,14 +55,21 @@ async function getBalances(pools: TinlakePool[], address: string) { if (!seenCurrencies.has(pool.addresses.TINLAKE_CURRENCY.toLowerCase())) { calls.push({ target: pool.addresses.TINLAKE_CURRENCY, - call: ['balanceOf(address)(uint256)', address], + call: ['function balanceOf(address) view returns (uint256)', address], returns: [[`currencies.${pool.addresses.TINLAKE_CURRENCY}`, toCurrencyBalance]], }) seenCurrencies.add(pool.addresses.TINLAKE_CURRENCY) } }) - const multicallData = await multicall(calls) + calls.push({ + target: WCFG_ADDRESS, + call: ['function balanceOf(address) view returns (uint256)', address], + returns: [[`currencies.${WCFG_ADDRESS}`, toCurrencyBalance]], + allowFailure: true, + }) + + const multicallData = await evmMulticall(calls, { rpcProvider: provider }) const balances = { tranches: [] as AccountTokenBalance[], @@ -64,10 +88,10 @@ async function getBalances(pools: TinlakePool[], address: string) { }) }) - Object.values(multicallData.currencies).forEach((balance) => { + Object.entries(multicallData.currencies).forEach(([currencyAddress, balance]) => { balances.currencies.push({ balance, - currency: currencies.DAI, + currency: currencyAddress === WCFG_ADDRESS ? currencies.wCFG : currencies.DAI, }) }) diff --git a/centrifuge-js/src/index.ts b/centrifuge-js/src/index.ts index c73744beba..58eae4cb39 100644 --- a/centrifuge-js/src/index.ts +++ b/centrifuge-js/src/index.ts @@ -8,6 +8,7 @@ export type { TinlakeContractAddresses, TinlakeContractNames, TinlakeContractVer export * from './types' export * from './utils' export * from './utils/BN' +export { Call as EvmMulticallCall, multicall as evmMulticall } from './utils/evmMulticall' export * from './utils/solver' export default Centrifuge