diff --git a/centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx b/centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx index 04596cd21..87d551707 100644 --- a/centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx +++ b/centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx @@ -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, @@ -15,7 +24,7 @@ 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' @@ -23,14 +32,15 @@ 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: { @@ -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) @@ -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 @@ -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) { @@ -290,90 +314,205 @@ export function NavManagementAssetTable({ poolId }: { poolId: string }) { } return ( - - - setIsConfirming(false)}> - + <> + + + + + + + setIsConfirming(false)}> - Confirm NAV - - - {pool.tranches.length === 1 && ( - Token price update - - - {pool.tranches[0].currency.symbol} price:{' '} - {formatBalance(pool.tranches[0].tokenPrice ?? 0, pool.currency.symbol, 5)} - - {' '} - - {pool.tranches[0].currency.symbol} price: {formatBalance(newPrice ?? 0, pool.currency.symbol, 5)} - - + Confirm NAV + - )} - - - - - - {liquidityAdminAccount && orders?.length ? ( - - There are open investment or redemption orders, updating the NAV will trigger the execution of orders. - - ) : null} - - - - + {pool.tranches.length === 1 && ( + + Token price update + + + {pool.tranches[0].currency.symbol} price:{' '} + {formatBalance(pool.tranches[0].tokenPrice ?? 0, pool.currency.symbol, 5)} + + {' '} + + {pool.tranches[0].currency.symbol} price: {formatBalance(newPrice ?? 0, pool.currency.symbol, 5)} + + + + )} + - - ) : ( - - - - ) - } - > - - - + + {liquidityAdminAccount && orders?.length ? ( + + There are open investment or redemption orders, updating the NAV will trigger the execution of orders. + + ) : null} + + + + + + + ) : ( + + + + + ) + } + > + + + + + + ) +} + +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]) + + return ( + + ) +} + +export function VisualNavCard({ + currency, + current, + change, + pendingFees, + pendingNav, +}: { + currency: Pick + current: number + change: number + pendingFees: number + pendingNav: number +}) { + return ( + + + + Current NAV + + {formatBalance(current, currency.displayName, 2)} + + + + + + Change in asset valuation + + = 0 ? 'statusOk' : 'statusCritical'}> + {formatBalance(change, currency.displayName, 2)} + + + + + Pending fees + + + -{formatBalance(pendingFees, currency.displayName, 2)} + + + + + + + + + Pending NAV + + + + {formatBalance(pendingNav, currency.displayName, 2)} + + ) } diff --git a/centrifuge-app/src/pages/NavManagement/Overview.tsx b/centrifuge-app/src/pages/NavManagement/Overview.tsx index 385efb4d0..6a0e25299 100644 --- a/centrifuge-app/src/pages/NavManagement/Overview.tsx +++ b/centrifuge-app/src/pages/NavManagement/Overview.tsx @@ -1,14 +1,12 @@ import { useParams } from 'react-router' -import { LayoutBase } from '../../components/LayoutBase' import { PageSummary } from '../../components/PageSummary' import { Tooltips } from '../../components/Tooltips' import { formatBalance } from '../../utils/formatting' -import { useDailyPoolStates, usePool, usePoolFees } from '../../utils/usePools' +import { useDailyPoolStates, usePool } from '../../utils/usePools' -import { CurrencyBalance, CurrencyMetadata } from '@centrifuge/centrifuge-js' -import { Divider, IconClockForward, Shelf, Stack, Text } from '@centrifuge/fabric' +import { CurrencyBalance } from '@centrifuge/centrifuge-js' +import { Stack, Text } from '@centrifuge/fabric' import { BN } from 'bn.js' -import React from 'react' import { LayoutSection } from '../../components/LayoutBase/LayoutSection' import { NavManagementAssetTable } from './NavManagementAssetTable' @@ -16,7 +14,7 @@ export default function NavManagementOverviewPage() { const { pid } = useParams<{ pid: string }>() if (!pid) throw new Error('Pool not found') return ( - + <> @@ -25,11 +23,8 @@ export default function NavManagementOverviewPage() { - - - - + ) } @@ -77,100 +72,3 @@ export function NavManagementPageSummary({ poolId }: { poolId: string }) { /> ) } - -export function NavOverviewCard({ poolId }: { poolId: string }) { - const pool = usePool(poolId) - const poolFees = usePoolFees(poolId) - const today = new Date() - today.setHours(0, 0, 0, 0) - const { poolStates: dailyPoolStates } = - useDailyPoolStates(poolId, new Date(new Date(pool.createdAt || today)), today) || {} - - 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 changeInValuation = React.useMemo(() => { - const lastUpdated = pool?.nav.lastUpdated || new Date() - const lastUpdatedSumBorrowedAmountByPeriod = dailyPoolStates?.find( - (state) => state.timestamp >= lastUpdated - )?.sumBorrowedAmountByPeriod - const todaySumBorrowedAmountByPeriod = dailyPoolStates?.[0]?.sumBorrowedAmountByPeriod - return lastUpdatedSumBorrowedAmountByPeriod && todaySumBorrowedAmountByPeriod - ? new BN(todaySumBorrowedAmountByPeriod).sub(new BN(lastUpdatedSumBorrowedAmountByPeriod)) - : new BN(0) - }, [dailyPoolStates, pool?.nav.lastUpdated]) - - return ( - - ) -} - -export function VisualNavCard({ - currency, - current, - change, - pendingFees, - pendingNav, -}: { - currency: Pick - current: number - change: number - pendingFees: number - pendingNav: number -}) { - return ( - - - - Current NAV - - {formatBalance(current, currency.displayName, 2)} - - - - - - Change in asset valuation - - = 0 ? 'statusOk' : 'statusCritical'}> - {formatBalance(change, currency.displayName, 2)} - - - - - Pending fees - - - -{formatBalance(pendingFees, currency.displayName, 2)} - - - - - - - - - Pending NAV - - - - {formatBalance(pendingNav, currency.displayName, 2)} - - - - ) -}