Skip to content

Commit

Permalink
Show Tinlake portfolio (#1873)
Browse files Browse the repository at this point in the history
  • Loading branch information
onnovisser authored Jan 19, 2024
1 parent 43808cf commit 5677627
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
13 changes: 5 additions & 8 deletions centrifuge-app/src/components/Portfolio/CardPortfolioValue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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',
Expand Down Expand Up @@ -78,7 +75,7 @@ export function CardPortfolioValue({ address }: { address?: string }) {
</Shelf>
</Shelf>
</Stack>
{transactions?.investorTransactions.length === 0 || !address ? null : (
{address && transactions?.investorTransactions.length ? (
<>
<Stack gap={1}>
<Shelf justifyContent="flex-end" pr="20px">
Expand Down Expand Up @@ -106,7 +103,7 @@ export function CardPortfolioValue({ address }: { address?: string }) {
<PortfolioValue rangeValue={range.value} address={address} />
</Box>
</>
)}
) : null}
</Box>
</Box>
)
Expand Down
90 changes: 52 additions & 38 deletions centrifuge-app/src/components/Portfolio/Holdings.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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'
Expand All @@ -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'
Expand All @@ -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[] = [
Expand Down Expand Up @@ -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,
Expand All @@ -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,
}
Expand All @@ -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 ? (
<>
Expand Down
2 changes: 2 additions & 0 deletions centrifuge-app/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,15 @@ 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/',
poolsHash: 'QmQe9NTiVJnVcb4srw6sBpHefhYieubR7v3J8ZriULQ8vB', // TODO: add registry to config and fetch poolHash
blockExplorerUrl: 'https://goerli.etherscan.io',
}
const mainnetConfig = {
chainId: 1,
rpcUrl: 'https://mainnet.infura.io/v3/ed5e0e19bcbc427cbf8f661736d44516',
poolRegistryAddress: '0x5ba1e12693dc8f9c48aad8770482f4739beed696',
tinlakeUrl: 'https://tinlake.centrifuge.io',
Expand Down
4 changes: 2 additions & 2 deletions centrifuge-app/src/pages/Portfolio/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
Expand All @@ -38,7 +38,7 @@ function Portfolio() {
<CardPortfolioValue address={address} />
</LayoutSection>

{transactions?.investorTransactions.length === 0 ? (
{transactions?.investorTransactions.length === 0 && connectedNetwork === 'centrifuge' ? (
<LayoutSection>
<Stack maxWidth="700px" gap={2}>
<Text variant="body2">
Expand Down
8 changes: 8 additions & 0 deletions centrifuge-app/src/utils/address.ts
Original file line number Diff line number Diff line change
@@ -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 }
8 changes: 8 additions & 0 deletions centrifuge-app/src/utils/tinlake/currencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
}
54 changes: 39 additions & 15 deletions centrifuge-app/src/utils/tinlake/useTinlakeBalances.ts
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -25,27 +42,34 @@ 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]],
}
)

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<State>(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<State>(calls, { rpcProvider: provider })

const balances = {
tranches: [] as AccountTokenBalance[],
Expand All @@ -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,
})
})

Expand Down
1 change: 1 addition & 0 deletions centrifuge-js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 5677627

Please sign in to comment.