Skip to content

Commit

Permalink
Key metrics redesign
Browse files Browse the repository at this point in the history
  • Loading branch information
kattylucy committed Sep 12, 2024
1 parent 09913a1 commit e407a5f
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 86 deletions.
28 changes: 19 additions & 9 deletions centrifuge-app/src/components/PoolList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,6 @@ export function poolsToPoolCardProps(
cent: Centrifuge
): PoolCardProps[] {
return pools.map((pool) => {
const tinlakePool = pool.id?.startsWith('0x') && (pool as TinlakePool)
const metaData = typeof pool.metadata === 'string' ? metaDataById[pool.id] : pool.metadata

return {
Expand All @@ -144,21 +143,32 @@ export function poolsToPoolCardProps(
assetClass: metaData?.pool?.asset.subClass,
valueLocked: getPoolValueLocked(pool),
currencySymbol: pool.currency.symbol,
status:
tinlakePool && tinlakePool.tinlakeMetadata.isArchived
? 'Archived'
: tinlakePool && tinlakePool.addresses.CLERK !== undefined && tinlakePool.tinlakeMetadata.maker?.ilk
? 'Closed'
: pool.tranches.at(0)?.capacity?.toFloat() // pool is displayed as "open for investments" if the most junior tranche has a capacity
? 'Open for investments'
: ('Closed' as PoolStatusKey),
status: getPoolStatus(pool),
iconUri: metaData?.pool?.icon?.uri ? cent.metadata.parseMetadataUrl(metaData?.pool?.icon?.uri) : undefined,
tranches: pool.tranches as Tranche[],
metaData: metaData as MetaData,
}
})
}

export function getPoolStatus(pool: Pool | TinlakePool): PoolStatusKey {
const tinlakePool = pool.id?.startsWith('0x') && (pool as TinlakePool)

if (tinlakePool && tinlakePool.tinlakeMetadata.isArchived) {
return 'Archived'
}

if (tinlakePool && tinlakePool.addresses.CLERK !== undefined && tinlakePool.tinlakeMetadata.maker?.ilk) {
return 'Closed'
}

if (pool.tranches.at(0)?.capacity?.toFloat()) {
return 'Open for investments'
}

return 'Closed'
}

function getMetasById(pools: Pool[], poolMetas: PoolMetaDataPartial[]) {
const result: MetaDataById = {}

Expand Down
171 changes: 99 additions & 72 deletions centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,98 @@
import { ActiveLoan, Loan, TinlakeLoan } from '@centrifuge/centrifuge-js'
import { Loan, Perquintill, Pool, TinlakeLoan } from '@centrifuge/centrifuge-js'
import { NetworkIcon } from '@centrifuge/centrifuge-react'
import { Box, Card, Grid, IconExternalLink, Shelf, Stack, Text, Tooltip } from '@centrifuge/fabric'
import { Box, Card, IconExternalLink, Shelf, Stack, Text, Tooltip } from '@centrifuge/fabric'
import capitalize from 'lodash/capitalize'
import startCase from 'lodash/startCase'
import { useMemo } from 'react'
import { evmChains } from '../../config'
import { formatPercentage } from '../../utils/formatting'
import { TinlakePool } from '../../utils/tinlake/useTinlakePools'
import { useActiveDomains } from '../../utils/useLiquidityPools'
import { usePool } from '../../utils/usePools'
import { useDailyTranchesStates, usePool } from '../../utils/usePools'
import { PoolStatus } from '../PoolCard/PoolStatus'
import { getPoolStatus } from '../PoolList'
import { Spinner } from '../Spinner'

type Props = {
assetType?: { class: string; subClass: string }
averageMaturity: string
loans: TinlakeLoan[] | Loan[] | null | undefined
poolId: string
pool: Pool | TinlakePool
}

export const KeyMetrics = ({ assetType, averageMaturity, loans, poolId }: Props) => {
interface DailyTrancheState {
yield30DaysAnnualized: Perquintill
timestamp: string
}

type DailyTrancheStateArr = Record<string, DailyTrancheState[]>

const getTodayValue = (data: DailyTrancheStateArr | null | undefined): DailyTrancheStateArr | undefined => {
if (!data) return
if (!Object.keys(data).length) return

const today = new Date()

const filteredData: DailyTrancheStateArr = Object.keys(data).reduce((result, key) => {
const filteredValues = data[key].filter((obj) => {
const objDate = new Date(obj.timestamp)
return (
objDate.getDate() === today.getDate() &&
objDate.getMonth() === today.getMonth() &&
objDate.getFullYear() === today.getFullYear()
)
})

if (filteredValues.length > 0) {
result[key] = filteredValues
}

return result
}, {} as DailyTrancheStateArr)

return filteredData
}

export const KeyMetrics = ({ assetType, averageMaturity, loans, poolId, pool }: Props) => {
const isTinlakePool = poolId.startsWith('0x')
const tranchesIds = pool.tranches.map((tranche) => tranche.id)
const dailyTranches = useDailyTranchesStates(tranchesIds)

function hasValuationMethod(pricing: any): pricing is { valuationMethod: string } {
return pricing && typeof pricing.valuationMethod === 'string'
}

const ongoingAssetCount =
loans &&
[...loans].filter(
(loan) =>
loan.status === 'Active' &&
hasValuationMethod(loan.pricing) &&
loan.pricing.valuationMethod !== 'cash' &&
!loan.outstandingDebt.isZero()
).length

const writtenOffAssetCount =
loans && [...loans].filter((loan) => loan.status === 'Active' && (loan as ActiveLoan).writeOffStatus).length

const overdueAssetCount =
loans &&
[...loans].filter((loan) => {
const today = new Date()
today.setUTCHours(0, 0, 0, 0)
return (
loan.status === 'Active' &&
loan.pricing.maturityDate &&
new Date(loan.pricing.maturityDate).getTime() < Date.now() &&
!loan.outstandingDebt.isZero()
)
}).length
console.log(getTodayValue(dailyTranches))

const tranchesAPY = useMemo(() => {
const thirtyDayAPY = getTodayValue(dailyTranches)
if (!thirtyDayAPY) return null

return Object.keys(thirtyDayAPY).map((key) => {
return thirtyDayAPY[key][0].yield30DaysAnnualized
? formatPercentage(thirtyDayAPY[key][0].yield30DaysAnnualized)
: null
})
}, [dailyTranches])

const isBT3BT4 =
poolId.toLowerCase() === '0x90040f96ab8f291b6d43a8972806e977631affde' ||
poolId.toLowerCase() === '0x55d86d51ac3bcab7ab7d2124931fba106c8b60c7'

const metrics = [
{
metric: 'Asset class',
metric: 'Asset type',
value: `${capitalize(startCase(assetType?.class))} - ${assetType?.subClass}`,
},
{
metric: '30-day APY',
value: tranchesAPY?.length
? tranchesAPY.map((tranche, index) => {
return tranche && `${tranche} ${index !== tranchesAPY?.length - 1 ? '-' : ''} `
})
: '-',
},
...(isBT3BT4
? []
: [
Expand All @@ -66,32 +102,13 @@ export const KeyMetrics = ({ assetType, averageMaturity, loans, poolId }: Props)
},
]),
{
metric: 'Total assets',
value:
loans?.filter((loan) => hasValuationMethod(loan.pricing) && loan.pricing.valuationMethod !== 'cash').length ||
0,
metric: 'Min. investment',
value: averageMaturity,
},
{
metric: 'Ongoing assets',
value: ongoingAssetCount,
metric: 'Investor type',
value: averageMaturity,
},
...(writtenOffAssetCount
? [
{
metric: 'Written off assets',
value: writtenOffAssetCount,
},
]
: []),
...(overdueAssetCount
? [
{
metric: 'Overdue assets',
value: overdueAssetCount,
},
]
: []),

...(!isTinlakePool
? [
{
Expand All @@ -100,35 +117,45 @@ export const KeyMetrics = ({ assetType, averageMaturity, loans, poolId }: Props)
},
]
: []),
{
metric: 'Pool structure',
value:
loans?.filter((loan) => hasValuationMethod(loan.pricing) && loan.pricing.valuationMethod !== 'cash').length ||
0,
},
{
metric: 'Rating',
value:
loans?.filter((loan) => hasValuationMethod(loan.pricing) && loan.pricing.valuationMethod !== 'cash').length ||
0,
},
{
metric: 'Expense ratio',
value:
loans?.filter((loan) => hasValuationMethod(loan.pricing) && loan.pricing.valuationMethod !== 'cash').length ||
0,
},
]

return (
<Card p={3}>
<Stack gap={2}>
<Text fontSize="18px" fontWeight="500">
Key metrics
</Text>
<Box borderStyle="solid" borderWidth="1px" borderColor="borderPrimary">
<Stack gap={1}>
<Box display="flex" justifyContent="space-between">
<Text variant="body2" fontWeight="500">
Overview
</Text>
<PoolStatus status={getPoolStatus(pool)} />
</Box>
<Box marginTop={2}>
{metrics.map(({ metric, value }, index) => (
<Grid
borderBottomStyle={index === metrics.length - 1 ? 'none' : 'solid'}
borderBottomWidth={index === metrics.length - 1 ? '0' : '1px'}
borderBottomColor={index === metrics.length - 1 ? 'none' : 'borderPrimary'}
height={32}
key={index}
px={1}
gridTemplateColumns="1fr 1fr"
width="100%"
alignItems="center"
gap={2}
>
<Text variant="body3" textOverflow="ellipsis" whiteSpace="nowrap">
<Box key={index} display="flex" justifyContent="space-between" mt="6px">
<Text color="textSecondary" variant="body2" textOverflow="ellipsis" whiteSpace="nowrap">
{metric}
</Text>
<Text variant="body3" textOverflow="ellipsis" whiteSpace="nowrap">
{value}
</Text>
</Grid>
</Box>
))}
</Box>
</Stack>
Expand Down
5 changes: 2 additions & 3 deletions centrifuge-app/src/pages/Pool/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Box, Shelf, Text, TextWithPlaceholder } from '@centrifuge/fabric'
import * as React from 'react'
import { useLocation, useParams } from 'react-router'
import { useTheme } from 'styled-components'
import { Eththumbnail } from '../../components/EthThumbnail'
import { BASE_PADDING } from '../../components/LayoutBase/BasePadding'
import { NavigationTabs, NavigationTabsItem } from '../../components/NavigationTabs'
import { PageHeader } from '../../components/PageHeader'
Expand Down Expand Up @@ -32,7 +31,7 @@ export function PoolDetailHeader({ actions }: Props) {
title={<TextWithPlaceholder isLoading={isLoading}>{metadata?.pool?.name ?? 'Unnamed pool'}</TextWithPlaceholder>}
parent={{ to: `/pools${state?.token ? '/tokens' : ''}`, label: state?.token ? 'Tokens' : 'Pools' }}
icon={
<Eththumbnail show={isTinlakePool}>
<>
{metadata?.pool?.icon ? (
<Box as="img" width="iconLarge" height="iconLarge" src={iconUri} borderRadius={4} />
) : (
Expand All @@ -46,7 +45,7 @@ export function PoolDetailHeader({ actions }: Props) {
<Text variant="body1">{(isLoading ? '' : metadata?.pool?.name ?? 'U')[0]}</Text>
</Shelf>
)}
</Eththumbnail>
</>
}
border={false}
actions={actions}
Expand Down
1 change: 1 addition & 0 deletions centrifuge-app/src/pages/Pool/Overview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export function PoolDetailOverview() {
averageMaturity={averageMaturity}
loans={loans}
poolId={poolId}
pool={pool}
/>
</React.Suspense>
</Grid>
Expand Down
10 changes: 8 additions & 2 deletions centrifuge-js/src/modules/pools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2455,13 +2455,19 @@ export function getPoolsModule(inst: Centrifuge) {
}),
takeLast(1),
map(({ trancheSnapshots }) => {
const trancheStates: Record<string, { timestamp: string; tokenPrice: Price }[]> = {}
const trancheStates: Record<
string,
{ timestamp: string; tokenPrice: Price; yield30DaysAnnualized: Perquintill }[]
> = {}
trancheSnapshots?.forEach((state) => {
const tid = state.tranche.trancheId
const entry = {
timestamp: state.timestamp,
tokenPrice: new Price(state.tokenPrice),
pool: state.tranche.poolId,
yield30DaysAnnualized: state.yield30DaysAnnualized
? new Perquintill(hexToBN(state.yield30DaysAnnualized))
: new Perquintill(0),
}
if (trancheStates[tid]) {
trancheStates[tid].push(entry)
Expand Down Expand Up @@ -4556,7 +4562,7 @@ export function getPoolsModule(inst: Centrifuge) {
}
}

function hexToBN(value?: string | number | null) {
export function hexToBN(value?: string | number | null) {
if (typeof value === 'number' || value == null) return new BN(value ?? 0)
return new BN(value.toString().substring(2), 'hex')
}
Expand Down

0 comments on commit e407a5f

Please sign in to comment.