Skip to content

Commit

Permalink
Make change asset in asset valuation dynamic
Browse files Browse the repository at this point in the history
  • Loading branch information
sophialittlejohn committed Sep 9, 2024
1 parent 958faf9 commit 6dde8af
Show file tree
Hide file tree
Showing 2 changed files with 225 additions and 188 deletions.
301 changes: 220 additions & 81 deletions centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { ActiveLoan, CreatedLoan, CurrencyBalance, ExternalLoan } from '@centrifuge/centrifuge-js'
import {
ActiveLoan,
CreatedLoan,
CurrencyBalance,
CurrencyKey,
CurrencyMetadata,
ExternalLoan,
} from '@centrifuge/centrifuge-js'
import { useCentrifugeApi, useCentrifugeQuery, useCentrifugeTransaction } from '@centrifuge/centrifuge-react'
import {
Box,
Button,
CurrencyInput,
Divider,
Drawer,
IconArrowRight,
IconClockForward,
IconDownload,
Shelf,
Stack,
Expand All @@ -15,22 +24,23 @@ import {
import { BN } from 'bn.js'
import { Field, FieldProps, FormikProvider, useFormik } from 'formik'
import * as React from 'react'
import { switchMap } from 'rxjs'
import { combineLatest, switchMap } from 'rxjs'
import daiLogo from '../../assets/images/dai-logo.svg'
import usdcLogo from '../../assets/images/usdc-logo.svg'
import { ButtonGroup } from '../../components/ButtonGroup'
import { DataTable } from '../../components/DataTable'
import { LayoutSection } from '../../components/LayoutBase/LayoutSection'
import { AssetName } from '../../components/LoanList'
import { RouterTextLink } from '../../components/TextLink'
import { Dec } from '../../utils/Decimal'
import { formatBalance } from '../../utils/formatting'
import { useLiquidity } from '../../utils/useLiquidity'
import { useActiveDomains } from '../../utils/useLiquidityPools'
import { useSuitableAccounts } from '../../utils/usePermissions'
import { usePool, usePoolAccountOrders, usePoolFees } from '../../utils/usePools'
import { usePoolsForWhichAccountIsFeeder } from '../../utils/usePoolsForWhichAccountIsFeeder'
import { positiveNumber } from '../../utils/validation'
import { isCashLoan, isExternalLoan } from '../Loan/utils'
import { VisualNavCard } from './Overview'

type FormValues = {
feed: {
Expand All @@ -49,6 +59,7 @@ type Row = FormValues['feed'][0] | ActiveLoan | CreatedLoan
const MAX_COLLECT = 100 // maximum number of transactions to collect in one batch

export function NavManagementAssetTable({ poolId }: { poolId: string }) {
const { data: domains } = useActiveDomains(poolId)
const allowedPools = usePoolsForWhichAccountIsFeeder()
const isFeeder = !!allowedPools?.find((p) => p.id === poolId)
const [isEditing, setIsEditing] = React.useState(false)
Expand Down Expand Up @@ -91,8 +102,20 @@ export function NavManagementAssetTable({ poolId }: { poolId: string }) {
const { execute, isLoading } = useCentrifugeTransaction(
'Update NAV',
(cent) => (args: [values: FormValues], options) => {
return cent.pools.closeEpoch([poolId, false], { batch: true }).pipe(
switchMap((closeTx) => {
const domain = domains?.find((domain) => domain.isActive && domain.hasDeployedLp)
const updateTokenPrices = domain
? Object.entries(domain.liquidityPools).flatMap(([tid, poolsByCurrency]) => {
return domain.currencies
.filter((cur) => !!poolsByCurrency[cur.address])
.map((cur) => [tid, cur.key] satisfies [string, CurrencyKey])
.map(([tid, curKey]) =>
cent.liquidityPools.updateTokenPrice([poolId, tid, curKey, domain.chainId], { batch: true })
)
})
: []

return combineLatest([cent.pools.closeEpoch([poolId, false], { batch: true }), ...updateTokenPrices]).pipe(
switchMap(([closeTx, ...updateTokenPricesTxs]) => {
const [values] = args
const batch = [
...values.feed
Expand All @@ -103,6 +126,7 @@ export function NavManagementAssetTable({ poolId }: { poolId: string }) {
}),
api.tx.oraclePriceCollection.updateCollection(poolId),
api.tx.loans.updatePortfolioValuation(poolId),
...updateTokenPricesTxs,
]

if (liquidityAdminAccount && orders?.length) {
Expand Down Expand Up @@ -290,90 +314,205 @@ export function NavManagementAssetTable({ poolId }: { poolId: string }) {
}

return (
<Stack pb={8}>
<FormikProvider value={form}>
<Drawer isOpen={isConfirming} onClose={() => setIsConfirming(false)}>
<Stack gap={2}>
<>
<LayoutSection pt={3}>
<NavOverviewCard poolId={pool.id} updatedPrices={form.values.feed} />
</LayoutSection>

<Stack pb={8}>
<FormikProvider value={form}>
<Drawer isOpen={isConfirming} onClose={() => setIsConfirming(false)}>
<Stack gap={2}>
<Text variant="heading3">Confirm NAV</Text>
<VisualNavCard
currency={pool.currency}
current={pool.nav.total.toFloat()}
change={newNav - pool.nav.total.toFloat()}
pendingFees={pendingFees.toFloat()}
pendingNav={newNav}
/>
</Stack>
{pool.tranches.length === 1 && (
<Stack gap={2}>
<Text variant="heading3">Token price update</Text>
<Shelf bg="backgroundSecondary" p={1} gap={1}>
<Text variant="body2">
{pool.tranches[0].currency.symbol} price:{' '}
{formatBalance(pool.tranches[0].tokenPrice ?? 0, pool.currency.symbol, 5)}
</Text>
<IconArrowRight size={16} />{' '}
<Text variant="body2" color="accentPrimary">
{pool.tranches[0].currency.symbol} price: {formatBalance(newPrice ?? 0, pool.currency.symbol, 5)}
</Text>
</Shelf>
<Text variant="heading3">Confirm NAV</Text>
<VisualNavCard
currency={pool.currency}
current={pool.nav.total.toFloat()}
change={newNav - pool.nav.total.toFloat()}
pendingFees={pendingFees.toFloat()}
pendingNav={newNav}
/>
</Stack>
)}
<ButtonGroup>
<Button
onClick={() => {
form.submitForm()
setIsConfirming(false)
}}
>
Update NAV
</Button>
<Button variant="secondary" onClick={() => setIsConfirming(false)}>
Cancel
</Button>
</ButtonGroup>

{liquidityAdminAccount && orders?.length ? (
<Text variant="body3">
There are open investment or redemption orders, updating the NAV will trigger the execution of orders.
</Text>
) : null}
</Stack>
</Drawer>
<LayoutSection
title="Assets"
pt={5}
headerRight={
isEditing ? (
<ButtonGroup variant="small" key="editing">
<Button variant="secondary" onClick={() => setIsEditing(false)} small>
Cancel
</Button>
{pool.tranches.length === 1 && (
<Stack gap={2}>
<Text variant="heading3">Token price update</Text>
<Shelf bg="backgroundSecondary" p={1} gap={1}>
<Text variant="body2">
{pool.tranches[0].currency.symbol} price:{' '}
{formatBalance(pool.tranches[0].tokenPrice ?? 0, pool.currency.symbol, 5)}
</Text>
<IconArrowRight size={16} />{' '}
<Text variant="body2" color="accentPrimary">
{pool.tranches[0].currency.symbol} price: {formatBalance(newPrice ?? 0, pool.currency.symbol, 5)}
</Text>
</Shelf>
</Stack>
)}
<ButtonGroup>
<Button
small
onClick={() => setIsConfirming(true)}
loading={isLoading || form.isSubmitting}
loadingMessage={isLoading ? 'Pending...' : undefined}
disabled={!isFeeder}
onClick={() => {
form.submitForm()
setIsConfirming(false)
}}
>
Done
</Button>
</ButtonGroup>
) : (
<ButtonGroup variant="small" key="edit">
<Button variant="tertiary" small icon={IconDownload}>
Download
Update NAV
</Button>
<Button onClick={() => setIsEditing(true)} small>
Edit
<Button variant="secondary" onClick={() => setIsConfirming(false)}>
Cancel
</Button>
</ButtonGroup>
)
}
>
<DataTable data={[...reserveRow, ...cashLoans, ...form.values.feed]} columns={columns} />
</LayoutSection>
</FormikProvider>

{liquidityAdminAccount && orders?.length ? (
<Text variant="body3">
There are open investment or redemption orders, updating the NAV will trigger the execution of orders.
</Text>
) : null}
</Stack>
</Drawer>
<LayoutSection
title="Assets"
pt={5}
headerRight={
isEditing ? (
<ButtonGroup variant="small" key="editing">
<Button variant="secondary" onClick={() => setIsEditing(false)} small>
Cancel
</Button>
<Button
small
onClick={() => setIsConfirming(true)}
loading={isLoading || form.isSubmitting}
loadingMessage={isLoading ? 'Pending...' : undefined}
disabled={!isFeeder}
>
Done
</Button>
</ButtonGroup>
) : (
<ButtonGroup variant="small" key="edit">
<Button variant="tertiary" small icon={IconDownload}>
Download
</Button>
<Button onClick={() => setIsEditing(true)} small>
Edit
</Button>
</ButtonGroup>
)
}
>
<DataTable data={[...reserveRow, ...cashLoans, ...form.values.feed]} columns={columns} />
</LayoutSection>
</FormikProvider>
</Stack>
</>
)
}

export function NavOverviewCard({ poolId, updatedPrices }: { poolId: string; updatedPrices: FormValues['feed'] }) {
const pool = usePool(poolId)
const poolFees = usePoolFees(poolId)
const today = new Date()
today.setHours(0, 0, 0, 0)

const pendingFees = React.useMemo(() => {
return new CurrencyBalance(
poolFees?.map((f) => f.amounts.pending).reduce((acc, f) => acc.add(f), new BN(0)) ?? new BN(0),
pool.currency.decimals
)
}, [poolFees, pool.currency.decimals])

const [allLoans] = useCentrifugeQuery(['loans', poolId], (cent) => cent.pools.getLoans([poolId]), {
enabled: !!poolId && !!pool,
})

const externalLoans = React.useMemo(
() =>
(allLoans?.filter(
// Keep external loans, except ones that are fully repaid
(l) =>
isExternalLoan(l) &&
l.status !== 'Closed' &&
l.status !== 'Created' &&
(!('presentValue' in l) || !l.presentValue.isZero())
) as ActiveLoan[]) ?? [],
[allLoans]
)

const changeInValuation = React.useMemo(() => {
return externalLoans.reduce((prev, curr) => {
const price = curr.currentPrice.toDecimal()
const quantity = (curr as ExternalLoan).pricing.outstandingQuantity.toDecimal()
const updatedPrice = Dec(updatedPrices.find((p) => p.id === curr.id)?.value || 0)
return CurrencyBalance.fromFloat(
prev.toDecimal().add(price.sub(updatedPrice).mul(quantity)).toString(),
pool.currency.decimals
)
}, new CurrencyBalance(0, pool.currency.decimals))
}, [externalLoans, pool?.nav, updatedPrices])

Check warning on line 451 in centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx

View workflow job for this annotation

GitHub Actions / build-app

React Hook React.useMemo has a missing dependency: 'pool.currency.decimals'. Either include it or remove the dependency array

Check warning on line 451 in centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx

View workflow job for this annotation

GitHub Actions / ff-prod / build-app

React Hook React.useMemo has a missing dependency: 'pool.currency.decimals'. Either include it or remove the dependency array

return (
<VisualNavCard
currency={pool.currency}
current={pool.nav.total.toFloat()}
change={changeInValuation ? changeInValuation.toDecimal().toNumber() : 0}
pendingFees={pendingFees.toFloat()}
pendingNav={changeInValuation.toDecimal().add(pool.nav.total.toDecimal()).sub(pendingFees.toDecimal()).toNumber()}
/>
)
}

export function VisualNavCard({
currency,
current,
change,
pendingFees,
pendingNav,
}: {
currency: Pick<CurrencyMetadata, 'displayName' | 'decimals'>
current: number
change: number
pendingFees: number
pendingNav: number
}) {
return (
<Stack p={2} maxWidth="444px" bg="backgroundTertiary" gap={2}>
<Shelf justifyContent="space-between">
<Text variant="body2" color="textPrimary">
Current NAV
</Text>
<Text variant="body2">{formatBalance(current, currency.displayName, 2)}</Text>
</Shelf>
<Divider borderColor="statusInfoBg" />
<Stack gap={1}>
<Shelf justifyContent="space-between">
<Text variant="body2" color="textPrimary">
Change in asset valuation
</Text>
<Text variant="body2" color={change >= 0 ? 'statusOk' : 'statusCritical'}>
{formatBalance(change, currency.displayName, 2)}
</Text>
</Shelf>
<Shelf justifyContent="space-between">
<Text variant="body2" color="textPrimary">
Pending fees
</Text>
<Text variant="body2" color="statusCritical">
-{formatBalance(pendingFees, currency.displayName, 2)}
</Text>
</Shelf>
</Stack>
<Divider borderColor="statusInfoBg" />
<Shelf justifyContent="space-between">
<Shelf gap={1}>
<IconClockForward color="textSelected" size="iconSmall" />
<Text variant="body2" color="textSelected">
Pending NAV
</Text>
</Shelf>
<Text variant="body2" color="textSelected">
{formatBalance(pendingNav, currency.displayName, 2)}
</Text>
</Shelf>
</Stack>
)
}
Loading

0 comments on commit 6dde8af

Please sign in to comment.