diff --git a/centrifuge-app/src/components/LoanList.tsx b/centrifuge-app/src/components/LoanList.tsx index 20d92ef717..0c2a4fae51 100644 --- a/centrifuge-app/src/components/LoanList.tsx +++ b/centrifuge-app/src/components/LoanList.tsx @@ -20,7 +20,7 @@ import { formatBalance } from '../utils/formatting' import { useAvailableFinancing } from '../utils/useLoans' import { useMetadata } from '../utils/useMetadata' import { useCentNFT } from '../utils/useNFTs' -import { usePool, usePoolMetadata } from '../utils/usePools' +import { useBorrowerAssetTransactions, usePool, usePoolMetadata } from '../utils/usePools' import { Column, DataTable, SortableTableHeader } from './DataTable' import { LoadBoundary } from './LoadBoundary' import LoanLabel, { getLoanLabelStatus } from './LoanLabel' @@ -205,6 +205,7 @@ function AssetName({ loan }: { loan: Row }) { function Amount({ loan }: { loan: Row }) { const pool = usePool(loan.poolId) const { current } = useAvailableFinancing(loan.poolId, loan.id) + const { currentFace } = useBorrowerAssetTransactions(loan.poolId, loan.id) function getAmount(l: Row) { switch (l.status) { @@ -220,6 +221,10 @@ function Amount({ loan }: { loan: Row }) { return formatBalance(l.totalRepaid, pool?.currency.symbol) } + if ('valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'oracle') { + return formatBalance(currentFace, pool?.currency.symbol) + } + return formatBalance(l.outstandingDebt, pool?.currency.symbol) default: diff --git a/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx b/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx index 38a4a9d6ce..18ac8d830e 100644 --- a/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx +++ b/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx @@ -90,7 +90,7 @@ export function ExternalFinanceForm({ loan }: { loan: LoanType }) { const repayFormRef = React.useRef(null) useFocusInvalidInput(repayForm, repayFormRef) - const borrowerAssetTransactions = useBorrowerAssetTransactions(loan.poolId, loan.id) + const { currentFace } = useBorrowerAssetTransactions(loan.poolId, loan.id) if (loan.status === 'Closed' || ('valuationMethod' in loan.pricing && loan.pricing.valuationMethod !== 'oracle')) { return null @@ -102,23 +102,6 @@ export function ExternalFinanceForm({ loan }: { loan: LoanType }) { const maturityDatePassed = loan?.pricing && 'maturityDate' in loan.pricing && new Date() > new Date(loan.pricing.maturityDate) - const currentFace = - borrowerAssetTransactions?.reduce((sum, trx) => { - if (trx.type === 'BORROWED') { - sum = new CurrencyBalance( - sum.add(trx.amount ? new BN(trx.amount).mul(new BN(100)) : new CurrencyBalance(0, pool.currency.decimals)), - pool.currency.decimals - ) - } - if (trx.type === 'REPAID') { - sum = new CurrencyBalance( - sum.sub(trx.amount ? new BN(trx.amount).mul(new BN(100)) : new CurrencyBalance(0, pool.currency.decimals)), - pool.currency.decimals - ) - } - return sum - }, new CurrencyBalance(0, pool.currency.decimals)) || new CurrencyBalance(0, pool.currency.decimals) - return ( diff --git a/centrifuge-app/src/pages/Loan/PricingValues.tsx b/centrifuge-app/src/pages/Loan/PricingValues.tsx index 767519c35b..001a22b60e 100644 --- a/centrifuge-app/src/pages/Loan/PricingValues.tsx +++ b/centrifuge-app/src/pages/Loan/PricingValues.tsx @@ -7,10 +7,10 @@ import { TinlakePool } from '../../utils/tinlake/useTinlakePools' type Props = { loan: Loan | TinlakeLoan pool: Pool | TinlakePool - latestSettlementPrice: string | null + latestPrice: CurrencyBalance } -export function PricingValues({ loan: { pricing }, pool, latestSettlementPrice }: Props) { +export function PricingValues({ loan: { pricing }, pool, latestPrice }: Props) { const isOutstandingDebtOrDiscountedCashFlow = 'valuationMethod' in pricing && (pricing.valuationMethod === 'outstandingDebt' || pricing.valuationMethod === 'discountedCashFlow') @@ -21,20 +21,12 @@ export function PricingValues({ loan: { pricing }, pool, latestSettlementPrice } const days = getAge(new Date(pricing.oracle.timestamp).toISOString()) - const getLatestPrice = () => { - if (latestSettlementPrice && pricing.oracle.value.isZero()) { - return new CurrencyBalance(latestSettlementPrice, pool.currency.decimals) - } - - return new CurrencyBalance(pricing.oracle.value.toString(), 18).toDecimal() - } - return ( <> diff --git a/centrifuge-app/src/pages/Loan/index.tsx b/centrifuge-app/src/pages/Loan/index.tsx index a94f5a84d2..ff8469afb6 100644 --- a/centrifuge-app/src/pages/Loan/index.tsx +++ b/centrifuge-app/src/pages/Loan/index.tsx @@ -1,4 +1,4 @@ -import { Loan as LoanType, Pool, TinlakeLoan } from '@centrifuge/centrifuge-js' +import { CurrencyBalance, Loan as LoanType, Pool, TinlakeLoan } from '@centrifuge/centrifuge-js' import { AnchorButton, Box, @@ -90,7 +90,7 @@ const Loan: React.FC<{ setShowOraclePricing?: () => void }> = ({ setShowOraclePr const metadataIsLoading = poolMetadataIsLoading || nftMetadataIsLoading const address = useAddress() const canOraclePrice = useCanSetOraclePrice(address) - const borrowerAssetTransactions = useBorrowerAssetTransactions(poolId, assetId) + const { borrowerAssetTransactions, currentFace } = useBorrowerAssetTransactions(poolId, assetId) const templateIds = poolMetadata?.loanTemplates?.map((s) => s.id) ?? [] const templateId = templateIds.at(-1) @@ -126,9 +126,21 @@ const Loan: React.FC<{ setShowOraclePricing?: () => void }> = ({ setShowOraclePr return 0 }, [originationDate, loan?.pricing.maturityDate]) - const latestSettlementPrice = borrowerAssetTransactions?.length - ? borrowerAssetTransactions[borrowerAssetTransactions.length - 1]?.settlementPrice - : null + const getLatestPrice = () => { + if (loan?.pricing && 'oracle' in loan.pricing) { + const latestSettlementPrice = borrowerAssetTransactions?.length + ? borrowerAssetTransactions[borrowerAssetTransactions.length - 1]?.settlementPrice + : null + + if (latestSettlementPrice && loan.pricing.oracle.value.isZero()) { + return new CurrencyBalance(latestSettlementPrice, pool.currency.decimals) + } + + return new CurrencyBalance(loan.pricing.oracle.value.toString(), 18) + } + + return new CurrencyBalance(0, 18) + } return ( @@ -186,7 +198,12 @@ const Loan: React.FC<{ setShowOraclePricing?: () => void }> = ({ setShowOraclePr ? [ { label: 'Current value', - value: `${formatBalance(loan.presentValue, pool.currency.symbol, 2, 2)}`, + value: `${formatBalance( + currentFace.toDecimal().mul(getLatestPrice().toDecimal()).div(100), + pool.currency.symbol, + 2, + 2 + )}`, }, ] : []), @@ -223,7 +240,7 @@ const Loan: React.FC<{ setShowOraclePricing?: () => void }> = ({ setShowOraclePr Pricing}> - + {canOraclePrice && setShowOraclePricing && diff --git a/centrifuge-app/src/pages/Pool/Assets/index.tsx b/centrifuge-app/src/pages/Pool/Assets/index.tsx index 277a62394c..82942389a4 100644 --- a/centrifuge-app/src/pages/Pool/Assets/index.tsx +++ b/centrifuge-app/src/pages/Pool/Assets/index.tsx @@ -10,7 +10,7 @@ import { Tooltips } from '../../../components/Tooltips' import { Dec } from '../../../utils/Decimal' import { formatBalance, formatPercentage } from '../../../utils/formatting' import { useLoans } from '../../../utils/useLoans' -import { usePool } from '../../../utils/usePools' +import { useAverageAmount, usePool } from '../../../utils/usePools' import { PoolDetailHeader } from '../Header' import { PoolDetailSideBar } from '../Overview' @@ -29,6 +29,7 @@ export const PoolDetailAssets: React.FC = () => { const { pid: poolId } = useParams<{ pid: string }>() const pool = usePool(poolId) const loans = useLoans(poolId) + const averageAmount = useAverageAmount(poolId) if (!pool) return null @@ -52,10 +53,14 @@ export const PoolDetailAssets: React.FC = () => { .toFixed(2) .toString() - const avgAmount = ongoingAssets - .reduce((curr, prev) => curr.add(prev.outstandingDebt.toDecimal() || Dec(0)), Dec(0)) - .dividedBy(ongoingAssets.length) - .toDecimalPlaces(2) + const isExternal = 'valuationMethod' in loans[0].pricing && loans[0].pricing.valuationMethod === 'oracle' + + const avgAmount = isExternal + ? averageAmount + : ongoingAssets + .reduce((curr, prev) => curr.add(prev.outstandingDebt.toDecimal() || Dec(0)), Dec(0)) + .dividedBy(ongoingAssets.length) + .toDecimalPlaces(2) const assetValue = formatBalance(pool.nav.latest.toDecimal().toNumber(), pool.currency.symbol) diff --git a/centrifuge-app/src/utils/usePools.ts b/centrifuge-app/src/utils/usePools.ts index 419bb91a3f..0f138d1173 100644 --- a/centrifuge-app/src/utils/usePools.ts +++ b/centrifuge-app/src/utils/usePools.ts @@ -1,9 +1,19 @@ -import Centrifuge, { BorrowerTransaction, Pool, PoolMetadata } from '@centrifuge/centrifuge-js' +import Centrifuge, { + BorrowerTransaction, + CurrencyBalance, + ExternalPricingInfo, + Pool, + PoolMetadata, +} from '@centrifuge/centrifuge-js' import { useCentrifuge, useCentrifugeQuery, useWallet } from '@centrifuge/centrifuge-react' +import BN from 'bn.js' +import Decimal from 'decimal.js-light' import { useEffect } from 'react' import { useQuery } from 'react-query' import { combineLatest, map, Observable } from 'rxjs' +import { Dec } from './Decimal' import { TinlakePool, useTinlakePools } from './tinlake/useTinlakePools' +import { useLoans } from './useLoans' import { useMetadata } from './useMetadata' export function usePools(suspense = true) { @@ -73,7 +83,82 @@ export function useBorrowerTransactions(poolId: string, from?: Date, to?: Date) return result } +export function useAverageAmount(poolId: string) { + const borrowerTransactions = useBorrowerTransactions(poolId) + const pool = usePool(poolId) + const loans = useLoans(poolId) + + if (!loans || !pool || !borrowerTransactions) return new BN(0) + + const getLatestPrice = (assetId: string) => { + const pricing = loans.find((loan) => loan.id === assetId)?.pricing as ExternalPricingInfo + + const borrowerAssetTransactions = borrowerTransactions.filter( + (borrowerTransaction) => borrowerTransaction.id === assetId + ) + + const latestSettlementPrice = borrowerAssetTransactions?.length + ? borrowerAssetTransactions[borrowerAssetTransactions.length - 1]?.settlementPrice + : null + + if (latestSettlementPrice && pricing.oracle.value.isZero()) { + return new CurrencyBalance(latestSettlementPrice, pool.currency.decimals) + } + + return new CurrencyBalance(pricing.oracle.value.toString(), 18) + } + + const poolsByLoanId = + borrowerTransactions.reduce((pools, pool) => { + const [, assetId] = pool.loanId.split('-') + + const somePool = { ...pool, oracleValue: getLatestPrice(assetId) } + if (pools[assetId]) { + pools[assetId] = [...pools[assetId], somePool] + } else { + pools[assetId] = [somePool] + } + + return pools + }, {} as Record>) || {} + + const currentFaces = Object.entries(poolsByLoanId).reduce((sum, [assetId, transactions]) => { + const item = + transactions.reduce((sum, trx) => { + if (trx.type === 'BORROWED') { + sum = new CurrencyBalance( + sum.add(trx.amount ? new BN(trx.amount).mul(new BN(100)) : new CurrencyBalance(0, pool.currency.decimals)), + pool.currency.decimals + ) + } + if (trx.type === 'REPAID') { + sum = new CurrencyBalance( + sum.sub(trx.amount ? new BN(trx.amount).mul(new BN(100)) : new CurrencyBalance(0, pool.currency.decimals)), + pool.currency.decimals + ) + } + return sum + }, new CurrencyBalance(0, pool.currency.decimals)) || new CurrencyBalance(0, pool.currency.decimals) + + sum = { ...sum, [assetId]: item } + + return sum + }, {} as Record) + + const currentValues = Object.entries(currentFaces).reduce((values, [assetId, currentFace]) => { + values[assetId] = currentFace.toDecimal().mul(getLatestPrice(assetId).toDecimal()).div(100) + + return values + }, {} as Record) + + return Object.values(currentValues) + .reduce((sum, value) => sum.add(value), Dec(0)) + .div(loans.length) +} + export function useBorrowerAssetTransactions(poolId: string, assetId: string, from?: Date, to?: Date) { + const pool = usePool(poolId) + const [result] = useCentrifugeQuery( ['borrowerAssetTransactions', poolId, assetId, from, to], (cent) => { @@ -87,10 +172,28 @@ export function useBorrowerAssetTransactions(poolId: string, assetId: string, fr }, { suspense: true, + enabled: !!pool, } ) - return result + const currentFace = + result?.reduce((sum, trx) => { + if (trx.type === 'BORROWED') { + sum = new CurrencyBalance( + sum.add(trx.amount ? new BN(trx.amount).mul(new BN(100)) : new CurrencyBalance(0, pool.currency.decimals)), + pool.currency.decimals + ) + } + if (trx.type === 'REPAID') { + sum = new CurrencyBalance( + sum.sub(trx.amount ? new BN(trx.amount).mul(new BN(100)) : new CurrencyBalance(0, pool.currency.decimals)), + pool.currency.decimals + ) + } + return sum + }, new CurrencyBalance(0, pool.currency.decimals)) || new CurrencyBalance(0, pool.currency.decimals) + + return { borrowerAssetTransactions: result, currentFace } } export function useDailyPoolStates(poolId: string, from?: Date, to?: Date) {