Skip to content

Commit

Permalink
Nav Management Drawer (#2280)
Browse files Browse the repository at this point in the history
  • Loading branch information
onnovisser authored Jul 16, 2024
1 parent 02634d4 commit ca125cc
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 133 deletions.
11 changes: 1 addition & 10 deletions centrifuge-app/src/components/LiquidityEpochSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,7 @@ export function LiquidityEpochSection({ pool }: LiquidityEpochSectionProps) {
const MAX_COLLECT = 100

function EpochStatusOngoing({ pool }: { pool: Pool }) {
const { sumOfLockedInvestments, sumOfLockedRedemptions, sumOfExecutableInvestments, sumOfExecutableRedemptions } =
useLiquidity(pool.id)
const { sumOfLockedInvestments, sumOfLockedRedemptions, ordersFullyExecutable } = useLiquidity(pool.id)
const { message: epochTimeRemaining } = useEpochTimeCountdown(pool.id)
const [account] = useSuitableAccounts({
poolId: pool.id,
Expand Down Expand Up @@ -120,14 +119,6 @@ function EpochStatusOngoing({ pool }: { pool: Pool }) {
}

const ordersLocked = !epochTimeRemaining && sumOfLockedInvestments.add(sumOfLockedRedemptions).gt(0)
// const ordersPartiallyExecutable =
// (sumOfExecutableInvestments.gt(0) && sumOfExecutableInvestments.lt(sumOfLockedInvestments)) ||
// (sumOfExecutableRedemptions.gt(0) && sumOfExecutableRedemptions.lt(sumOfLockedRedemptions))
const ordersFullyExecutable =
sumOfLockedInvestments.equals(sumOfExecutableInvestments) &&
sumOfLockedRedemptions.equals(sumOfExecutableRedemptions)
// const noOrdersExecutable =
// !ordersFullyExecutable && sumOfExecutableInvestments.eq(0) && sumOfExecutableRedemptions.eq(0)
return (
<PageSection
title="Order overview"
Expand Down
2 changes: 1 addition & 1 deletion centrifuge-app/src/pages/Loan/FinanceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ export function useWithdraw(poolId: string, borrower: CombinedSubstrateAccount,
const ao = access.assetOriginators.find((a) => a.address === borrower.actingAddress)
const withdrawAddresses = ao?.transferAllowlist ?? []

if (!isLocalAsset) {
if (!isLocalAsset || !withdrawAddresses.length) {
if (!withdrawAddresses.length)
return {
render: () => null,
Expand Down
147 changes: 100 additions & 47 deletions centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@ import { useCentrifugeApi, useCentrifugeQuery, useCentrifugeTransaction } from '
import {
Box,
Button,
Checkbox,
CurrencyInput,
Drawer,
IconArrowRight,
IconDownload,
Shelf,
Stack,
Text,
Thumbnail,
} from '@centrifuge/fabric'
import { BN } from 'bn.js'
import { Field, FieldProps, FormikProvider, useFormik } from 'formik'
import * as React from 'react'
import { switchMap } from 'rxjs'
import daiLogo from '../../assets/images/dai-logo.svg'
import usdcLogo from '../../assets/images/usdc-logo.svg'
import { ButtonGroup } from '../../components/ButtonGroup'
Expand All @@ -22,11 +24,13 @@ import { LayoutSection } from '../../components/LayoutBase/LayoutSection'
import { AssetName } from '../../components/LoanList'
import { formatDate } from '../../utils/date'
import { formatBalance } from '../../utils/formatting'
import { useLiquidity } from '../../utils/useLiquidity'
import { useSuitableAccounts } from '../../utils/usePermissions'
import { usePool, usePoolAccountOrders } from '../../utils/usePools'
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 @@ -40,7 +44,6 @@ type FormValues = {
currentPrice: number
withLinearPricing: boolean
}[]
closeEpoch: boolean
}
type Row = FormValues['feed'][0] | ActiveLoan | CreatedLoan

Expand All @@ -53,13 +56,18 @@ export function NavManagementAssetTable({ poolId }: { poolId: string }) {
const [isConfirming, setIsConfirming] = React.useState(false)
const orders = usePoolAccountOrders(poolId)
const [liquidityAdminAccount] = useSuitableAccounts({ poolId, poolRole: ['LiquidityAdmin'] })
const pool = usePool(poolId, false)
const pool = usePool(poolId)
const [allLoans] = useCentrifugeQuery(['loans', poolId], (cent) => cent.pools.getLoans([poolId]), {
enabled: !!poolId && !!pool,
})
const poolFees = usePoolFees(poolId)

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

Expand All @@ -73,46 +81,55 @@ export function NavManagementAssetTable({ poolId }: { poolId: string }) {
isin: 'Onchain reserve',
quantity: 1,
currentPrice: 0,
value: pool?.reserve.total.toDecimal().toNumber(),
value: pool.reserve.total.toFloat(),
formIndex: -1,
maturity: '',
oldValue: '',
},
]

const { ordersFullyExecutable } = useLiquidity(poolId)

const { execute, isLoading } = useCentrifugeTransaction(
'Set oracle prices',
'Update NAV',
(cent) => (args: [values: FormValues], options) => {
const [values] = args
const batch = [
...values.feed
.filter((f) => typeof f.value === 'number' && !Number.isNaN(f.value))
.map((f) => {
const feed = f.isin ? { Isin: f.isin } : { poolloanid: [poolId, f.id] }
return api.tx.oraclePriceFeed.feed(feed, CurrencyBalance.fromFloat(f.value, 18))
}),
api.tx.oraclePriceCollection.updateCollection(poolId),
api.tx.loans.updatePortfolioValuation(poolId),
]

if (liquidityAdminAccount && values.closeEpoch) {
batch.push(api.tx.poolSystem.closeEpoch(poolId))
}
return cent.pools.closeEpoch([poolId, false], { batch: true }).pipe(
switchMap((closeTx) => {
const [values] = args
const batch = [
...values.feed
.filter((f) => typeof f.value === 'number' && !Number.isNaN(f.value))
.map((f) => {
const feed = f.isin ? { Isin: f.isin } : { poolloanid: [poolId, f.id] }
return api.tx.oraclePriceFeed.feed(feed, CurrencyBalance.fromFloat(f.value, 18))
}),
api.tx.oraclePriceCollection.updateCollection(poolId),
api.tx.loans.updatePortfolioValuation(poolId),
]

if (orders?.length) {
batch.push(
...orders
.slice(0, MAX_COLLECT)
.map((order) =>
api.tx.investments[order.type === 'invest' ? 'collectInvestmentsFor' : 'collectRedemptionsFor'](
order.accountId,
[poolId, order.trancheId]
)
if (liquidityAdminAccount && orders?.length) {
batch.push(
...closeTx.method.args[0],
...orders
.slice(0, ordersFullyExecutable ? MAX_COLLECT : 0)
.map((order) =>
api.tx.investments[order.type === 'invest' ? 'collectInvestmentsFor' : 'collectRedemptionsFor'](
order.accountId,
[poolId, order.trancheId]
)
)
)
)
}
const tx = api.tx.utility.batchAll(batch)
return cent.wrapSignAndSend(api, tx, options)
}

const tx = api.tx.utility.batchAll(batch)
return cent.wrapSignAndSend(api, tx, options)
})
)
},
{
onSuccess: () => {
setIsEditing(false)
},
}
)

Expand All @@ -138,7 +155,6 @@ export function NavManagementAssetTable({ poolId }: { poolId: string }) {
currentPrice: l.status === 'Active' ? l?.currentPrice.toDecimal().toNumber() : 0,
}
}) ?? [],
closeEpoch: false,
}),
[externalLoans]
)
Expand All @@ -158,13 +174,22 @@ export function NavManagementAssetTable({ poolId }: { poolId: string }) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialValues, isEditing, isLoading])

const poolReserve = pool?.reserve.total.toDecimal().toNumber() || 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 poolReserve = pool.reserve.total.toDecimal().toNumber() || 0
const newNavExternal = form.values.feed.reduce(
(acc, cur) => acc + cur.quantity * (isEditing && cur.value ? cur.value : cur.oldValue),
0
)
const newNavCash = cashLoans.reduce((acc, cur) => acc + cur.outstandingDebt.toFloat(), 0)
const newNav = newNavExternal + newNavCash + poolReserve
// Only for single tranche pools
const newPrice = newNav / pool.tranches[0].totalIssuance.toFloat()
const isTinlakePool = poolId.startsWith('0x')

const columns = [
Expand Down Expand Up @@ -215,7 +240,7 @@ export function NavManagementAssetTable({ poolId }: { poolId: string }) {
row.id !== 'reserve'
? formatBalance(
'currentPrice' in row && typeof row.currentPrice === 'number' ? row.currentPrice : 0,
pool?.currency.displayName,
pool.currency.displayName,
8
)
: '',
Expand All @@ -231,7 +256,7 @@ export function NavManagementAssetTable({ poolId }: { poolId: string }) {
{...field}
placeholder={row.oldValue.toString()}
errorMessage={meta.touched ? meta.error : undefined}
currency={pool?.currency.displayName}
currency={pool.currency.displayName}
onChange={(value) => form.setFieldValue(`feed.${row.formIndex}.value`, value)}
value={field.value}
onClick={(e) => e.preventDefault()}
Expand All @@ -247,13 +272,14 @@ export function NavManagementAssetTable({ poolId }: { poolId: string }) {
align: 'right',
header: 'Value',
cell: (row: Row) => {
if (row.id === 'reserve' && 'value' in row) return formatBalance(row.value || 0, pool.currency.symbol)
const newValue =
'value' in row && !Number.isNaN(row.value) && typeof row.value === 'number' && isEditing
? row.value
: undefined
return 'oldValue' in row
? formatBalance(row.quantity * (newValue ?? row.oldValue), pool?.currency.symbol)
: formatBalance(row.outstandingDebt, pool?.currency.symbol)
? formatBalance(row.quantity * (newValue ?? row.oldValue), pool.currency.symbol)
: formatBalance(row.outstandingDebt, pool.currency.symbol)
},
},
]
Expand All @@ -266,10 +292,31 @@ export function NavManagementAssetTable({ poolId }: { poolId: string }) {
<Stack pb={8}>
<FormikProvider value={form}>
<Drawer isOpen={isConfirming} onClose={() => setIsConfirming(false)}>
<Text variant="heading2">Confirm NAV</Text>
<Stack gap={2}>
{liquidityAdminAccount && (
<Checkbox {...form.getFieldProps('closeEpoch')} checked={form.values.closeEpoch} label="Close epoch" />
<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 - pendingFees.toFloat()}
/>
</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>
</Stack>
)}
<ButtonGroup>
<Button
Expand All @@ -278,12 +325,18 @@ export function NavManagementAssetTable({ poolId }: { poolId: string }) {
setIsConfirming(false)
}}
>
Confirm
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
Expand Down Expand Up @@ -321,7 +374,7 @@ export function NavManagementAssetTable({ poolId }: { poolId: string }) {
data={[...reserveRow, ...cashLoans, ...form.values.feed]}
columns={columns}
onRowClicked={(row) =>
row.id !== 'reserve' ? `/issuer/${pool?.id}/assets/${row.id}` : `/nav-management/${pool?.id}`
row.id !== 'reserve' ? `/issuer/${pool.id}/assets/${row.id}` : `/nav-management/${pool.id}`
}
footer={
<DataRow>
Expand All @@ -337,7 +390,7 @@ export function NavManagementAssetTable({ poolId }: { poolId: string }) {
{isEditing && <DataCol />}
<DataCol>
<Text color="accentPrimary" variant="body2">
{formatBalance(newNav, pool?.currency.symbol)}
{formatBalance(newNav, pool.currency.symbol)}
</Text>
</DataCol>
</DataRow>
Expand Down
Loading

0 comments on commit ca125cc

Please sign in to comment.