From 3dfec3da2286a49433a9b7aa990732b84596f5f6 Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Wed, 22 May 2024 09:41:28 +0200 Subject: [PATCH 01/10] Report updates (#2094) --- centrifuge-app/src/components/DataTable.tsx | 39 +-- .../src/components/Portfolio/usePortfolio.ts | 22 +- .../src/components/Report/AssetList.tsx | 174 +++++++----- .../components/Report/AssetTransactions.tsx | 170 +++++++++--- .../src/components/Report/FeeTransactions.tsx | 104 ++++--- .../src/components/Report/Holders.tsx | 123 +++++++-- .../Report/InvestorTransactions.tsx | 246 ++++++++++++----- .../src/components/Report/PoolBalance.tsx | 165 ++++++------ .../src/components/Report/ReportContext.tsx | 93 +++---- .../src/components/Report/ReportFilter.tsx | 255 ++++++++++++++---- .../src/components/Report/TokenPrice.tsx | 153 +++++++++++ .../src/components/Report/UserFeedback.tsx | 3 +- .../src/components/Report/index.tsx | 18 +- centrifuge-app/src/utils/formatting.ts | 4 +- centrifuge-app/src/utils/useLiquidityPools.ts | 4 + centrifuge-js/src/modules/pools.ts | 143 +++++----- centrifuge-js/src/types/subquery.ts | 57 ++-- 17 files changed, 1214 insertions(+), 559 deletions(-) create mode 100644 centrifuge-app/src/components/Report/TokenPrice.tsx diff --git a/centrifuge-app/src/components/DataTable.tsx b/centrifuge-app/src/components/DataTable.tsx index dad9c7c2a7..9a4505ae8a 100644 --- a/centrifuge-app/src/components/DataTable.tsx +++ b/centrifuge-app/src/components/DataTable.tsx @@ -13,9 +13,11 @@ import { Text, Tooltip, } from '@centrifuge/fabric' + import css from '@styled-system/css' import BN from 'bn.js' import Decimal from 'decimal.js-light' +import { getIn } from 'formik' import * as React from 'react' import { Link, LinkProps } from 'react-router-dom' import styled from 'styled-components' @@ -60,37 +62,22 @@ export type Column = { } const sorter = >(data: Array, order: OrderBy, sortKey?: string) => { if (!sortKey) return data - if (order === 'asc') { - return data.sort((a, b) => { - try { - if ( - (a[sortKey] instanceof Decimal && b[sortKey] instanceof Decimal) || - (BN.isBN(a[sortKey]) && BN.isBN(b[sortKey])) - ) - return a[sortKey].gt(b[sortKey]) ? 1 : -1 - - if (typeof a[sortKey] === 'string' && typeof b[sortKey] === 'string') { - return new BN(a[sortKey]).gt(new BN(b[sortKey])) ? 1 : -1 - } - } catch {} - - return a[sortKey] > b[sortKey] ? 1 : -1 - }) - } + const up = order === 'asc' ? 1 : -1 + const down = order === 'asc' ? -1 : 1 + return data.sort((a, b) => { + const A = getIn(a, sortKey) + const B = getIn(b, sortKey) try { - if ( - (a[sortKey] instanceof Decimal && b[sortKey] instanceof Decimal) || - (BN.isBN(a[sortKey]) && BN.isBN(b[sortKey])) - ) - return b[sortKey].gt(a[sortKey]) ? 1 : -1 - - if (typeof a[sortKey] === 'string' && typeof b[sortKey] === 'string') { - return new BN(b[sortKey]).gt(new BN(a[sortKey])) ? 1 : -1 + if ((A instanceof Decimal && B instanceof Decimal) || (BN.isBN(A) && BN.isBN(B))) + return A.gt(B as any) ? up : down + + if (typeof A === 'string' && typeof B === 'string') { + return new BN(A).gt(new BN(B)) ? up : down } } catch {} - return b[sortKey] > a[sortKey] ? 1 : -1 + return A > B ? up : down }) } diff --git a/centrifuge-app/src/components/Portfolio/usePortfolio.ts b/centrifuge-app/src/components/Portfolio/usePortfolio.ts index 03aa66f9f3..840a64bd2c 100644 --- a/centrifuge-app/src/components/Portfolio/usePortfolio.ts +++ b/centrifuge-app/src/components/Portfolio/usePortfolio.ts @@ -111,16 +111,18 @@ const getPriceAtDate = ( day: number, today: Date ) => { - return dailyTrancheStatesByTrancheId[trancheId].slice(0 - rangeValue)?.find((state) => { - return ( - `${new Date(state.timestamp).getMonth()}/${new Date(state.timestamp).getDate()}/${new Date( - state.timestamp - ).getFullYear()}` === - `${new Date(today.getTime() - day * 1000 * 60 * 60 * 24).getMonth()}/${new Date( - today.getTime() - day * 1000 * 60 * 60 * 24 - ).getDate()}/${new Date(today.getTime() - day * 1000 * 60 * 60 * 24).getFullYear()}` - ) - })?.tokenPrice + return new Price( + dailyTrancheStatesByTrancheId[trancheId].slice(0 - rangeValue)?.find((state) => { + return ( + `${new Date(state.timestamp).getMonth()}/${new Date(state.timestamp).getDate()}/${new Date( + state.timestamp + ).getFullYear()}` === + `${new Date(today.getTime() - day * 1000 * 60 * 60 * 24).getMonth()}/${new Date( + today.getTime() - day * 1000 * 60 * 60 * 24 + ).getDate()}/${new Date(today.getTime() - day * 1000 * 60 * 60 * 24).getFullYear()}` + ) + })?.tokenPrice ?? Price.fromFloat(1) + ) } export function usePortfolio(address?: string) { diff --git a/centrifuge-app/src/components/Report/AssetList.tsx b/centrifuge-app/src/components/Report/AssetList.tsx index d79bfdf15f..387bf2c2fd 100644 --- a/centrifuge-app/src/components/Report/AssetList.tsx +++ b/centrifuge-app/src/components/Report/AssetList.tsx @@ -2,40 +2,99 @@ import { Loan, Pool } from '@centrifuge/centrifuge-js' import { Text } from '@centrifuge/fabric' import * as React from 'react' import { formatDate } from '../../utils/date' -import { formatBalanceAbbreviated, formatPercentage } from '../../utils/formatting' +import { formatBalance, formatPercentage } from '../../utils/formatting' import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl' import { useLoans } from '../../utils/useLoans' import { DataTable } from '../DataTable' import { Spinner } from '../Spinner' -import type { TableDataRow } from './index' import { ReportContext } from './ReportContext' import { UserFeedback } from './UserFeedback' +import type { TableDataRow } from './index' -const headers = [ - 'ID', - 'Status', - // 'Collateral value', - 'Outstanding', - 'Total financed', - 'Total repaid', - 'Financing date', - 'Maturity date', - 'Interest rate', - // 'Advance rate', - // 'PD', - // 'LGD', - // 'Discount rate', -] - -const columns = headers.map((col, index) => ({ - align: 'left', - header: col, - cell: (row: TableDataRow) => {(row.value as any)[index]}, -})) +const noop = (v: any) => v export function AssetList({ pool }: { pool: Pool }) { const loans = useLoans(pool.id) as Loan[] - const { setCsvData, startDate, endDate } = React.useContext(ReportContext) + const { setCsvData, loanStatus } = React.useContext(ReportContext) + const { symbol } = pool.currency + + const columnConfig = [ + { + header: 'ID', + align: 'left', + csvOnly: false, + formatter: noop, + }, + { + header: 'Status', + align: 'left', + csvOnly: false, + formatter: noop, + }, + { + header: 'Outstanding', + align: 'right', + csvOnly: false, + formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, symbol, 5) : '-'), + }, + { + header: 'Outstanding currency', + align: 'left', + csvOnly: true, + formatter: noop, + }, + { + header: 'Total financed', + align: 'right', + csvOnly: false, + formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, symbol, 5) : '-'), + }, + { + header: 'Total financed currency', + align: 'left', + csvOnly: true, + formatter: noop, + }, + { + header: 'Total repaid', + align: 'right', + csvOnly: false, + formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, symbol, 5) : '-'), + }, + { + header: 'Total repaid currency', + align: 'left', + csvOnly: true, + formatter: noop, + }, + { + header: 'Financing date', + align: 'left', + csvOnly: false, + formatter: (v: any) => (v !== '-' ? formatDate(v) : v), + }, + { + header: 'Maturity date', + align: 'left', + csvOnly: false, + formatter: formatDate, + }, + { + header: 'Interest rate', + align: 'left', + csvOnly: false, + formatter: (v: any) => (typeof v === 'number' ? formatPercentage(v, true, undefined, 5) : '-'), + }, + ] + + const columns = columnConfig + .map((col, index) => ({ + align: col.align, + header: col.header, + cell: (row: TableDataRow) => {col.formatter((row.value as any)[index])}, + csvOnly: col.csvOnly, + })) + .filter((col) => !col.csvOnly) const data: TableDataRow[] = React.useMemo(() => { if (!loans) { @@ -48,58 +107,43 @@ export function AssetList({ pool }: { pool: Pool }) { name: '', value: [ loan.id, - loan.status === 'Created' ? 'New' : loan.status, - // 'value' in loan.pricing - // ? formatBalanceAbbreviated(loan.pricing.value.toDecimal(), pool.currency.symbol) - // : '-', - 'outstandingDebt' in loan - ? formatBalanceAbbreviated(loan.outstandingDebt.toDecimal(), pool.currency.symbol) - : '-', - 'totalBorrowed' in loan - ? formatBalanceAbbreviated(loan.totalBorrowed.toDecimal(), pool.currency.symbol) - : '-', - 'totalRepaid' in loan ? formatBalanceAbbreviated(loan.totalRepaid.toDecimal(), pool.currency.symbol) : '-', - 'originationDate' in loan ? formatDate(loan.originationDate) : '-', - formatDate(loan.pricing.maturityDate), - 'interestRate' in loan.pricing ? formatPercentage(loan.pricing.interestRate.toPercent()) : '-', - // 'advanceRate' in loan.pricing ? formatPercentage(loan.pricing.advanceRate.toPercent()) : '-', - // 'probabilityOfDefault' in loan.pricing - // ? formatPercentage((loan.pricing.probabilityOfDefault as Rate).toPercent()) - // : '-', - // 'lossGivenDefault' in loan.pricing - // ? formatPercentage((loan.pricing.lossGivenDefault as Rate).toPercent()) - // : '-', - // 'discountRate' in loan.pricing ? formatPercentage((loan.pricing.discountRate as Rate).toPercent()) : '-', + loan.status === 'Closed' ? 'Repaid' : new Date() > new Date(loan.pricing.maturityDate) ? 'Overdue' : 'Active', + 'outstandingDebt' in loan ? loan.outstandingDebt.toFloat() : '-', + symbol, + 'totalBorrowed' in loan ? loan.totalBorrowed.toFloat() : '-', + symbol, + 'totalRepaid' in loan ? loan.totalRepaid.toFloat() : '-', + symbol, + 'originationDate' in loan ? loan.originationDate : '-', + loan.pricing.maturityDate, + 'interestRate' in loan.pricing ? loan.pricing.interestRate.toPercent().toNumber() : '-', ], heading: false, })) - }, [loans, pool.currency.symbol]) + .filter((row) => (loanStatus === 'all' || !loanStatus ? true : row.value[1] === loanStatus)) + }, [loans, symbol, loanStatus]) - const dataUrl = React.useMemo(() => { + React.useEffect(() => { if (!data.length) { return } - const formatted = data - .map(({ value }) => value as string[]) - .map((values) => Object.fromEntries(headers.map((_, index) => [headers[index], `"${values[index]}"`]))) - - return getCSVDownloadUrl(formatted) - }, [data]) - - React.useEffect(() => { - setCsvData( - dataUrl - ? { - dataUrl, - fileName: `${pool.id}-asset-list-${startDate}-${endDate}.csv`, - } - : undefined + const formatted = data.map(({ value: values }) => + Object.fromEntries(columnConfig.map((col, index) => [col.header, `"${values[index]}"`])) ) + const dataUrl = getCSVDownloadUrl(formatted) - return () => setCsvData(undefined) + setCsvData({ + dataUrl, + fileName: `${pool.id}-asset-list-${loanStatus.toLowerCase()}.csv`, + }) + + return () => { + setCsvData(undefined) + URL.revokeObjectURL(dataUrl) + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [dataUrl, pool.id, startDate, endDate]) + }, [data]) if (!loans) { return diff --git a/centrifuge-app/src/components/Report/AssetTransactions.tsx b/centrifuge-app/src/components/Report/AssetTransactions.tsx index 2f77f32dd5..dc4e359c06 100644 --- a/centrifuge-app/src/components/Report/AssetTransactions.tsx +++ b/centrifuge-app/src/components/Report/AssetTransactions.tsx @@ -1,9 +1,10 @@ -import { Pool } from '@centrifuge/centrifuge-js/dist/modules/pools' -import { Text } from '@centrifuge/fabric' +import { Pool } from '@centrifuge/centrifuge-js' +import { formatBalance, useGetExplorerUrl } from '@centrifuge/centrifuge-react' +import { IconAnchor, IconExternalLink, Text } from '@centrifuge/fabric' import * as React from 'react' import { formatDate } from '../../utils/date' -import { formatBalanceAbbreviated } from '../../utils/formatting' import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl' +import { useMetadataMulti } from '../../utils/useMetadata' import { useAssetTransactions } from '../../utils/usePools' import { DataTable } from '../DataTable' import { Spinner } from '../Spinner' @@ -12,61 +13,148 @@ import { UserFeedback } from './UserFeedback' import type { TableDataRow } from './index' import { formatAssetTransactionType } from './utils' +const noop = (v: any) => v + export function AssetTransactions({ pool }: { pool: Pool }) { - const { startDate, endDate, setCsvData } = React.useContext(ReportContext) - const transactions = useAssetTransactions(pool.id, startDate, endDate) + const { startDate, endDate, setCsvData, txType, loan: loanId } = React.useContext(ReportContext) + const transactions = useAssetTransactions(pool.id, new Date(startDate), new Date(endDate)) + const explorer = useGetExplorerUrl('centrifuge') + + const columnConfig = [ + { + header: 'Asset ID', + align: 'left', + csvOnly: false, + formatter: noop, + }, + { + header: 'Asset name', + align: 'left', + csvOnly: false, + formatter: noop, + }, + { + header: 'Epoch', + align: 'right', + csvOnly: false, + formatter: noop, + }, + { + header: 'Date', + align: 'left', + csvOnly: true, + formatter: formatDate, + }, + { + header: 'Transaction type', + align: 'right', + csvOnly: false, + formatter: noop, + }, + { + header: 'Currency amount', + align: 'left', + csvOnly: true, + formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, pool.currency.symbol, 5) : '-'), + }, + { + header: 'Currency', + align: 'right', + csvOnly: false, + formatter: noop, + }, + { + header: 'Transaction', + align: 'right', + csvOnly: false, + formatter: (v: any) => ( + + + + ), + }, + ] - const headers = ['Asset ID', 'Epoch', 'Date', 'Type', `${pool ? `${pool.currency.symbol} amount` : '—'}`] + const metaUrls = [...new Set(transactions?.map((tx) => tx.asset.metadata) || [])] + const queries = useMetadataMulti(metaUrls) + const metadataByUrl: Record = {} + metaUrls.forEach((url, i) => { + metadataByUrl[url] = queries[i].data + }) const data: TableDataRow[] = React.useMemo(() => { if (!transactions) { return [] } - return transactions?.map((tx) => ({ - name: '', - value: [ - tx.asset.id.split('-').at(-1)!, - tx.epochId.split('-').at(-1)!, - formatDate(tx.timestamp.toString()), - formatAssetTransactionType(tx.type), - tx.amount ? formatBalanceAbbreviated(tx.amount, pool.currency.symbol) : '-', - ], - heading: false, - })) - }, [transactions, pool.currency.symbol]) + return transactions + ?.map((tx) => ({ + name: '', + value: [ + tx.asset.id.split('-').at(-1)!, + metadataByUrl[tx.asset.metadata]?.name ?? '', + tx.epochId.split('-').at(-1)!, + tx.timestamp.toISOString(), + formatAssetTransactionType(tx.type), + tx.amount?.toFloat() ?? '', + pool.currency.symbol, + tx.hash, + ], + heading: false, + })) + .filter((row) => { + if (!loanId || loanId === 'all') return true + return loanId === row.value[0] + }) + .filter((row) => (!txType || txType === 'all' ? true : row.value[3] === txType)) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [transactions, txType, loanId, ...queries.map((q) => q.data)]) - const columns = headers.map((col, index) => ({ - align: 'left', - header: col, - cell: (row: TableDataRow) => {(row.value as any)[index]}, - })) + const columns = columnConfig + .map((col, index) => ({ + align: col.align, + header: col.header, + cell: (row: TableDataRow) => {col.formatter((row.value as any)[index])}, + csvOnly: col.csvOnly, + })) + .filter((col) => !col.csvOnly) - const dataUrl = React.useMemo(() => { + React.useEffect(() => { if (!data.length) { return } - const formatted = data - .map(({ value }) => value as string[]) - .map((values) => Object.fromEntries(headers.map((_, index) => [headers[index], `"${values[index]}"`]))) - - return getCSVDownloadUrl(formatted) - }, [data]) - - React.useEffect(() => { - setCsvData( - dataUrl - ? { - dataUrl, - fileName: `${pool.id}-asset-transactions-${startDate}-${endDate}.csv`, - } - : undefined + const formatted = data.map(({ value: values }) => + Object.fromEntries(columnConfig.map((col, index) => [col.header, `"${values[index]}"`])) ) + const dataUrl = getCSVDownloadUrl(formatted) - return () => setCsvData(undefined) + setCsvData({ + dataUrl, + fileName: `${pool.id}-asset-transactions-${formatDate(startDate, { + weekday: 'short', + month: 'short', + day: '2-digit', + year: 'numeric', + }).replaceAll(',', '')}-${formatDate(endDate, { + weekday: 'short', + month: 'short', + day: '2-digit', + year: 'numeric', + }).replaceAll(',', '')}.csv`, + }) + + return () => { + setCsvData(undefined) + URL.revokeObjectURL(dataUrl) + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [dataUrl, startDate, endDate, pool.id]) + }, [data]) if (!transactions) { return diff --git a/centrifuge-app/src/components/Report/FeeTransactions.tsx b/centrifuge-app/src/components/Report/FeeTransactions.tsx index 9ac48c4c9a..fa860f1d91 100644 --- a/centrifuge-app/src/components/Report/FeeTransactions.tsx +++ b/centrifuge-app/src/components/Report/FeeTransactions.tsx @@ -2,7 +2,7 @@ import { Pool } from '@centrifuge/centrifuge-js' import { Text } from '@centrifuge/fabric' import * as React from 'react' import { formatDate } from '../../utils/date' -import { formatBalanceAbbreviated } from '../../utils/formatting' +import { formatBalance } from '../../utils/formatting' import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl' import { useFeeTransactions, usePoolMetadata } from '../../utils/usePools' import { DataTable } from '../DataTable' @@ -12,12 +12,45 @@ import { UserFeedback } from './UserFeedback' import type { TableDataRow } from './index' import { formatPoolFeeTransactionType } from './utils' +const noop = (v: any) => v + export function FeeTransactions({ pool }: { pool: Pool }) { - const { startDate, endDate, setCsvData } = React.useContext(ReportContext) - const transactions = useFeeTransactions(pool.id, startDate, endDate) + const { startDate, endDate, setCsvData, txType } = React.useContext(ReportContext) + const transactions = useFeeTransactions(pool.id, new Date(startDate), new Date(endDate)) const { data: poolMetadata } = usePoolMetadata(pool) - const headers = ['Date', 'Fee name', 'Transaction type', `${pool ? `${pool.currency.symbol} amount` : '—'}`] + const columnConfig = [ + { + header: 'Date', + align: 'left', + csvOnly: false, + formatter: formatDate, + }, + { + header: 'Fee name', + align: 'left', + csvOnly: false, + formatter: noop, + }, + { + header: 'Transaction type', + align: 'left', + csvOnly: false, + formatter: noop, + }, + { + header: 'Currency ammount', + align: 'right', + csvOnly: false, + formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, pool.currency.symbol, 5) : '-'), + }, + { + header: 'Currency', + align: 'left', + csvOnly: true, + formatter: noop, + }, + ] const data: TableDataRow[] = React.useMemo(() => { if (!transactions) { @@ -26,49 +59,60 @@ export function FeeTransactions({ pool }: { pool: Pool }) { return transactions ?.filter((tx) => tx.type !== 'PROPOSED' && tx.type !== 'ADDED' && tx.type !== 'REMOVED') + .filter((tx) => (!txType || txType === 'all' ? true : tx.type === txType)) .map((tx) => ({ name: '', value: [ - formatDate(tx.timestamp.toString()), + tx.timestamp.toISOString(), poolMetadata?.pool?.poolFees?.find((f) => f.id === tx.poolFee.feeId)?.name || '-', formatPoolFeeTransactionType(tx.type), - tx.amount ? formatBalanceAbbreviated(tx.amount, pool.currency.symbol, 3) : '-', + tx.amount?.toFloat() ?? '-', + pool.currency.symbol, ], heading: false, })) - }, [transactions, pool.currency.symbol]) + }, [transactions, txType, poolMetadata, pool.currency.symbol]) - const columns = headers.map((col, index) => ({ - align: 'left', - header: col, - cell: (row: TableDataRow) => {(row.value as any)[index]}, - })) + const columns = columnConfig + .map((col, index) => ({ + align: col.align, + header: col.header, + cell: (row: TableDataRow) => {col.formatter((row.value as any)[index])}, + csvOnly: col.csvOnly, + })) + .filter((col) => !col.csvOnly) - const dataUrl = React.useMemo(() => { + React.useEffect(() => { if (!data.length) { return } - const formatted = data - .map(({ value }) => value as string[]) - .map((values) => Object.fromEntries(headers.map((_, index) => [headers[index], `"${values[index]}"`]))) - - return getCSVDownloadUrl(formatted) - }, [data]) - - React.useEffect(() => { - setCsvData( - dataUrl - ? { - dataUrl, - fileName: `${pool.id}-fee-transactions-${startDate}-${endDate}.csv`, - } - : undefined + const formatted = data.map(({ value: values }) => + Object.fromEntries(columnConfig.map((col, index) => [col.header, `"${values[index]}"`])) ) + const dataUrl = getCSVDownloadUrl(formatted) + + setCsvData({ + dataUrl, + fileName: `${pool.id}-fee-transactions-${formatDate(startDate, { + weekday: 'short', + month: 'short', + day: '2-digit', + year: 'numeric', + }).replaceAll(',', '')}-${formatDate(endDate, { + weekday: 'short', + month: 'short', + day: '2-digit', + year: 'numeric', + }).replaceAll(',', '')}.csv`, + }) - return () => setCsvData(undefined) + return () => { + setCsvData(undefined) + URL.revokeObjectURL(dataUrl) + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [dataUrl, startDate, endDate, pool.id]) + }, [data]) if (!transactions) { return diff --git a/centrifuge-app/src/components/Report/Holders.tsx b/centrifuge-app/src/components/Report/Holders.tsx index f0b7d51378..ec2b0d1c37 100644 --- a/centrifuge-app/src/components/Report/Holders.tsx +++ b/centrifuge-app/src/components/Report/Holders.tsx @@ -1,6 +1,7 @@ -import { Pool } from '@centrifuge/centrifuge-js' +import { Pool, isSameAddress } from '@centrifuge/centrifuge-js' import { useCentrifugeUtils } from '@centrifuge/centrifuge-react' import { Text } from '@centrifuge/fabric' +import { isAddress } from '@polkadot/util-crypto' import * as React from 'react' import { evmChains } from '../../config' import { formatBalance } from '../../utils/formatting' @@ -8,50 +9,113 @@ import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl' import { useHolders } from '../../utils/usePools' import { DataTable } from '../DataTable' import { Spinner } from '../Spinner' -import type { TableDataRow } from './index' import { ReportContext } from './ReportContext' import { UserFeedback } from './UserFeedback' +import type { TableDataRow } from './index' import { copyable } from './utils' -const headers = ['Network', 'Account', 'Position', 'Pending invest order', 'Pending redeem order'] - const noop = (v: any) => v -const cellFormatters = [noop, copyable, noop, noop, noop] - -const columns = headers.map((col, index) => ({ - align: 'left', - header: col, - cell: (row: TableDataRow) => {cellFormatters[index]((row.value as any)[index])}, -})) export function Holders({ pool }: { pool: Pool }) { - const { activeTranche, setCsvData } = React.useContext(ReportContext) + const { activeTranche, setCsvData, network, address } = React.useContext(ReportContext) const utils = useCentrifugeUtils() const holders = useHolders(pool.id, activeTranche === 'all' ? undefined : activeTranche) + const columnConfig = [ + { + header: 'Network', + align: 'left', + csvOnly: false, + formatter: noop, + }, + { + header: 'Account', + align: 'left', + csvOnly: false, + formatter: copyable, + }, + { + header: 'Position', + align: 'right', + csvOnly: false, + formatter: (v: any, row: any) => (typeof v === 'number' ? formatBalance(v, row.token.currency.symbol, 5) : '-'), + }, + { + header: 'Position currency', + align: 'left', + csvOnly: true, + formatter: noop, + }, + { + header: 'Pending invest order', + align: 'right', + csvOnly: false, + formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, pool.currency.symbol, 5) : '-'), + }, + { + header: 'Pending invest order currency', + align: 'left', + csvOnly: true, + formatter: noop, + }, + { + header: 'Pending redeem order', + align: 'right', + csvOnly: false, + formatter: (v: any, row: any) => (typeof v === 'number' ? formatBalance(v, row.token.currency.symbol, 5) : '-'), + }, + { + header: 'Pending redeem order currency', + align: 'left', + csvOnly: true, + formatter: noop, + }, + ] + + const columns = columnConfig + .map((col, index) => ({ + align: col.align, + header: col.header, + cell: (row: TableDataRow) => {col.formatter((row.value as any)[index], row)}, + csvOnly: col.csvOnly, + })) + .filter((col) => !col.csvOnly) + const data: TableDataRow[] = React.useMemo(() => { if (!holders) { return [] } - return holders .filter((holder) => !holder.balance.isZero() || !holder.claimableTrancheTokens.isZero()) - .map((holder) => ({ - name: '', - value: [ - (evmChains as any)[holder.chainId]?.name || 'Centrifuge', - holder.evmAddress || utils.formatAddress(holder.accountId), - formatBalance( - holder.balance.toDecimal().add(holder.claimableTrancheTokens.toDecimal()), - pool.tranches[0].currency // TODO: not hardcode to tranche index 0 - ), - formatBalance(holder.pendingInvestCurrency.toDecimal(), pool.currency), - formatBalance(holder.pendingRedeemTrancheTokens.toDecimal(), pool.tranches[0].currency), // TODO: not hardcode to tranche index 0 - ], - heading: false, - })) - }, [holders]) + .filter((tx) => { + if (!network || network === 'all') return true + return network === (tx.chainId || 'centrifuge') + }) + .map((holder) => { + const token = pool.tranches.find((t) => t.id === holder.trancheId)! + return { + name: '', + value: [ + (evmChains as any)[holder.chainId]?.name || 'Centrifuge', + holder.evmAddress || utils.formatAddress(holder.accountId), + holder.balance.toFloat() + holder.claimableTrancheTokens.toFloat(), + token.currency.symbol, + holder.pendingInvestCurrency.toFloat(), + pool.currency.symbol, + holder.pendingRedeemTrancheTokens.toFloat(), + token.currency.symbol, + ], + token, + heading: false, + } + }) + .filter((row) => { + if (!address) return true + return isAddress(address) && isSameAddress(address, row.value[1]) + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [holders, network, pool, address]) const dataUrl = React.useMemo(() => { if (!data.length) { @@ -60,9 +124,10 @@ export function Holders({ pool }: { pool: Pool }) { const formatted = data .map(({ value }) => value as string[]) - .map((values) => Object.fromEntries(headers.map((_, index) => [headers[index], `"${values[index]}"`]))) + .map((values) => Object.fromEntries(columnConfig.map((col, index) => [col.header, `"${values[index]}"`]))) return getCSVDownloadUrl(formatted) + // eslint-disable-next-line react-hooks/exhaustive-deps }, [data]) React.useEffect(() => { diff --git a/centrifuge-app/src/components/Report/InvestorTransactions.tsx b/centrifuge-app/src/components/Report/InvestorTransactions.tsx index 62a029e1ee..cdbe68f1f8 100644 --- a/centrifuge-app/src/components/Report/InvestorTransactions.tsx +++ b/centrifuge-app/src/components/Report/InvestorTransactions.tsx @@ -1,50 +1,148 @@ +import { isSameAddress } from '@centrifuge/centrifuge-js' import { Pool } from '@centrifuge/centrifuge-js/dist/modules/pools' -import { useCentrifugeUtils } from '@centrifuge/centrifuge-react' -import { Text } from '@centrifuge/fabric' +import { useCentrifugeUtils, useGetExplorerUrl } from '@centrifuge/centrifuge-react' +import { IconAnchor, IconExternalLink, Text } from '@centrifuge/fabric' +import { isAddress } from '@polkadot/util-crypto' import * as React from 'react' import { evmChains } from '../../config' import { formatDate } from '../../utils/date' import { formatBalance } from '../../utils/formatting' import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl' import { useInvestorTransactions } from '../../utils/usePools' -import { DataTable } from '../DataTable' +import { DataTable, SortableTableHeader } from '../DataTable' import { Spinner } from '../Spinner' -import type { TableDataRow } from './index' import { ReportContext } from './ReportContext' import { UserFeedback } from './UserFeedback' +import type { TableDataRow } from './index' import { copyable, formatInvestorTransactionsType } from './utils' const noop = (v: any) => v -const cellFormatters = [noop, noop, copyable, noop, noop, noop, noop, noop, noop] export function InvestorTransactions({ pool }: { pool: Pool }) { - const { activeTranche, setCsvData, startDate, endDate, investorTxType } = React.useContext(ReportContext) + const { activeTranche, setCsvData, startDate, endDate, txType, address, network } = React.useContext(ReportContext) const utils = useCentrifugeUtils() + const explorer = useGetExplorerUrl('centrifuge') + + const columnConfig = [ + { + header: 'Token', + align: 'left', + sortable: false, + csvOnly: false, + formatter: noop, + }, + { + header: 'Network', + align: 'left', + sortable: false, + csvOnly: false, + formatter: noop, + }, + { + header: 'Account', + align: 'left', + sortable: false, + csvOnly: false, + formatter: copyable, + }, + { + header: 'Epoch', + align: 'left', + sortable: false, + csvOnly: false, + formatter: noop, + }, + { + header: 'Date', + align: 'left', + sortable: true, + csvOnly: false, + formatter: (v: any) => formatDate(v), + }, + { + header: 'Transaction type', + align: 'left', + sortable: false, + csvOnly: false, + formatter: noop, + }, + { + header: 'Currency amount', + align: 'right', + sortable: true, + csvOnly: false, + formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, pool.currency.symbol, 5) : '-'), + }, + { + header: 'Currency', + align: 'left', + sortable: false, + csvOnly: true, + formatter: noop, + }, + { + header: 'Token amount', + align: 'right', + sortable: true, + csvOnly: false, + formatter: (v: any, row: any) => (typeof v === 'number' ? formatBalance(v, row[9], 5) : '-'), + }, + { + header: 'Token currency', + align: 'left', + sortable: false, + csvOnly: true, + formatter: noop, + }, + { + header: 'Price', + align: 'right', + sortable: true, + csvOnly: false, + formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, pool.currency.symbol, 5) : '-'), + }, + { + header: 'Price currency', + align: 'left', + sortable: false, + csvOnly: true, + formatter: noop, + }, + { + header: 'Transaction', + align: 'left', + sortable: false, + csvOnly: false, + formatter: (v: any) => ( + + + + ), + }, + ] const transactions = useInvestorTransactions( pool.id, activeTranche === 'all' ? undefined : activeTranche, - startDate, - endDate + startDate ? new Date(startDate) : undefined, + endDate ? new Date(endDate) : undefined ) - const headers = [ - 'Token', - 'Network', - 'Account', - 'Epoch', - 'Date', - 'Type', - `${pool ? `${pool.currency.symbol} amount` : '—'}`, - 'Token amount', - 'Price', - ] + const columns = columnConfig - const columns = headers.map((col, index) => ({ - align: 'left', - header: col, - cell: (row: TableDataRow) => {cellFormatters[index]((row.value as any)[index])}, - })) + .map((col, index) => ({ + align: col.align, + header: col.sortable ? : col.header, + cell: (row: TableDataRow) => {col.formatter((row.value as any)[index], row.value)}, + sortKey: col.sortable ? `value[${index}]` : undefined, + csvOnly: col.csvOnly, + })) + .filter((col) => !col.csvOnly) const data: TableDataRow[] = React.useMemo(() => { if (!transactions) { @@ -53,91 +151,107 @@ export function InvestorTransactions({ pool }: { pool: Pool }) { return transactions ?.filter((tx) => { - if (investorTxType == 'all') { + if (txType === 'all') { return true } if ( - investorTxType == 'orders' && - (tx.type == 'INVEST_ORDER_UPDATE' || - tx.type == 'REDEEM_ORDER_UPDATE' || - tx.type == 'INVEST_ORDER_CANCEL' || - tx.type == 'REDEEM_ORDER_CANCEL') + txType === 'orders' && + (tx.type === 'INVEST_ORDER_UPDATE' || + tx.type === 'REDEEM_ORDER_UPDATE' || + tx.type === 'INVEST_ORDER_CANCEL' || + tx.type === 'REDEEM_ORDER_CANCEL') ) { return true } - if (investorTxType == 'executions' && (tx.type == 'INVEST_EXECUTION' || tx.type == 'REDEEM_EXECUTION')) { + if (txType === 'executions' && (tx.type === 'INVEST_EXECUTION' || tx.type === 'REDEEM_EXECUTION')) { return true } if ( - investorTxType == 'transfers' && - (tx.type == 'INVEST_COLLECT' || - tx.type == 'REDEEM_COLLECT' || - tx.type == 'INVEST_LP_COLLECT' || - tx.type == 'REDEEM_LP_COLLECT' || - tx.type == 'TRANSFER_IN' || - tx.type == 'TRANSFER_OUT') + txType === 'transfers' && + (tx.type === 'INVEST_COLLECT' || + tx.type === 'REDEEM_COLLECT' || + tx.type === 'INVEST_LP_COLLECT' || + tx.type === 'REDEEM_LP_COLLECT' || + tx.type === 'TRANSFER_IN' || + tx.type === 'TRANSFER_OUT') ) { return true } return false }) + .filter((tx) => { + if (!network || network === 'all') return true + return network === (tx.chainId || 'centrifuge') + }) .map((tx) => { - const tokenId = tx.trancheId.split('-')[1] - const token = pool.tranches.find((t) => t.id === tokenId)! - + const token = pool.tranches.find((t) => t.id === tx.trancheId)! return { name: '', value: [ token.currency.name, (evmChains as any)[tx.chainId]?.name || 'Centrifuge', - tx.evmAddress || utils.formatAddress(tx.accountId), + utils.formatAddress(tx.evmAddress || tx.accountId), tx.epochNumber ? tx.epochNumber.toString() : '-', - formatDate(tx.timestamp.toString()), + tx.timestamp.toISOString(), formatInvestorTransactionsType({ type: tx.type, trancheTokenSymbol: token.currency.symbol, poolCurrencySymbol: pool.currency.symbol, currencyAmount: tx.currencyAmount ? tx.currencyAmount?.toNumber() : null, }), - tx.currencyAmount ? formatBalance(tx.currencyAmount.toDecimal(), pool.currency) : '-', - tx.tokenAmount ? formatBalance(tx.tokenAmount.toDecimal(), pool.tranches[0].currency) : '-', // TODO: not hardcode to 0 - tx.tokenPrice ? formatBalance(tx.tokenPrice.toDecimal(), pool.currency.symbol, 6) : '-', + tx.currencyAmount?.toFloat() ?? '-', + pool.currency.symbol, + tx.tokenAmount?.toFloat() ?? '-', + token.currency.symbol, + tx.tokenPrice?.toFloat() ?? '-', + pool.currency.symbol, + tx.hash, ], heading: false, } }) - }, [transactions, pool.currency, pool.tranches, investorTxType]) + .filter((row) => { + if (!address) return true + return isAddress(address) && isSameAddress(address, row.value[2]) + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [transactions, pool.currency, pool.tranches, txType, address, network]) - const dataUrl = React.useMemo(() => { + React.useEffect(() => { if (!data.length) { return } - const formatted = data - .map(({ value }) => value as string[]) - .map((values) => Object.fromEntries(headers.map((_, index) => [headers[index], `"${values[index]}"`]))) - - return getCSVDownloadUrl(formatted) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [data]) - - React.useEffect(() => { - setCsvData( - dataUrl - ? { - dataUrl, - fileName: `${pool.id}-investor-transactions-${startDate}-${endDate}.csv`, - } - : undefined + const formatted = data.map(({ value: values }) => + Object.fromEntries(columnConfig.map((col, index) => [col.header, `"${values[index]}"`])) ) + const dataUrl = getCSVDownloadUrl(formatted) + + setCsvData({ + dataUrl, + fileName: `${pool.id}-investor-transactions-${formatDate(startDate, { + weekday: 'short', + month: 'short', + day: '2-digit', + year: 'numeric', + }).replaceAll(',', '')}-${formatDate(endDate, { + weekday: 'short', + month: 'short', + day: '2-digit', + year: 'numeric', + }).replaceAll(',', '')}.csv`, + }) - return () => setCsvData(undefined) + return () => { + setCsvData(undefined) + URL.revokeObjectURL(dataUrl) + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [dataUrl, pool.id, startDate, endDate]) + }, [data]) if (!transactions) { return diff --git a/centrifuge-app/src/components/Report/PoolBalance.tsx b/centrifuge-app/src/components/Report/PoolBalance.tsx index 90555ae040..e90615b11d 100644 --- a/centrifuge-app/src/components/Report/PoolBalance.tsx +++ b/centrifuge-app/src/components/Report/PoolBalance.tsx @@ -1,9 +1,8 @@ -import { CurrencyBalance } from '@centrifuge/centrifuge-js' import { Pool } from '@centrifuge/centrifuge-js/dist/modules/pools' import { Text } from '@centrifuge/fabric' import * as React from 'react' -import { Dec } from '../../utils/Decimal' -import { formatBalanceAbbreviated } from '../../utils/formatting' +import { formatDate } from '../../utils/date' +import { formatBalance, formatPercentage } from '../../utils/formatting' import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl' import { useDailyPoolStates, useMonthlyPoolStates } from '../../utils/usePools' import { DataTable } from '../DataTable' @@ -13,11 +12,21 @@ import { ReportContext } from './ReportContext' import { UserFeedback } from './UserFeedback' import type { TableDataRow } from './index' +type Row = TableDataRow & { + formatter?: (v: any) => any +} + export function PoolBalance({ pool }: { pool: Pool }) { const { startDate, endDate, groupBy, setCsvData } = React.useContext(ReportContext) - const { poolStates: dailyPoolStates } = useDailyPoolStates(pool.id, startDate, endDate) || {} - const monthlyPoolStates = useMonthlyPoolStates(pool.id, startDate, endDate) + const { poolStates: dailyPoolStates } = + useDailyPoolStates(pool.id, startDate ? new Date(startDate) : undefined, endDate ? new Date(endDate) : undefined) || + {} + const monthlyPoolStates = useMonthlyPoolStates( + pool.id, + startDate ? new Date(startDate) : undefined, + endDate ? new Date(endDate) : undefined + ) const poolStates = groupBy === 'day' ? dailyPoolStates : monthlyPoolStates const columns = React.useMemo(() => { @@ -29,22 +38,31 @@ export function PoolBalance({ pool }: { pool: Pool }) { { align: 'left', header: '', - cell: (row: TableDataRow) => {row.name}, + cell: (row: Row) => {row.name}, width: '200px', }, ] .concat( poolStates.map((state, index) => ({ align: 'right', - header: `${new Date(state.timestamp).toLocaleDateString('en-US', { - month: 'short', - })} ${ + timestamp: state.timestamp, + header: groupBy === 'day' - ? new Date(state.timestamp).toLocaleDateString('en-US', { day: 'numeric' }) - : new Date(state.timestamp).toLocaleDateString('en-US', { year: 'numeric' }) - }`, - cell: (row: TableDataRow) => {(row.value as any)[index]}, - width: '120px', + ? new Date(state.timestamp).toLocaleDateString('en-US', { + day: 'numeric', + month: 'short', + year: 'numeric', + }) + : new Date(state.timestamp).toLocaleDateString('en-US', { year: 'numeric', month: 'short' }), + cell: (row: Row) => ( + + {(row.value as any)[index] !== '' && + (row.formatter + ? row.formatter((row.value as any)[index]) + : formatBalance((row.value as any)[index], pool.currency.symbol, 5))} + + ), + width: '200px', })) ) .concat({ @@ -53,65 +71,46 @@ export function PoolBalance({ pool }: { pool: Pool }) { cell: () => , width: '1fr', }) - }, [poolStates, groupBy]) + }, [poolStates, groupBy, pool]) - const overviewRecords: TableDataRow[] = React.useMemo(() => { + const overviewRecords: Row[] = React.useMemo(() => { return [ { name: 'NAV', - value: poolStates?.map((state) => formatBalanceAbbreviated(state.poolValue, pool.currency.symbol)) || [], + value: poolStates?.map((state) => state.poolValue.toFloat()) || [], heading: false, }, { - name: 'Asset value', + name: 'NAV change', value: - poolStates?.map((state) => - formatBalanceAbbreviated( - new CurrencyBalance(state.poolValue.sub(state.poolState.totalReserve), pool.currency.decimals), - pool.currency.symbol - ) - ) || [], + poolStates?.map((state, i) => { + if (i === 0) return '' + const prev = poolStates[i - 1].poolValue.toFloat() + const cur = state.poolValue.toFloat() + const change = (cur / prev - 1) * 100 + return change < 0 ? change : `+${change}` + }) || [], heading: false, + formatter: (v: any) => `${v < 0 ? '' : '+'}${formatPercentage(v, true, {}, 5)}`, }, { - name: 'Reserve', - value: - poolStates?.map((state) => formatBalanceAbbreviated(state.poolState.totalReserve, pool.currency.symbol)) || - [], + name: 'Asset value', + value: poolStates?.map((state) => state.poolValue.toFloat() - state.poolState.totalReserve.toFloat()) || [], heading: false, }, - ] - }, [poolStates, pool.currency.symbol]) - - const priceRecords: TableDataRow[] = React.useMemo(() => { - return [ { - name: 'Token price', - value: poolStates?.map(() => '') || [], + name: 'Onchain reserve', + value: poolStates?.map((state) => state.poolState.totalReserve.toFloat()) || [], heading: false, }, - ].concat( - pool?.tranches - .slice() - .reverse() - .map((token) => ({ - name: `\u00A0 \u00A0 ${token.currency.name.split(' ').at(-1)} tranche`, - value: - poolStates?.map((state) => - state.tranches[token.id]?.price - ? formatBalanceAbbreviated(state.tranches[token.id].price?.toFloat()!, pool.currency.symbol, 5) - : '1.000' - ) || [], - heading: false, - })) || [] - ) - }, [poolStates, pool.currency.symbol, pool?.tranches]) + ] + }, [poolStates]) - const inOutFlowRecords: TableDataRow[] = React.useMemo(() => { + const inOutFlowRecords: Row[] = React.useMemo(() => { return [ { name: 'Investments', - value: poolStates?.map(() => '') || [], + value: poolStates?.map(() => '' as any) || [], heading: false, }, ].concat( @@ -120,19 +119,13 @@ export function PoolBalance({ pool }: { pool: Pool }) { .reverse() .map((token) => ({ name: `\u00A0 \u00A0 ${token.currency.name.split(' ').at(-1)} tranche`, - value: - poolStates?.map((state) => - formatBalanceAbbreviated( - state.tranches[token.id]?.fulfilledInvestOrders.toDecimal() ?? Dec(0), - pool.currency.symbol - ) - ) || [], + value: poolStates?.map((state) => state.tranches[token.id]?.fulfilledInvestOrders.toFloat() ?? 0) || [], heading: false, })) || [], [ { name: 'Redemptions', - value: poolStates?.map(() => '') || [], + value: poolStates?.map(() => '' as any) || [], heading: false, }, ].concat( @@ -141,39 +134,48 @@ export function PoolBalance({ pool }: { pool: Pool }) { .reverse() .map((token) => ({ name: `\u00A0 \u00A0 ${token.currency.name.split(' ').at(-1)} tranche`, - value: - poolStates?.map((state) => - formatBalanceAbbreviated( - state.tranches[token.id]?.fulfilledRedeemOrders.toDecimal() ?? Dec(0), - pool.currency.symbol - ) - ) || [], + value: poolStates?.map((state) => state.tranches[token.id]?.fulfilledRedeemOrders ?? 0) || [], heading: false, })) || [] ) ) - }, [poolStates, pool.currency.symbol, pool?.tranches]) + }, [poolStates, pool?.tranches]) + + const headers = columns.slice(0, -1).map(({ header }) => header) - const headers = columns.map(({ header }) => header) + React.useEffect(() => { + const f = [...overviewRecords, ...inOutFlowRecords].map(({ name, value }) => [name.trim(), ...(value as string[])]) + let formatted = f.map((values) => + Object.fromEntries(headers.map((_, index) => [`"${headers[index]}"`, `"${values[index]}"`])) + ) - const dataUrl = React.useMemo(() => { - const formatted = [...overviewRecords, ...priceRecords, ...inOutFlowRecords] - .map(({ name, value }) => [name, ...(value as string[])]) - .map((values) => Object.fromEntries(headers.map((_, index) => [headers[index], `"${values[index]}"`]))) + if (!formatted.length) { + return + } - return getCSVDownloadUrl(formatted) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [overviewRecords, priceRecords, inOutFlowRecords]) + const dataUrl = getCSVDownloadUrl(formatted) - React.useEffect(() => { setCsvData({ dataUrl, - fileName: `${pool.id}-pool-balance-${startDate}-${endDate}.csv`, + fileName: `${pool.id}-pool-balance-${formatDate(startDate, { + weekday: 'short', + month: 'short', + day: '2-digit', + year: 'numeric', + }).replaceAll(',', '')}-${formatDate(endDate, { + weekday: 'short', + month: 'short', + day: '2-digit', + year: 'numeric', + }).replaceAll(',', '')}.csv`, }) - return () => setCsvData(undefined) + return () => { + setCsvData(undefined) + URL.revokeObjectURL(dataUrl) + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [dataUrl, pool.id, startDate, endDate]) + }, [overviewRecords, inOutFlowRecords]) if (!poolStates) { return @@ -182,7 +184,6 @@ export function PoolBalance({ pool }: { pool: Pool }) { return poolStates?.length > 0 ? ( - ) : ( diff --git a/centrifuge-app/src/components/Report/ReportContext.tsx b/centrifuge-app/src/components/Report/ReportContext.tsx index a417b34d35..3ffc5a70b9 100644 --- a/centrifuge-app/src/components/Report/ReportContext.tsx +++ b/centrifuge-app/src/components/Report/ReportContext.tsx @@ -1,24 +1,18 @@ -import { RangeOptionValue } from '@centrifuge/fabric' import * as React from 'react' export type GroupBy = 'day' | 'month' -export type Report = 'pool-balance' | 'asset-list' | 'investor-tx' | 'asset-tx' | 'fee-tx' | 'holders' - -export type InvestorTxType = 'all' | 'orders' | 'executions' | 'transfers' +export type Report = 'pool-balance' | 'token-price' | 'asset-list' | 'investor-tx' | 'asset-tx' | 'fee-tx' | 'holders' export type ReportContextType = { csvData?: CsvDataProps setCsvData: (data?: CsvDataProps) => void - startDate: Date - setStartDate: (date: Date) => void - - endDate: Date - setEndDate: (date: Date) => void + startDate: string + setStartDate: (date: string) => void - range: RangeOptionValue - setRange: (range: RangeOptionValue) => void + endDate: string + setEndDate: (date: string) => void report: Report setReport: (report: Report) => void @@ -26,11 +20,23 @@ export type ReportContextType = { groupBy: GroupBy setGroupBy: (groupBy: GroupBy) => void + loanStatus: string + setLoanStatus: (status: string) => void + + txType: string + setTxType: (type: string) => void + activeTranche?: string setActiveTranche: (tranche: string) => void - investorTxType: InvestorTxType - setInvestorTxType: (investorTxType: InvestorTxType) => void + address: string + setAddress: (type: string) => void + + network: string | number + setNetwork: (type: string | number) => void + + loan: string + setLoan: (type: string) => void } export type CsvDataProps = { @@ -38,47 +44,26 @@ export type CsvDataProps = { fileName: string } -const defaultContext = { - csvData: undefined, - setCsvData() {}, - - startDate: new Date(), - setStartDate() {}, - - endDate: new Date(), - setEndDate() {}, - - range: 'last-month' as RangeOptionValue, - setRange() {}, - - report: 'investor-tx' as Report, - setReport() {}, - - groupBy: 'month' as GroupBy, - setGroupBy() {}, - - activeTranche: 'all', - setActiveTranche() {}, - - investorTxType: 'all' as InvestorTxType, - setInvestorTxType() {}, -} - -export const ReportContext = React.createContext(defaultContext) +export const ReportContext = React.createContext({} as any) export function ReportContextProvider({ children }: { children: React.ReactNode }) { const [csvData, setCsvData] = React.useState(undefined) // Global filters - const [startDate, setStartDate] = React.useState(defaultContext.startDate) - const [endDate, setEndDate] = React.useState(defaultContext.endDate) - const [report, setReport] = React.useState(defaultContext.report) - const [range, setRange] = React.useState(defaultContext.range) + const [startDate, setStartDate] = React.useState( + new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10) + ) + const [endDate, setEndDate] = React.useState(new Date().toISOString().slice(0, 10)) + const [report, setReport] = React.useState('investor-tx') // Custom filters for specific reports - const [groupBy, setGroupBy] = React.useState(defaultContext.groupBy) - const [activeTranche, setActiveTranche] = React.useState(defaultContext.activeTranche) - const [investorTxType, setInvestorTxType] = React.useState(defaultContext.investorTxType) + const [loanStatus, setLoanStatus] = React.useState('all') + const [groupBy, setGroupBy] = React.useState('month') + const [activeTranche, setActiveTranche] = React.useState('all') + const [txType, setTxType] = React.useState('all') + const [address, setAddress] = React.useState('') + const [network, setNetwork] = React.useState('all') + const [loan, setLoan] = React.useState('all') return ( {children} diff --git a/centrifuge-app/src/components/Report/ReportFilter.tsx b/centrifuge-app/src/components/Report/ReportFilter.tsx index a62be4d356..b0ecda19e1 100644 --- a/centrifuge-app/src/components/Report/ReportFilter.tsx +++ b/centrifuge-app/src/components/Report/ReportFilter.tsx @@ -1,8 +1,15 @@ -import { Pool } from '@centrifuge/centrifuge-js' -import { AnchorButton, Box, DateRange, Select, Shelf } from '@centrifuge/fabric' +import { Loan, Pool } from '@centrifuge/centrifuge-js' +import { useGetNetworkName } from '@centrifuge/centrifuge-react' +import { AnchorButton, Box, DateInput, SearchInput, Select, Shelf } from '@centrifuge/fabric' import * as React from 'react' +import { nftMetadataSchema } from '../../schemas' +import { useActiveDomains } from '../../utils/useLiquidityPools' +import { useLoans } from '../../utils/useLoans' +import { useMetadata } from '../../utils/useMetadata' +import { useCentNFT } from '../../utils/useNFTs' import { useDebugFlags } from '../DebugFlags' -import { GroupBy, InvestorTxType, Report, ReportContext } from './ReportContext' +import { GroupBy, Report, ReportContext } from './ReportContext' +import { formatPoolFeeTransactionType } from './utils' type ReportFilterProps = { pool: Pool @@ -14,27 +21,38 @@ export function ReportFilter({ pool }: ReportFilterProps) { const { csvData, setStartDate, + startDate, endDate, setEndDate, - range, - setRange, report, setReport, + loanStatus, + setLoanStatus, + txType, + setTxType, groupBy, setGroupBy, activeTranche, setActiveTranche, - investorTxType, - setInvestorTxType, + address, + setAddress, + network, + setNetwork, + loan, + setLoan, } = React.useContext(ReportContext) + const { data: domains } = useActiveDomains(pool.id) + const getNetworkName = useGetNetworkName() + const loans = useLoans(pool.id) as Loan[] | undefined const reportOptions: { label: string; value: Report }[] = [ { label: 'Investor transactions', value: 'investor-tx' }, { label: 'Asset transactions', value: 'asset-tx' }, { label: 'Fee transactions', value: 'fee-tx' }, { label: 'Pool balance', value: 'pool-balance' }, + { label: 'Token price', value: 'token-price' }, { label: 'Asset list', value: 'asset-list' }, - ...(holdersReport == true ? [{ label: 'Holders', value: 'holders' as Report }] : []), + ...(holdersReport === true ? [{ label: 'Holders', value: 'holders' as Report }] : []), ] return ( @@ -46,12 +64,11 @@ export function ReportFilter({ pool }: ReportFilterProps) { borderWidth={0} borderBottomWidth={1} borderStyle="solid" - borderColor="borderSecondary" + borderColor="borderPrimary" > { @@ -100,11 +108,39 @@ export function ReportFilter({ pool }: ReportFilterProps) { /> )} + {report === 'asset-list' && ( + )} - - {report === 'investor-tx' && ( + {report === 'asset-tx' && ( { if (event.target.value) { - setInvestorTxType(event.target.value as InvestorTxType) + setTxType(event.target.value) } }} /> )} + {['investor-tx', 'holders'].includes(report) && ( + <> + + form.setFieldValue(`poolFees.${index}.type`, event.target.value) + } + onBlur={field.onBlur} disabled={!poolAdmin || updateFeeTxLoading} - errorMessage={(meta.touched && meta.error) || ''} + errorMessage={meta.touched && meta.error ? meta.error : undefined} + value={field.value} + options={[ + { label: 'Fixed', value: 'fixed' }, + { label: 'Direct charge', value: 'chargedUpTo' }, + ]} /> - ) - }} - - - {({ field, meta }: FieldProps) => { - return ( - - ) - }} - + )} + + + + + {({ field, meta }: FieldProps) => { + return ( + + ) + }} + + - + {({ field, meta }: FieldProps) => { return ( { }} - + + + ) })} @@ -273,7 +294,13 @@ export const EditFeesDrawer = ({ onClose, isOpen }: ChargeFeesProps) => { variant="tertiary" disabled={!poolAdmin || updateFeeTxLoading} onClick={() => - push({ feeName: '', percentOfNav: undefined, receivingAddress: '', feeId: undefined }) + push({ + feeName: '', + percentOfNav: '', + receivingAddress: '', + feeId: undefined, + type: 'chargedUpTo', + }) } > Add new fee diff --git a/centrifuge-app/src/components/PoolFees/index.tsx b/centrifuge-app/src/components/PoolFees/index.tsx index 08c741a92c..66198312d7 100644 --- a/centrifuge-app/src/components/PoolFees/index.tsx +++ b/centrifuge-app/src/components/PoolFees/index.tsx @@ -7,7 +7,7 @@ import { useHistory, useLocation, useParams } from 'react-router' import { CopyToClipboard } from '../../utils/copyToClipboard' import { formatBalance, formatPercentage } from '../../utils/formatting' import { usePoolAdmin } from '../../utils/usePermissions' -import { usePool, usePoolMetadata } from '../../utils/usePools' +import { usePool, usePoolFees, usePoolMetadata } from '../../utils/usePools' import { DataTable } from '../DataTable' import { PageSection } from '../PageSection' import { PageSummary } from '../PageSummary' @@ -50,6 +50,7 @@ type PoolFeeChange = { export function PoolFees() { const { pid: poolId } = useParams<{ pid: string }>() const pool = usePool(poolId) + const poolFees = usePoolFees(poolId) const { data: poolMetadata } = usePoolMetadata(pool) const { search, pathname } = useLocation() const { push } = useHistory() @@ -140,7 +141,7 @@ export function PoolFees() { ) }, }, - ...(!!poolAdmin || pool?.poolFees?.map((fee) => addressToHex(fee.destination)).includes(address! as `0x${string}`) + ...(!!poolAdmin || poolFees?.map((fee) => addressToHex(fee.destination)).includes(address! as `0x${string}`) ? [ { align: 'left', @@ -153,7 +154,7 @@ export function PoolFees() { const data = React.useMemo(() => { const activeFees = - pool.poolFees + poolFees ?.filter((feeChainData) => poolMetadata?.pool?.poolFees?.find((f) => f.id === feeChainData.id)) ?.map((feeChainData, index) => { const feeMetadata = poolMetadata?.pool?.poolFees?.find((f) => f.id === feeChainData.id) @@ -223,7 +224,8 @@ export function PoolFees() { } return activeFees - }, [poolMetadata, pool, poolId, changes, applyNewFee]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [poolMetadata, pool, poolId, changes, address, poolFees, poolAdmin]) React.useEffect(() => { if (drawer === 'edit') { @@ -238,7 +240,7 @@ export function PoolFees() { label: , value: formatBalance( new CurrencyBalance( - pool.poolFees?.reduce((acc, fee) => acc.add(fee.amounts.pending), new BN(0)) || new BN(0), + poolFees?.reduce((acc, fee) => acc.add(fee.amounts.pending), new BN(0)) || new BN(0), pool.currency.decimals ) || 0, pool.currency.symbol, diff --git a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx index d870c06220..70e52361c6 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx @@ -451,14 +451,13 @@ function CreatePoolForm() { amount: Rate.fromPercent(fee.percentOfNav), feeType: fee.feeType, limit: 'ShareOfPortfolioValuation', - feeId: feeId + i, account: fee.feeType === 'chargedUpTo' ? fee.walletAddress : undefined, feePosition: fee.feePosition, } }) - metadataValues.poolFees = poolFees.map((fee) => ({ + metadataValues.poolFees = poolFees.map((fee, i) => ({ name: fee.name, - id: fee.feeId, + id: feeId + i, feePosition: fee.feePosition, feeType: fee.feeType, })) diff --git a/centrifuge-app/src/pages/Pool/Overview/index.tsx b/centrifuge-app/src/pages/Pool/Overview/index.tsx index 838d92e9da..ced4c4c50c 100644 --- a/centrifuge-app/src/pages/Pool/Overview/index.tsx +++ b/centrifuge-app/src/pages/Pool/Overview/index.tsx @@ -25,7 +25,7 @@ import { useAverageMaturity } from '../../../utils/useAverageMaturity' import { useConnectBeforeAction } from '../../../utils/useConnectBeforeAction' import { useIsAboveBreakpoint } from '../../../utils/useIsAboveBreakpoint' import { useLoans } from '../../../utils/useLoans' -import { usePool, usePoolMetadata } from '../../../utils/usePools' +import { usePool, usePoolFees, usePoolMetadata } from '../../../utils/usePools' import { PoolDetailHeader } from '../Header' export type Token = { @@ -63,6 +63,7 @@ export function PoolDetailOverview() { const { pid: poolId } = useParams<{ pid: string }>() const isTinlakePool = poolId.startsWith('0x') const pool = usePool(poolId) + const poolFees = usePoolFees(poolId) const { data: metadata, isLoading: metadataIsLoading } = usePoolMetadata(pool) const averageMaturity = useAverageMaturity(poolId) const loans = useLoans(poolId) @@ -144,7 +145,7 @@ export function PoolDetailOverview() { poolFees={ metadata?.pool?.poolFees?.map((fee) => { return { - fee: pool.poolFees?.find((f) => f.id === fee.id)?.amounts.percentOfNav ?? Rate.fromFloat(0), + fee: poolFees?.find((f) => f.id === fee.id)?.amounts.percentOfNav ?? Rate.fromFloat(0), name: fee.name, id: fee.id, } diff --git a/centrifuge-app/src/utils/usePools.ts b/centrifuge-app/src/utils/usePools.ts index 0b14e1866a..fd230a7097 100644 --- a/centrifuge-app/src/utils/usePools.ts +++ b/centrifuge-app/src/utils/usePools.ts @@ -92,6 +92,14 @@ export function useAssetTransactions(poolId: string, from?: Date, to?: Date) { return result } +export function usePoolFees(poolId: string) { + const [result] = useCentrifugeQuery(['poolFees', poolId], (cent) => cent.pools.getPoolFees([poolId]), { + enabled: !poolId.startsWith('0x'), + }) + + return result +} + export function useFeeTransactions(poolId: string, from?: Date, to?: Date) { const [result] = useCentrifugeQuery( ['feeTransactions', poolId, from, to], diff --git a/centrifuge-js/src/CentrifugeBase.ts b/centrifuge-js/src/CentrifugeBase.ts index 46c28f1dae..d0af496be1 100644 --- a/centrifuge-js/src/CentrifugeBase.ts +++ b/centrifuge-js/src/CentrifugeBase.ts @@ -110,8 +110,8 @@ const parachainTypes = { }, PoolFeesList: 'Vec', PoolFeesOfBucket: { - bucket: 'PoolFeeBucket', - fees: 'Vec', + bucket: 'CfgTraitsFeePoolFeeBucket', + fees: 'Vec', }, PriceCollectionInput: { _enum: ['Empty', 'Custom(BoundedBTreeMap)', 'FromRegistry'], diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index 7a9d0f7fef..3a655f14d8 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -37,7 +37,6 @@ const PerquintillBN = new BN(10).pow(new BN(18)) const PriceBN = new BN(10).pow(new BN(18)) const MaxU128 = '340282366920938463463374607431768211455' const SEC_PER_DAY = 24 * 60 * 60 -const SEC_PER_YEAR = SEC_PER_DAY * 365 type AdminRole = | 'PoolAdmin' @@ -336,7 +335,6 @@ export type Pool = { value: CurrencyBalance createdAt: string | null tranches: Token[] - poolFees: ActivePoolFees[] | null reserve: { max: CurrencyBalance available: CurrencyBalance @@ -573,12 +571,13 @@ export type DailyPoolState = { poolValue: CurrencyBalance timestamp: string tranches: { [trancheId: string]: DailyTrancheState } - - sumBorrowedAmountByPeriod?: string | null - sumInterestRepaidAmountByPeriod?: string | null - sumRepaidAmountByPeriod?: string | null - sumInvestedAmountByPeriod?: string | null - sumRedeemedAmountByPeriod?: string | null + sumChargedAmountByPeriod: string | null + sumAccruedAmountByPeriod: string | null + sumBorrowedAmountByPeriod: string + sumInterestRepaidAmountByPeriod: string + sumRepaidAmountByPeriod: string + sumInvestedAmountByPeriod: string + sumRedeemedAmountByPeriod: string blockNumber: number } @@ -840,12 +839,18 @@ export type ActivePoolFees = { } limit: FeeLimits destination: string + editor: + | { + root: null + } + | { account: string } id: number + position: 'Top' } export type ActivePoolFeesData = { amounts: { - disbursement: CurrencyBalance + disbursement: string feeType: { [K in FeeTypes]: { limit: { @@ -856,12 +861,14 @@ export type ActivePoolFeesData = { payable: { upTo: string } - pending: CurrencyBalance + pending: string } destination: string - editor: { - root: { account: string } | null - } + editor: + | { + root: null + } + | { account: string } id: number } @@ -871,7 +878,6 @@ export type AddFee = { feeType: FeeTypes limit: 'ShareOfPortfolioValuation' | 'AmountPerSecond' name: string - feeId: number amount: Rate account?: string feePosition: 'Top of waterfall' @@ -1820,9 +1826,7 @@ export function getPoolsModule(inst: Centrifuge) { api.events.poolSystem.EpochExecuted.is(event) || api.events.poolSystem.SolutionSubmitted.is(event) || api.events.investments.InvestOrderUpdated.is(event) || - api.events.investments.RedeemOrderUpdated.is(event) || - api.events.poolFees.Charged.is(event) || - api.events.poolFees.Added.is(event) + api.events.investments.RedeemOrderUpdated.is(event) ) return !!event }) @@ -1850,254 +1854,221 @@ export function getPoolsModule(inst: Centrifuge) { api.query.loans.portfolioValuation.entries(), api.query.poolSystem.epochExecution.entries(), getCurrencies(), - api.query.poolFees.activeFees.entries(), ]), - (api, [rawPools, rawMetadatas, rawPortfolioValuation, rawEpochExecutions, currencies, activePoolFees]) => ({ + (api, [rawPools, rawMetadatas, rawPortfolioValuation, rawEpochExecutions, currencies]) => ({ api, rawPools, rawMetadatas, rawPortfolioValuation, rawEpochExecutions, currencies, - activePoolFees, }) ), - switchMap( - ({ api, rawPools, rawMetadatas, rawPortfolioValuation, rawEpochExecutions, currencies, activePoolFees }) => { - if (!rawPools.length) return of([]) - - const poolFeesMap = activePoolFees.reduce((acc, [key, fees]) => { - const poolId = formatPoolKey(key as StorageKey<[u32]>) - acc[poolId] = fees.toJSON() as unknown as ActivePoolFeesData[] - return acc - }, {} as Record) - - const portfolioValuationMap = rawPortfolioValuation.reduce((acc, [key, navValue]) => { - const poolId = formatPoolKey(key as StorageKey<[u32]>) - const nav = navValue.toJSON() as unknown as NAVDetailsData - acc[poolId] = { - lastUpdated: nav ? nav.lastUpdated : 0, - } - return acc - }, {} as Record) - - const epochExecutionMap = rawEpochExecutions.reduce((acc, [key, navValue]) => { - const poolId = formatPoolKey(key as StorageKey<[u32]>) - const epoch = navValue.toJSON() as EpochExecutionData - acc[poolId] = { - epoch: epoch.epoch, - challengePeriodEnd: epoch.challengePeriodEnd, - } - return acc - }, {} as Record>) - - const metadataMap = rawMetadatas.reduce((acc, [key, metadataValue]) => { - const poolId = formatPoolKey(key as StorageKey<[u32]>) - const metadata = (metadataValue.toHuman() as { metadata: string }).metadata - acc[poolId] = metadata - return acc - }, {} as Record) - - // read pools, poolIds and currencies from observable - const pools = rawPools.map(([poolKeys, poolValue]) => { - const data = poolValue.toJSON() as PoolDetailsData - const { currency } = poolValue.toHuman() as any - data.currency = parseCurrencyKey(currency) - return { - id: formatPoolKey(poolKeys as any), // poolId - data, // pool data - } + switchMap(({ api, rawPools, rawMetadatas, rawPortfolioValuation, rawEpochExecutions, currencies }) => { + if (!rawPools.length) return of([]) + + const portfolioValuationMap = rawPortfolioValuation.reduce((acc, [key, navValue]) => { + const poolId = formatPoolKey(key as StorageKey<[u32]>) + const nav = navValue.toJSON() as unknown as NAVDetailsData + acc[poolId] = { + lastUpdated: nav ? nav.lastUpdated : 0, + } + return acc + }, {} as Record) + + const epochExecutionMap = rawEpochExecutions.reduce((acc, [key, navValue]) => { + const poolId = formatPoolKey(key as StorageKey<[u32]>) + const epoch = navValue.toJSON() as EpochExecutionData + acc[poolId] = { + epoch: epoch.epoch, + challengePeriodEnd: epoch.challengePeriodEnd, + } + return acc + }, {} as Record>) + + const metadataMap = rawMetadatas.reduce((acc, [key, metadataValue]) => { + const poolId = formatPoolKey(key as StorageKey<[u32]>) + const metadata = (metadataValue.toHuman() as { metadata: string }).metadata + acc[poolId] = metadata + return acc + }, {} as Record) + + // read pools, poolIds and currencies from observable + const pools = rawPools.map(([poolKeys, poolValue]) => { + const data = poolValue.toJSON() as PoolDetailsData + const { currency } = poolValue.toHuman() as any + data.currency = parseCurrencyKey(currency) + return { + id: formatPoolKey(poolKeys as any), // poolId + data, // pool data + } + }) + + const keys = pools + .map(({ id, data }) => { + return data.tranches.ids.map((tid) => [id, tid, data.epoch.lastExecuted] as const) }) + .flat() - const keys = pools - .map(({ id, data }) => { - return data.tranches.ids.map((tid) => [id, tid, data.epoch.lastExecuted] as const) - }) - .flat() + const $yield30DaysAnnualized = combineLatest( + keys.map((key) => { + const poolIdTrancheId = `${key[0]}-${key[1].toLowerCase()}` + return getLatestTrancheSnapshot(poolIdTrancheId).pipe(map((snapshot) => snapshot?.trancheSnapshots.nodes)) + }) + ) as Observable<{ yield30DaysAnnualized: string | null; trancheId: string }[][]> - const $yield30DaysAnnualized = combineLatest( - keys.map((key) => { - const poolIdTrancheId = `${key[0]}-${key[1].toLowerCase()}` - return getLatestTrancheSnapshot(poolIdTrancheId).pipe(map((snapshot) => snapshot?.trancheSnapshots.nodes)) - }) - ) as Observable<{ yield30DaysAnnualized: string | null; trancheId: string }[][]> + const trancheIdToIndex: Record = {} + keys.forEach(([, tid], i) => { + trancheIdToIndex[tid] = i + }) - const trancheIdToIndex: Record = {} - keys.forEach(([, tid], i) => { - trancheIdToIndex[tid] = i - }) + // modify keys for $issuance query [Tranche: [poolId, trancheId]] + const issuanceKeys = keys.map(([poolId, trancheId]) => ({ Tranche: [poolId, trancheId] })) + const $issuance = api.query.ormlTokens.totalIssuance.multi(issuanceKeys).pipe(take(1)) - // modify keys for $issuance query [Tranche: [poolId, trancheId]] - const issuanceKeys = keys.map(([poolId, trancheId]) => ({ Tranche: [poolId, trancheId] })) - const $issuance = api.query.ormlTokens.totalIssuance.multi(issuanceKeys).pipe(take(1)) + const $prices = combineLatest( + pools.map((p) => api.call.poolsApi.trancheTokenPrices(p.id).pipe(startWith(null))) as Observable< + Codec[] | null + >[] + ) - const $prices = combineLatest( - pools.map((p) => api.call.poolsApi.trancheTokenPrices(p.id).pipe(startWith(null))) as Observable< - Codec[] | null - >[] - ) + const $navs = combineLatest(pools.map((p) => api.call.poolsApi.nav(p.id)) as Observable[]) - const $navs = combineLatest(pools.map((p) => api.call.poolsApi.nav(p.id)) as Observable[]) + const $block = inst.getBlocks().pipe(take(1)) - const $block = inst.getBlocks().pipe(take(1)) + return combineLatest([$issuance, $block, $prices, $navs, $yield30DaysAnnualized]).pipe( + map(([rawIssuances, { block }, rawPrices, rawNavs, [rawYield30DaysAnnualized]]) => { + const blockNumber = block.header.number.toNumber() - return combineLatest([$issuance, $block, $prices, $navs, $yield30DaysAnnualized]).pipe( - map(([rawIssuances, { block }, rawPrices, rawNavs, [rawYield30DaysAnnualized]]) => { - const blockNumber = block.header.number.toNumber() + const yield30DaysAnnualizedByPoolIdTrancheId = rawYield30DaysAnnualized?.reduce( + (acc, { yield30DaysAnnualized, trancheId }) => { + acc[trancheId] = yield30DaysAnnualized + return acc + }, + {} as Record + ) - const yield30DaysAnnualizedByPoolIdTrancheId = rawYield30DaysAnnualized?.reduce( - (acc, { yield30DaysAnnualized, trancheId }) => { - acc[trancheId] = yield30DaysAnnualized - return acc - }, - {} as Record + const mappedPools = pools.map((poolObj, poolIndex) => { + const { data: pool, id: poolId } = poolObj + const metadata = metadataMap[poolId] + const portfolioValuationData = portfolioValuationMap[poolId] + const epochExecution = epochExecutionMap[poolId] + const currency = findCurrency(currencies, pool.currency)! + + const poolValue = new CurrencyBalance( + pool.tranches.tranches.reduce((prev: BN, tranche: TrancheDetailsData) => { + return new BN(prev.add(new BN(hexToBN(tranche.debt))).add(new BN(hexToBN(tranche.reserve)))) + }, new BN(0)), + currency.decimals ) - const mappedPools = pools.map((poolObj, poolIndex) => { - const { data: pool, id: poolId } = poolObj - const poolFees = poolFeesMap[poolId] - const metadata = metadataMap[poolId] - const portfolioValuationData = portfolioValuationMap[poolId] - const epochExecution = epochExecutionMap[poolId] - const currency = findCurrency(currencies, pool.currency)! - - const poolValue = new CurrencyBalance( - pool.tranches.tranches.reduce((prev: BN, tranche: TrancheDetailsData) => { - return new BN(prev.add(new BN(hexToBN(tranche.debt))).add(new BN(hexToBN(tranche.reserve)))) - }, new BN(0)), - currency.decimals - ) - - const maxReserve = new CurrencyBalance(hexToBN(pool.reserve.max), currency.decimals) - const availableReserve = new CurrencyBalance(hexToBN(pool.reserve.available), currency.decimals) - const totalReserve = new CurrencyBalance(hexToBN(pool.reserve.total), currency.decimals) - - const lastUpdatedNav = new Date((portfolioValuationData?.lastUpdated ?? 0) * 1000).toISOString() - // @ts-expect-error - const rawNav = rawNavs && rawNavs[poolIndex]?.toJSON() - - const mappedPool: Pool = { - id: poolId, - createdAt: null, - metadata, - currency, - poolFees: poolFees?.map((fee) => { - const type = Object.keys(fee.amounts.feeType)[0] as FeeTypes - const limit = Object.keys(fee.amounts.feeType[type].limit)[0] as FeeLimits - const percentOfNav = new Rate(hexToBN(fee.amounts.feeType[type].limit[limit])) - return { - ...fee, - type, - limit, - amounts: { - percentOfNav, - pending: - type === 'chargedUpTo' - ? new CurrencyBalance(fee.amounts.pending, currency.decimals) - : new CurrencyBalance( - new CurrencyBalance(fee.amounts.payable.upTo, currency.decimals) - .divn(limit === 'amountPerSecond' ? 1 : SEC_PER_YEAR) - .add(new BN(fee.amounts.pending)), - currency.decimals - ), - }, - } - }), - tranches: pool.tranches.tranches.map((tranche, index) => { - const trancheId = pool.tranches.ids[index] - const trancheKeyIndex = trancheIdToIndex[trancheId] - // const lastClosedEpoch = epochs[trancheKeyIndex] - - let minRiskBuffer: Perquintill | null = null - let interestRatePerSec: Rate | null = null - if ('nonResidual' in tranche.trancheType) { - minRiskBuffer = new Perquintill(hexToBN(tranche.trancheType.nonResidual.minRiskBuffer)) - interestRatePerSec = new Rate(hexToBN(tranche.trancheType.nonResidual.interestRatePerSec)) - } + const maxReserve = new CurrencyBalance(hexToBN(pool.reserve.max), currency.decimals) + const availableReserve = new CurrencyBalance(hexToBN(pool.reserve.available), currency.decimals) + const totalReserve = new CurrencyBalance(hexToBN(pool.reserve.total), currency.decimals) + + const lastUpdatedNav = new Date((portfolioValuationData?.lastUpdated ?? 0) * 1000).toISOString() + // @ts-expect-error + const rawNav = rawNavs && rawNavs[poolIndex]?.toJSON() + + const mappedPool: Pool = { + id: poolId, + createdAt: null, + metadata, + currency, + tranches: pool.tranches.tranches.map((tranche, index) => { + const trancheId = pool.tranches.ids[index] + const trancheKeyIndex = trancheIdToIndex[trancheId] + // const lastClosedEpoch = epochs[trancheKeyIndex] + + let minRiskBuffer: Perquintill | null = null + let interestRatePerSec: Rate | null = null + if ('nonResidual' in tranche.trancheType) { + minRiskBuffer = new Perquintill(hexToBN(tranche.trancheType.nonResidual.minRiskBuffer)) + interestRatePerSec = new Rate(hexToBN(tranche.trancheType.nonResidual.interestRatePerSec)) + } - const subordinateTranchesValue = new CurrencyBalance( - pool.tranches.tranches.slice(0, index).reduce((prev: BN, tranche: TrancheDetailsData) => { - return new BN(prev.add(new BN(hexToBN(tranche.debt))).add(new BN(hexToBN(tranche.reserve)))) - }, new BN(0)), - currency.decimals - ) - - // @ts-expect-error - const rawPrice = rawPrices?.[poolIndex]?.toPrimitive()?.[index] - const tokenPrice = rawPrice ? new Price(rawPrice) : Price.fromFloat(1) - - const currentRiskBuffer = subordinateTranchesValue.gtn(0) - ? Perquintill.fromFloat(subordinateTranchesValue.toDecimal().div(poolValue.toDecimal())) - : new Perquintill(0) - - const protection = minRiskBuffer?.toDecimal() ?? Dec(0) - const tvl = poolValue.toDecimal() - let capacityGivenMaxReserve = maxReserve.toDecimal().minus(totalReserve.toDecimal()) - capacityGivenMaxReserve = capacityGivenMaxReserve.lt(0) ? Dec(0) : capacityGivenMaxReserve - const capacityGivenProtection = protection.isZero() - ? capacityGivenMaxReserve - : currentRiskBuffer.toDecimal().div(protection).mul(tvl).minus(tvl) - const capacity = capacityGivenMaxReserve.gt(capacityGivenProtection) - ? capacityGivenProtection - : capacityGivenMaxReserve - - return { - id: trancheId, - index, - seniority: tranche.seniority, - tokenPrice, - poolCurrency: currency, - currency: findCurrency(currencies, { Tranche: [poolId, trancheId] })!, - totalIssuance: new TokenBalance(rawIssuances[trancheKeyIndex].toString(), currency.decimals), - poolId, - poolMetadata: (metadata ?? undefined) as string | undefined, - interestRatePerSec, - yield30DaysAnnualized: yield30DaysAnnualizedByPoolIdTrancheId?.[`${poolId}-${trancheId}`], - minRiskBuffer, - currentRiskBuffer, - capacity: CurrencyBalance.fromFloat(capacity, currency.decimals), - ratio: new Perquintill(hexToBN(tranche.ratio)), - lastUpdatedInterest: new Date(tranche.lastUpdatedInterest * 1000).toISOString(), - balance: new TokenBalance(hexToBN(tranche.debt).add(hexToBN(tranche.reserve)), currency.decimals), - } - }), - reserve: { - max: maxReserve, - available: availableReserve, - total: totalReserve, - }, - epoch: { - ...pool.epoch, - lastClosed: new Date(pool.epoch.lastClosed * 1000).toISOString(), - status: getEpochStatus(epochExecution, blockNumber), - challengePeriodEnd: epochExecution?.challengePeriodEnd, - }, - parameters: { - ...pool.parameters, - challengeTime: api.consts.poolSystem.challengeTime.toJSON() as number, // in blocks - }, - nav: { - lastUpdated: lastUpdatedNav, - total: rawNav?.total - ? new CurrencyBalance(hexToBN(rawNav.total).add(hexToBN(rawNav.navFees)), currency.decimals) - : new CurrencyBalance(0, currency.decimals), - aum: rawNav?.navAum - ? new CurrencyBalance(hexToBN(rawNav.navAum), currency.decimals) - : new CurrencyBalance(0, currency.decimals), - }, - value: rawNav?.total + const subordinateTranchesValue = new CurrencyBalance( + pool.tranches.tranches.slice(0, index).reduce((prev: BN, tranche: TrancheDetailsData) => { + return new BN(prev.add(new BN(hexToBN(tranche.debt))).add(new BN(hexToBN(tranche.reserve)))) + }, new BN(0)), + currency.decimals + ) + + // @ts-expect-error + const rawPrice = rawPrices?.[poolIndex]?.toPrimitive()?.[index] + const tokenPrice = rawPrice ? new Price(rawPrice) : Price.fromFloat(1) + + const currentRiskBuffer = subordinateTranchesValue.gtn(0) + ? Perquintill.fromFloat(subordinateTranchesValue.toDecimal().div(poolValue.toDecimal())) + : new Perquintill(0) + + const protection = minRiskBuffer?.toDecimal() ?? Dec(0) + const tvl = poolValue.toDecimal() + let capacityGivenMaxReserve = maxReserve.toDecimal().minus(totalReserve.toDecimal()) + capacityGivenMaxReserve = capacityGivenMaxReserve.lt(0) ? Dec(0) : capacityGivenMaxReserve + const capacityGivenProtection = protection.isZero() + ? capacityGivenMaxReserve + : currentRiskBuffer.toDecimal().div(protection).mul(tvl).minus(tvl) + const capacity = capacityGivenMaxReserve.gt(capacityGivenProtection) + ? capacityGivenProtection + : capacityGivenMaxReserve + + return { + id: trancheId, + index, + seniority: tranche.seniority, + tokenPrice, + poolCurrency: currency, + currency: findCurrency(currencies, { Tranche: [poolId, trancheId] })!, + totalIssuance: new TokenBalance(rawIssuances[trancheKeyIndex].toString(), currency.decimals), + poolId, + poolMetadata: (metadata ?? undefined) as string | undefined, + interestRatePerSec, + yield30DaysAnnualized: yield30DaysAnnualizedByPoolIdTrancheId?.[`${poolId}-${trancheId}`], + minRiskBuffer, + currentRiskBuffer, + capacity: CurrencyBalance.fromFloat(capacity, currency.decimals), + ratio: new Perquintill(hexToBN(tranche.ratio)), + lastUpdatedInterest: new Date(tranche.lastUpdatedInterest * 1000).toISOString(), + balance: new TokenBalance(hexToBN(tranche.debt).add(hexToBN(tranche.reserve)), currency.decimals), + } + }), + reserve: { + max: maxReserve, + available: availableReserve, + total: totalReserve, + }, + epoch: { + ...pool.epoch, + lastClosed: new Date(pool.epoch.lastClosed * 1000).toISOString(), + status: getEpochStatus(epochExecution, blockNumber), + challengePeriodEnd: epochExecution?.challengePeriodEnd, + }, + parameters: { + ...pool.parameters, + challengeTime: api.consts.poolSystem.challengeTime.toJSON() as number, // in blocks + }, + nav: { + lastUpdated: lastUpdatedNav, + total: rawNav?.total ? new CurrencyBalance(hexToBN(rawNav.total).add(hexToBN(rawNav.navFees)), currency.decimals) : new CurrencyBalance(0, currency.decimals), - } - - return mappedPool - }) + aum: rawNav?.navAum + ? new CurrencyBalance(hexToBN(rawNav.navAum), currency.decimals) + : new CurrencyBalance(0, currency.decimals), + }, + value: rawNav?.total + ? new CurrencyBalance(hexToBN(rawNav.total).add(hexToBN(rawNav.navFees)), currency.decimals) + : new CurrencyBalance(0, currency.decimals), + } - return mappedPools + return mappedPool }) - ) - } - ), + + return mappedPools + }) + ) + }), combineLatestWith($query), map(([pools, gqlResult]) => { return pools.map((pool) => { @@ -2139,6 +2110,55 @@ export function getPoolsModule(inst: Centrifuge) { ) } + function getPoolFees(args: [poolId: string]) { + const [poolId] = args + + const $events = inst.getEvents().pipe( + filter(({ api, events }) => { + const event = events.find( + ({ event }) => + api.events.poolFees.Charged.is(event) || + api.events.poolFees.Added.is(event) || + api.events.poolFees.Removed.is(event) || + api.events.poolFees.Proposed.is(event) + ) + return !!event + }) + ) + + return inst.getApi().pipe( + switchMap((api) => + combineLatest([api.call.poolFeesApi.listFees(poolId), getPoolCurrency([poolId])]).pipe( + map(([feesListData, currency]) => { + const feesList = feesListData.toJSON() as { bucket: 'Top'; fees: ActivePoolFeesData[] }[] + const fees: ActivePoolFees[] = feesList.flatMap((entry) => { + return entry.fees.map((fee) => { + const type = Object.keys(fee.amounts.feeType)[0] as FeeTypes + const limit = Object.keys(fee.amounts.feeType[type].limit)[0] as FeeLimits + const percentOfNav = new Rate(hexToBN(fee.amounts.feeType[type].limit[limit])) + return { + ...fee, + position: entry.bucket, + type, + limit, + amounts: { + percentOfNav, + pending: new CurrencyBalance( + new BN(fee.amounts.pending).iadd(new BN(fee.amounts.disbursement)), + currency.decimals + ), + }, + } + }) + }) + return fees + }), + repeatWhen(() => $events) + ) + ) + ) + } + function getLatestTrancheSnapshot(poolIdTrancheId: string) { return inst.getSubqueryObservable<{ trancheSnapshots: { nodes: { yield30DaysAnnualized: string | null; trancheId: string }[] } @@ -2176,6 +2196,8 @@ export function getPoolsModule(inst: Centrifuge) { totalReserve portfolioValuation blockNumber + sumChargedAmountByPeriod + sumAccruedAmountByPeriod sumBorrowedAmountByPeriod sumRepaidAmountByPeriod sumInvestedAmountByPeriod @@ -2262,27 +2284,18 @@ export function getPoolsModule(inst: Centrifuge) { expand(({ trancheSnapshots, endCursor, hasNextPage }) => { if (!hasNextPage) return EMPTY return getTrancheSnapshotsWithCursor(filter, endCursor, from, to).pipe( - map( - ( - response: { - trancheSnapshots: { - nodes: SubqueryTrancheSnapshot[] - pageInfo: { hasNextPage: boolean; endCursor: string } - } - } | null - ) => { - if (response?.trancheSnapshots) { - const { endCursor, hasNextPage } = response.trancheSnapshots.pageInfo + map((response) => { + if (response?.trancheSnapshots) { + const { endCursor, hasNextPage } = response.trancheSnapshots.pageInfo - return { - endCursor, - hasNextPage, - trancheSnapshots: [...trancheSnapshots, ...response.trancheSnapshots.nodes], - } + return { + endCursor, + hasNextPage, + trancheSnapshots: [...trancheSnapshots, ...response.trancheSnapshots.nodes], } - return {} } - ) + return {} + }) ) }), takeLast(1), @@ -2314,27 +2327,18 @@ export function getPoolsModule(inst: Centrifuge) { expand(({ poolSnapshots, endCursor, hasNextPage }) => { if (!hasNextPage) return EMPTY return getPoolSnapshotsWithCursor(poolId, endCursor, from, to).pipe( - map( - ( - response: { - poolSnapshots: { - nodes: SubqueryPoolSnapshot[] - pageInfo: { hasNextPage: boolean; endCursor: string } - } - } | null - ) => { - if (response?.poolSnapshots) { - const { endCursor, hasNextPage } = response.poolSnapshots.pageInfo + map((response) => { + if (response?.poolSnapshots) { + const { endCursor, hasNextPage } = response.poolSnapshots.pageInfo - return { - endCursor, - hasNextPage, - poolSnapshots: [...poolSnapshots, ...response.poolSnapshots.nodes], - } + return { + endCursor, + hasNextPage, + poolSnapshots: [...poolSnapshots, ...response.poolSnapshots.nodes], } - return {} } - ) + return {} + }) ) }) ), @@ -2377,6 +2381,22 @@ export function getPoolsModule(inst: Centrifuge) { id: state.id, portfolioValuation: new CurrencyBalance(state.portfolioValuation, poolCurrency.decimals), totalReserve: new CurrencyBalance(state.totalReserve, poolCurrency.decimals), + sumChargedAmountByPeriod: new CurrencyBalance( + state.sumChargedAmountByPeriod ?? 0, + poolCurrency.decimals + ), + sumAccruedAmountByPeriod: new CurrencyBalance( + state.sumAccruedAmountByPeriod ?? 0, + poolCurrency.decimals + ), + sumBorrowedAmountByPeriod: new CurrencyBalance(state.sumBorrowedAmountByPeriod, poolCurrency.decimals), + sumInterestRepaidAmountByPeriod: new CurrencyBalance( + state.sumInterestRepaidAmountByPeriod, + poolCurrency.decimals + ), + sumRepaidAmountByPeriod: new CurrencyBalance(state.sumRepaidAmountByPeriod, poolCurrency.decimals), + sumInvestedAmountByPeriod: new CurrencyBalance(state.sumInvestedAmountByPeriod, poolCurrency.decimals), + sumRedeemedAmountByPeriod: new CurrencyBalance(state.sumRedeemedAmountByPeriod, poolCurrency.decimals), } const poolValue = new CurrencyBalance(new BN(state?.portfolioValuation || '0'), poolCurrency.decimals) @@ -3622,6 +3642,7 @@ export function getPoolsModule(inst: Centrifuge) { return $api.pipe( switchMap((api) => api.query.poolFees.lastFeeId()), + take(1), combineLatestWith($api), switchMap(([lastFeeId, api]) => { const removeSubmittables = remove.map((feeId) => api.tx.poolFees.removeFee([feeId])) @@ -3643,7 +3664,7 @@ export function getPoolsModule(inst: Centrifuge) { }) || []), ...add.map((metadata, index) => { return { - id: parseInt(lastFeeId.toHuman() as string) + index + 1, + id: parseInt(lastFeeId.toHuman() as string, 10) + index + 1, name: metadata.fee.name, feePosition: metadata.fee.feePosition, } @@ -3682,7 +3703,7 @@ export function getPoolsModule(inst: Centrifuge) { const $api = inst.getApi() return $api.pipe( switchMap((api) => combineLatest([api.query.poolFees.lastFeeId()])), - map((feeId) => parseInt(feeId[0].toHuman() as string) + 1) + map((feeId) => parseInt(feeId[0].toHuman() as string, 10) + 1) ) } @@ -3767,6 +3788,7 @@ export function getPoolsModule(inst: Centrifuge) { getPoolOrders, getPortfolio, getLoans, + getPoolFees, getPendingCollect, getWriteOffPolicy, getProposedPoolSystemChanges, diff --git a/centrifuge-js/src/types/subquery.ts b/centrifuge-js/src/types/subquery.ts index 8bf11695ff..bc1a7c8630 100644 --- a/centrifuge-js/src/types/subquery.ts +++ b/centrifuge-js/src/types/subquery.ts @@ -1,25 +1,18 @@ import { CurrencyBalance, Price } from '../utils/BN' export type SubqueryPoolSnapshot = { - __typename?: 'PoolSnapshot' id: string timestamp: string value: string - portfolioValuation: string - totalReserve: string - availableReserve: string - maxReserve: string - totalDebt?: string | null - totalBorrowed?: string | null - totalRepaid?: string | null - totalInvested?: string | null - totalRedeemed?: string | null - sumBorrowedAmount?: string | null - sumBorrowedAmountByPeriod?: string | null - sumInterestRepaidAmountByPeriod?: string | null - sumRepaidAmountByPeriod?: string | null - sumInvestedAmountByPeriod?: string | null - sumRedeemedAmountByPeriod?: string | null + portfolioValuation: number + totalReserve: number + sumChargedAmountByPeriod: string | null + sumAccruedAmountByPeriod: string | null + sumBorrowedAmountByPeriod: string + sumInterestRepaidAmountByPeriod: string + sumRepaidAmountByPeriod: string + sumInvestedAmountByPeriod: string + sumRedeemedAmountByPeriod: string blockNumber: number } From 3d7258f771b0e3e7674ce77abf519cca38591fc8 Mon Sep 17 00:00:00 2001 From: Jeroen <1748621+hieronx@users.noreply.github.com> Date: Wed, 29 May 2024 11:17:03 +0200 Subject: [PATCH 05/10] Fix transaction history (#2157) * Fix transaction history * Fix investor list * Fix 0 tx showing up * Remove console log --- centrifuge-app/.env-config/.env.ff-prod | 2 +- centrifuge-app/.env-config/.env.production | 2 +- .../PoolOverview/TransactionHistory.tsx | 79 +++++++------ .../Portfolio/TransactionTypeChip.tsx | 2 +- .../components/Report/AssetTransactions.tsx | 6 +- .../Report/{Holders.tsx => InvestorList.tsx} | 38 +++--- .../src/components/Report/ReportContext.tsx | 9 +- .../src/components/Report/ReportFilter.tsx | 8 +- .../src/components/Report/index.tsx | 6 +- .../src/components/Report/utils.tsx | 1 + .../src/pages/Loan/TransactionTable.tsx | 110 +++++++++--------- centrifuge-app/src/utils/usePools.ts | 6 +- centrifuge-js/src/modules/pools.ts | 7 +- centrifuge-js/src/types/subquery.ts | 15 ++- 14 files changed, 165 insertions(+), 126 deletions(-) rename centrifuge-app/src/components/Report/{Holders.tsx => InvestorList.tsx} (77%) diff --git a/centrifuge-app/.env-config/.env.ff-prod b/centrifuge-app/.env-config/.env.ff-prod index ad8edfb97a..a2f9d57840 100644 --- a/centrifuge-app/.env-config/.env.ff-prod +++ b/centrifuge-app/.env-config/.env.ff-prod @@ -9,7 +9,7 @@ REACT_APP_ONBOARDING_API_URL=https://europe-central2-centrifuge-production-x.clo REACT_APP_PINNING_API_URL=https://europe-central2-centrifuge-production-x.cloudfunctions.net/pinning-api-production REACT_APP_POOL_CREATION_TYPE=propose REACT_APP_RELAY_WSS_URL=wss://rpc.polkadot.io -REACT_APP_SUBQUERY_URL=https://api.subquery.network/sq/centrifuge/pools +REACT_APP_SUBQUERY_URL=https://api.subquery.network/sq/centrifuge/pools-multichain REACT_APP_SUBSCAN_URL=https://centrifuge.subscan.io REACT_APP_TINLAKE_NETWORK=mainnet REACT_APP_INFURA_KEY=8ed99a9a115349bbbc01dcf3a24edc96 diff --git a/centrifuge-app/.env-config/.env.production b/centrifuge-app/.env-config/.env.production index d8ae8c2510..c9b8fdc1e5 100644 --- a/centrifuge-app/.env-config/.env.production +++ b/centrifuge-app/.env-config/.env.production @@ -9,7 +9,7 @@ REACT_APP_ONBOARDING_API_URL=https://europe-central2-centrifuge-production-x.clo REACT_APP_PINNING_API_URL=https://europe-central2-centrifuge-production-x.cloudfunctions.net/pinning-api-production REACT_APP_POOL_CREATION_TYPE=propose REACT_APP_RELAY_WSS_URL=wss://rpc.polkadot.io -REACT_APP_SUBQUERY_URL=https://api.subquery.network/sq/centrifuge/pools +REACT_APP_SUBQUERY_URL=https://api.subquery.network/sq/centrifuge/pools-multichain REACT_APP_SUBSCAN_URL=https://centrifuge.subscan.io REACT_APP_TINLAKE_NETWORK=mainnet REACT_APP_INFURA_KEY=8ed99a9a115349bbbc01dcf3a24edc96 diff --git a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx index 9e50fd296c..8f24a4f444 100644 --- a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx +++ b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx @@ -1,5 +1,5 @@ -import { AssetTransaction, AssetTransactionType, AssetType, CurrencyBalance } from '@centrifuge/centrifuge-js' -import { AnchorButton, Box, IconDownload, IconExternalLink, Shelf, Stack, StatusChip, Text } from '@centrifuge/fabric' +import { AssetTransaction, AssetType, CurrencyBalance } from '@centrifuge/centrifuge-js' +import { AnchorButton, IconDownload, IconExternalLink, Shelf, Stack, StatusChip, Text } from '@centrifuge/fabric' import BN from 'bn.js' import { nftMetadataSchema } from '../../schemas' import { formatDate } from '../../utils/date' @@ -88,25 +88,43 @@ export const TransactionHistory = ({ poolId, preview = true }: { poolId: string; nftMetadataSchema ) - const getLabelAndAmount = ( - transaction: Omit & { type: AssetTransactionType | 'SETTLED' } - ) => { - if (transaction.asset.type == AssetType.OffchainCash) { + const getLabelAndAmount = (transaction: AssetTransaction) => { + if (transaction.type === 'CASH_TRANSFER') { return { label: 'Cash transfer', amount: transaction.amount, } } - if (transaction.type === 'BORROWED' || transaction.type === 'SETTLED') { + if (transaction.type === 'BORROWED') { return { label: 'Purchase', amount: transaction.amount, } } - if (transaction.type === 'REPAID' && !new BN(transaction.interestAmount || 0).isZero()) { + + // TODO: ideally, if both principalAmount and interestAmount are non-zero, there should be 2 separate transactions + if ( + transaction.type === 'REPAID' && + !new BN(transaction.interestAmount || 0).isZero() && + !new BN(transaction.principalAmount || 0).isZero() + ) { + return { + label: 'Principal & interest payment', + amount: new CurrencyBalance( + new BN(transaction.principalAmount || 0).add(new BN(transaction.interestAmount || 0)), + transaction.principalAmount!.decimals + ), + } + } + + if ( + transaction.type === 'REPAID' && + !new BN(transaction.interestAmount || 0).isZero() && + new BN(transaction.principalAmount || 0).isZero() + ) { return { - label: 'Interest', + label: 'Interest payment', amount: transaction.interestAmount, } } @@ -117,22 +135,13 @@ export const TransactionHistory = ({ poolId, preview = true }: { poolId: string; } } - const settlements = transactions?.reduce((acc, transaction, index) => { - if (transaction.hash === transactions[index + 1]?.hash) { - acc[transaction.hash] = { ...transaction, type: 'SETTLED' } - } - - return acc - }, {} as Record & { type: AssetTransactionType | 'SETTLED' }>) - - const transformedTransactions = [ - ...(transactions?.filter((transaction) => !settlements?.[transaction.hash]) || []), - ...Object.values(settlements || []), - ] - .filter( - (transaction) => transaction.type !== 'CREATED' && transaction.type !== 'CLOSED' && transaction.type !== 'PRICED' - ) - .sort((a, b) => (a.timestamp > b.timestamp ? -1 : 1)) + const transformedTransactions = + transactions + ?.filter( + (transaction) => + transaction.type !== 'CREATED' && transaction.type !== 'CLOSED' && transaction.type !== 'PRICED' + ) + .sort((a, b) => (a.timestamp > b.timestamp ? -1 : 1)) || [] const csvData = transformedTransactions.map((transaction) => { const { label, amount } = getLabelAndAmount(transaction) @@ -151,9 +160,9 @@ export const TransactionHistory = ({ poolId, preview = true }: { poolId: string; 'Asset Name': transaction.asset.type == AssetType.OffchainCash ? transaction.type === 'BORROWED' - ? `Onchain reserve > Settlement account` - : `Settlement account` - : `${assetMetadata[Number(id) - 1].data?.name || '-'}`, + ? `Onchain reserve > Settlement Account` + : `Settlement Account > onchain reserve` + : assetMetadata[Number(id) - 1]?.data?.name || `Asset ${id}`, Amount: amount ? `"${formatBalance(amount, 'USD', 2, 2)}"` : '-', Transaction: `${import.meta.env.REACT_APP_SUBSCAN_URL}/extrinsic/${transaction.hash}`, } @@ -170,11 +179,11 @@ export const TransactionHistory = ({ poolId, preview = true }: { poolId: string; transactionDate: transaction.timestamp, assetId: transaction.asset.id, assetName: - transaction.asset.type == AssetType.OffchainCash - ? transaction.type === 'BORROWED' - ? `Onchain reserve > Settlement account` - : `Settlement account` - : `${assetMetadata[Number(id) - 1].data?.name || '-'}`, + transaction.type == 'CASH_TRANSFER' + ? transaction.fromAsset?.id === '0' + ? 'Onchain reserve > Offchain cash' + : 'Offchain cash > Onchain reserve' + : assetMetadata[Number(id) - 1]?.data?.name || `Asset ${id}`, amount: amount || 0, hash: transaction.hash, } @@ -199,9 +208,7 @@ export const TransactionHistory = ({ poolId, preview = true }: { poolId: string; )} - - - + {transactions?.length! > 8 && preview && ( View all diff --git a/centrifuge-app/src/components/Portfolio/TransactionTypeChip.tsx b/centrifuge-app/src/components/Portfolio/TransactionTypeChip.tsx index b44e886c39..170e85d016 100644 --- a/centrifuge-app/src/components/Portfolio/TransactionTypeChip.tsx +++ b/centrifuge-app/src/components/Portfolio/TransactionTypeChip.tsx @@ -1,6 +1,5 @@ import { AssetTransactionType, InvestorTransactionType } from '@centrifuge/centrifuge-js' import { StatusChip } from '@centrifuge/fabric' -import * as React from 'react' import { formatTransactionsType } from '../Report/utils' type TransactionTypeProps = { @@ -28,6 +27,7 @@ const status = { REPAID: 'default', CLOSED: 'default', PRICED: 'default', + CASH_TRANSFER: 'default', } as const export function TransactionTypeChip(props: TransactionTypeProps) { diff --git a/centrifuge-app/src/components/Report/AssetTransactions.tsx b/centrifuge-app/src/components/Report/AssetTransactions.tsx index dc4e359c06..e2e8953366 100644 --- a/centrifuge-app/src/components/Report/AssetTransactions.tsx +++ b/centrifuge-app/src/components/Report/AssetTransactions.tsx @@ -42,7 +42,7 @@ export function AssetTransactions({ pool }: { pool: Pool }) { { header: 'Date', align: 'left', - csvOnly: true, + csvOnly: false, formatter: formatDate, }, { @@ -54,7 +54,7 @@ export function AssetTransactions({ pool }: { pool: Pool }) { { header: 'Currency amount', align: 'left', - csvOnly: true, + csvOnly: false, formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, pool.currency.symbol, 5) : '-'), }, { @@ -97,7 +97,7 @@ export function AssetTransactions({ pool }: { pool: Pool }) { name: '', value: [ tx.asset.id.split('-').at(-1)!, - metadataByUrl[tx.asset.metadata]?.name ?? '', + metadataByUrl[tx.asset.metadata]?.name ?? `Asset ${tx.asset.id.split('-').at(-1)!}`, tx.epochId.split('-').at(-1)!, tx.timestamp.toISOString(), formatAssetTransactionType(tx.type), diff --git a/centrifuge-app/src/components/Report/Holders.tsx b/centrifuge-app/src/components/Report/InvestorList.tsx similarity index 77% rename from centrifuge-app/src/components/Report/Holders.tsx rename to centrifuge-app/src/components/Report/InvestorList.tsx index ec2b0d1c37..66a929faa7 100644 --- a/centrifuge-app/src/components/Report/Holders.tsx +++ b/centrifuge-app/src/components/Report/InvestorList.tsx @@ -6,7 +6,7 @@ import * as React from 'react' import { evmChains } from '../../config' import { formatBalance } from '../../utils/formatting' import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl' -import { useHolders } from '../../utils/usePools' +import { useInvestorList } from '../../utils/usePools' import { DataTable } from '../DataTable' import { Spinner } from '../Spinner' import { ReportContext } from './ReportContext' @@ -16,11 +16,11 @@ import { copyable } from './utils' const noop = (v: any) => v -export function Holders({ pool }: { pool: Pool }) { +export function InvestorList({ pool }: { pool: Pool }) { const { activeTranche, setCsvData, network, address } = React.useContext(ReportContext) const utils = useCentrifugeUtils() - const holders = useHolders(pool.id, activeTranche === 'all' ? undefined : activeTranche) + const investors = useInvestorList(pool.id, activeTranche === 'all' ? undefined : activeTranche) const columnConfig = [ { @@ -83,27 +83,27 @@ export function Holders({ pool }: { pool: Pool }) { .filter((col) => !col.csvOnly) const data: TableDataRow[] = React.useMemo(() => { - if (!holders) { + if (!investors) { return [] } - return holders - .filter((holder) => !holder.balance.isZero() || !holder.claimableTrancheTokens.isZero()) + return investors + .filter((investor) => !investor.balance.isZero() || !investor.claimableTrancheTokens.isZero()) .filter((tx) => { if (!network || network === 'all') return true return network === (tx.chainId || 'centrifuge') }) - .map((holder) => { - const token = pool.tranches.find((t) => t.id === holder.trancheId)! + .map((investor) => { + const token = pool.tranches.find((t) => t.id === investor.trancheId)! return { name: '', value: [ - (evmChains as any)[holder.chainId]?.name || 'Centrifuge', - holder.evmAddress || utils.formatAddress(holder.accountId), - holder.balance.toFloat() + holder.claimableTrancheTokens.toFloat(), + (evmChains as any)[investor.chainId]?.name || 'Centrifuge', + investor.evmAddress || utils.formatAddress(investor.accountId), + investor.balance.toFloat() + investor.claimableTrancheTokens.toFloat(), token.currency.symbol, - holder.pendingInvestCurrency.toFloat(), + investor.pendingInvestCurrency.toFloat(), pool.currency.symbol, - holder.pendingRedeemTrancheTokens.toFloat(), + investor.pendingRedeemTrancheTokens.toFloat(), token.currency.symbol, ], token, @@ -115,7 +115,7 @@ export function Holders({ pool }: { pool: Pool }) { return isAddress(address) && isSameAddress(address, row.value[1]) }) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [holders, network, pool, address]) + }, [investors, network, pool, address]) const dataUrl = React.useMemo(() => { if (!data.length) { @@ -135,7 +135,7 @@ export function Holders({ pool }: { pool: Pool }) { dataUrl ? { dataUrl, - fileName: `${pool.id}-holders.csv`, + fileName: `${pool.id}-investors.csv`, } : undefined ) @@ -144,9 +144,13 @@ export function Holders({ pool }: { pool: Pool }) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [dataUrl, pool.id]) - if (!holders) { + if (!investors) { return } - return data.length > 0 ? : + return data.length > 0 ? ( + + ) : ( + + ) } diff --git a/centrifuge-app/src/components/Report/ReportContext.tsx b/centrifuge-app/src/components/Report/ReportContext.tsx index 3ffc5a70b9..5024aff63a 100644 --- a/centrifuge-app/src/components/Report/ReportContext.tsx +++ b/centrifuge-app/src/components/Report/ReportContext.tsx @@ -2,7 +2,14 @@ import * as React from 'react' export type GroupBy = 'day' | 'month' -export type Report = 'pool-balance' | 'token-price' | 'asset-list' | 'investor-tx' | 'asset-tx' | 'fee-tx' | 'holders' +export type Report = + | 'pool-balance' + | 'token-price' + | 'asset-list' + | 'investor-tx' + | 'asset-tx' + | 'fee-tx' + | 'investor-list' export type ReportContextType = { csvData?: CsvDataProps diff --git a/centrifuge-app/src/components/Report/ReportFilter.tsx b/centrifuge-app/src/components/Report/ReportFilter.tsx index b0ecda19e1..bf42ceb21b 100644 --- a/centrifuge-app/src/components/Report/ReportFilter.tsx +++ b/centrifuge-app/src/components/Report/ReportFilter.tsx @@ -52,7 +52,7 @@ export function ReportFilter({ pool }: ReportFilterProps) { { label: 'Pool balance', value: 'pool-balance' }, { label: 'Token price', value: 'token-price' }, { label: 'Asset list', value: 'asset-list' }, - ...(holdersReport === true ? [{ label: 'Holders', value: 'holders' as Report }] : []), + ...(holdersReport === true ? [{ label: 'Investor list', value: 'investor-list' as Report }] : []), ] return ( @@ -78,7 +78,7 @@ export function ReportFilter({ pool }: ReportFilterProps) { }} /> - {!['holders', 'asset-list'].includes(report) && ( + {!['investor-list', 'asset-list'].includes(report) && ( <> setStartDate(e.target.value)} /> setEndDate(e.target.value)} /> @@ -137,7 +137,7 @@ export function ReportFilter({ pool }: ReportFilterProps) { /> )} - {(report === 'holders' || report === 'investor-tx') && ( + {(report === 'investor-list' || report === 'investor-tx') && ( - {!['holders', 'asset-list'].includes(report) && ( + {!['investor-list', 'asset-list'].includes(report) && ( <> {startDate ? formatDate(startDate) : 'The beginning of time'} {' - '} @@ -42,7 +42,7 @@ export function ReportComponent({ pool }: { pool: Pool }) { {report === 'pool-balance' && } {report === 'token-price' && } {report === 'asset-list' && } - {report === 'holders' && } + {report === 'investor-list' && } {report === 'investor-tx' && } {report === 'asset-tx' && } {report === 'fee-tx' && } diff --git a/centrifuge-app/src/components/Report/utils.tsx b/centrifuge-app/src/components/Report/utils.tsx index 6a3b1951a0..afbe849cbe 100644 --- a/centrifuge-app/src/components/Report/utils.tsx +++ b/centrifuge-app/src/components/Report/utils.tsx @@ -77,6 +77,7 @@ const assetTransactionTypes: { BORROWED: 'Financed', REPAID: 'Repaid', CLOSED: 'Closed', + CASH_TRANSFER: 'Cash transfer', } export function formatAssetTransactionType(type: AssetTransactionType) { diff --git a/centrifuge-app/src/pages/Loan/TransactionTable.tsx b/centrifuge-app/src/pages/Loan/TransactionTable.tsx index b324cf91ee..9837468f4e 100644 --- a/centrifuge-app/src/pages/Loan/TransactionTable.tsx +++ b/centrifuge-app/src/pages/Loan/TransactionTable.tsx @@ -58,62 +58,66 @@ export const TransactionTable = ({ return 0 }) - return sortedTransactions.map((transaction, index, array) => { - const termDays = originationDate - ? daysBetween(originationDate, maturityDate) - : daysBetween(new Date(), maturityDate) - const yearsBetweenDates = termDays / 365 + return sortedTransactions + .filter((transaction) => { + return !transaction.amount?.isZero() + }) + .map((transaction, index, array) => { + const termDays = originationDate + ? daysBetween(originationDate, maturityDate) + : daysBetween(new Date(), maturityDate) + const yearsBetweenDates = termDays / 365 - const faceValue = - transaction.quantity && (pricing as ExternalPricingInfo).notional - ? new CurrencyBalance(transaction.quantity, 18) - .toDecimal() - .mul((pricing as ExternalPricingInfo).notional.toDecimal()) - : null + const faceValue = + transaction.quantity && (pricing as ExternalPricingInfo).notional + ? new CurrencyBalance(transaction.quantity, 18) + .toDecimal() + .mul((pricing as ExternalPricingInfo).notional.toDecimal()) + : null - return { - type: transaction.type, - amount: transaction.amount, - quantity: transaction.quantity ? new CurrencyBalance(transaction.quantity, 18) : null, - transactionDate: transaction.timestamp, - yieldToMaturity: - transaction.amount && faceValue && transaction.type !== 'REPAID' - ? Dec(2) - .mul(faceValue?.sub(transaction.amount.toDecimal())) - .div(Dec(yearsBetweenDates).mul(faceValue.add(transaction.amount.toDecimal()))) - .mul(100) + return { + type: transaction.type, + amount: transaction.amount, + quantity: transaction.quantity ? new CurrencyBalance(transaction.quantity, 18) : null, + transactionDate: transaction.timestamp, + yieldToMaturity: + transaction.amount && faceValue && transaction.type !== 'REPAID' + ? Dec(2) + .mul(faceValue?.sub(transaction.amount.toDecimal())) + .div(Dec(yearsBetweenDates).mul(faceValue.add(transaction.amount.toDecimal()))) + .mul(100) + : null, + settlePrice: transaction.settlementPrice + ? new CurrencyBalance(new BN(transaction.settlementPrice), decimals) : null, - settlePrice: transaction.settlementPrice - ? new CurrencyBalance(new BN(transaction.settlementPrice), decimals) - : null, - faceValue, - position: array.slice(0, index + 1).reduce((sum, trx) => { - if (trx.type === 'BORROWED') { - sum = sum.add( - trx.quantity - ? new CurrencyBalance(trx.quantity, 18) - .toDecimal() - .mul((pricing as ExternalPricingInfo).notional.toDecimal()) - : trx.amount - ? trx.amount.toDecimal() - : Dec(0) - ) - } - if (trx.type === 'REPAID') { - sum = sum.sub( - trx.quantity - ? new CurrencyBalance(trx.quantity, 18) - .toDecimal() - .mul((pricing as ExternalPricingInfo).notional.toDecimal()) - : trx.amount - ? trx.amount.toDecimal() - : Dec(0) - ) - } - return sum - }, Dec(0)), - } - }) + faceValue, + position: array.slice(0, index + 1).reduce((sum, trx) => { + if (trx.type === 'BORROWED') { + sum = sum.add( + trx.quantity + ? new CurrencyBalance(trx.quantity, 18) + .toDecimal() + .mul((pricing as ExternalPricingInfo).notional.toDecimal()) + : trx.amount + ? trx.amount.toDecimal() + : Dec(0) + ) + } + if (trx.type === 'REPAID') { + sum = sum.sub( + trx.quantity + ? new CurrencyBalance(trx.quantity, 18) + .toDecimal() + .mul((pricing as ExternalPricingInfo).notional.toDecimal()) + : trx.amount + ? trx.amount.toDecimal() + : Dec(0) + ) + } + return sum + }, Dec(0)), + } + }) }, [transactions, decimals, pricing]) const getStatusChipType = (type: AssetTransactionType) => { diff --git a/centrifuge-app/src/utils/usePools.ts b/centrifuge-app/src/utils/usePools.ts index fd230a7097..f57eccb00f 100644 --- a/centrifuge-app/src/utils/usePools.ts +++ b/centrifuge-app/src/utils/usePools.ts @@ -64,9 +64,9 @@ export function useTransactionsByAddress(address?: string) { return result } -export function useHolders(poolId: string, trancheId?: string) { - const [result] = useCentrifugeQuery(['holders', poolId, trancheId], (cent) => - cent.pools.getHolders([poolId, trancheId]) +export function useInvestorList(poolId: string, trancheId?: string) { + const [result] = useCentrifugeQuery(['investors', poolId, trancheId], (cent) => + cent.pools.getInvestors([poolId, trancheId]) ) return result diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index 3a655f14d8..e6615e30ba 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -2681,16 +2681,19 @@ export function getPoolsModule(inst: Centrifuge) { asset { id metadata + name type } fromAsset { id metadata + name type } toAsset { id metadata + name type } } @@ -2772,7 +2775,7 @@ export function getPoolsModule(inst: Centrifuge) { ) } - function getHolders(args: [poolId: string, trancheId?: string]) { + function getInvestors(args: [poolId: string, trancheId?: string]) { const [poolId, trancheId] = args const $query = inst.getApi().pipe( switchMap(() => { @@ -3808,7 +3811,7 @@ export function getPoolsModule(inst: Centrifuge) { getDailyTrancheStates, getTransactionsByAddress, getDailyTVL, - getHolders, + getInvestors, } } diff --git a/centrifuge-js/src/types/subquery.ts b/centrifuge-js/src/types/subquery.ts index bc1a7c8630..9de154e9e5 100644 --- a/centrifuge-js/src/types/subquery.ts +++ b/centrifuge-js/src/types/subquery.ts @@ -68,7 +68,7 @@ export type SubqueryInvestorTransaction = { transactionFee?: string | null } -export type AssetTransactionType = 'CREATED' | 'PRICED' | 'BORROWED' | 'REPAID' | 'CLOSED' +export type AssetTransactionType = 'CREATED' | 'PRICED' | 'BORROWED' | 'REPAID' | 'CLOSED' | 'CASH_TRANSFER' export enum AssetType { OnchainCash = 'OnchainCash', @@ -93,6 +93,19 @@ export type SubqueryAssetTransaction = { asset: { id: string metadata: string + name: string + type: AssetType + } + fromAsset?: { + id: string + metadata: string + name: string + type: AssetType + } + toAsset?: { + id: string + metadata: string + name: string type: AssetType } } From abdfceb84526198eb5808ec754cbe64316a096fc Mon Sep 17 00:00:00 2001 From: Jeroen <1748621+hieronx@users.noreply.github.com> Date: Wed, 29 May 2024 11:24:57 +0200 Subject: [PATCH 06/10] Subquery fee renamings (#2158) * Fix transaction history * Fix investor list * Fix 0 tx showing up * Remove console log * Fix fee names --- .../src/components/Charts/CashflowsChart.tsx | 4 ++-- centrifuge-js/src/modules/pools.ts | 16 ++++++++-------- centrifuge-js/src/types/subquery.ts | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/centrifuge-app/src/components/Charts/CashflowsChart.tsx b/centrifuge-app/src/components/Charts/CashflowsChart.tsx index 22e7f74d97..f2cdf76b0f 100644 --- a/centrifuge-app/src/components/Charts/CashflowsChart.tsx +++ b/centrifuge-app/src/components/Charts/CashflowsChart.tsx @@ -44,8 +44,8 @@ export const CashflowsChart = ({ poolStates, pool }: Props) => { const interest = new CurrencyBalance(day.sumInterestRepaidAmountByPeriod, pool.currency.decimals).toFloat() const fees = - new CurrencyBalance(day.sumChargedAmountByPeriod ?? 0, pool.currency.decimals).toFloat() + - new CurrencyBalance(day.sumAccruedAmountByPeriod ?? 0, pool.currency.decimals).toFloat() + new CurrencyBalance(day.sumPoolFeesChargedAmountByPeriod ?? 0, pool.currency.decimals).toFloat() + + new CurrencyBalance(day.sumPoolFeesAccruedAmountByPeriod ?? 0, pool.currency.decimals).toFloat() return { name: new Date(day.timestamp), purchases, principalRepayments, interest, fees } }) || [], [poolStates, pool.currency.decimals] diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index e6615e30ba..9682e6d27a 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -571,8 +571,8 @@ export type DailyPoolState = { poolValue: CurrencyBalance timestamp: string tranches: { [trancheId: string]: DailyTrancheState } - sumChargedAmountByPeriod: string | null - sumAccruedAmountByPeriod: string | null + sumPoolFeesChargedAmountByPeriod: string | null + sumPoolFeesAccruedAmountByPeriod: string | null sumBorrowedAmountByPeriod: string sumInterestRepaidAmountByPeriod: string sumRepaidAmountByPeriod: string @@ -2196,8 +2196,8 @@ export function getPoolsModule(inst: Centrifuge) { totalReserve portfolioValuation blockNumber - sumChargedAmountByPeriod - sumAccruedAmountByPeriod + sumPoolFeesChargedAmountByPeriod + sumPoolFeesAccruedAmountByPeriod sumBorrowedAmountByPeriod sumRepaidAmountByPeriod sumInvestedAmountByPeriod @@ -2381,12 +2381,12 @@ export function getPoolsModule(inst: Centrifuge) { id: state.id, portfolioValuation: new CurrencyBalance(state.portfolioValuation, poolCurrency.decimals), totalReserve: new CurrencyBalance(state.totalReserve, poolCurrency.decimals), - sumChargedAmountByPeriod: new CurrencyBalance( - state.sumChargedAmountByPeriod ?? 0, + sumPoolFeesChargedAmountByPeriod: new CurrencyBalance( + state.sumPoolFeesChargedAmountByPeriod ?? 0, poolCurrency.decimals ), - sumAccruedAmountByPeriod: new CurrencyBalance( - state.sumAccruedAmountByPeriod ?? 0, + sumPoolFeesAccruedAmountByPeriod: new CurrencyBalance( + state.sumPoolFeesAccruedAmountByPeriod ?? 0, poolCurrency.decimals ), sumBorrowedAmountByPeriod: new CurrencyBalance(state.sumBorrowedAmountByPeriod, poolCurrency.decimals), diff --git a/centrifuge-js/src/types/subquery.ts b/centrifuge-js/src/types/subquery.ts index 9de154e9e5..427d122f7e 100644 --- a/centrifuge-js/src/types/subquery.ts +++ b/centrifuge-js/src/types/subquery.ts @@ -6,8 +6,8 @@ export type SubqueryPoolSnapshot = { value: string portfolioValuation: number totalReserve: number - sumChargedAmountByPeriod: string | null - sumAccruedAmountByPeriod: string | null + sumPoolFeesChargedAmountByPeriod: string | null + sumPoolFeesAccruedAmountByPeriod: string | null sumBorrowedAmountByPeriod: string sumInterestRepaidAmountByPeriod: string sumRepaidAmountByPeriod: string From 0e97ef6ce1a5aed75b0722d327d4d12a278a30f4 Mon Sep 17 00:00:00 2001 From: Jeroen <1748621+hieronx@users.noreply.github.com> Date: Wed, 29 May 2024 12:39:24 +0200 Subject: [PATCH 07/10] Add asset snapshots and improve investor list (#2160) * Fix transaction history * Fix investor list * Fix 0 tx showing up * Remove console log * Fix fee names * Add asset snapshot timeseries * Improve and open up investor list * Fix compilation --- .../src/components/DebugFlags/config.ts | 4 +- .../src/components/Report/InvestorList.tsx | 28 +++++- .../src/components/Report/ReportFilter.tsx | 5 +- centrifuge-app/src/pages/Loan/index.tsx | 40 +++++++- centrifuge-app/src/utils/usePools.ts | 12 +++ centrifuge-js/src/modules/pools.ts | 92 +++++++++++++++++++ centrifuge-js/src/types/subquery.ts | 20 ++++ 7 files changed, 188 insertions(+), 13 deletions(-) diff --git a/centrifuge-app/src/components/DebugFlags/config.ts b/centrifuge-app/src/components/DebugFlags/config.ts index 02c1e56420..e2dc2d4b11 100644 --- a/centrifuge-app/src/components/DebugFlags/config.ts +++ b/centrifuge-app/src/components/DebugFlags/config.ts @@ -51,7 +51,7 @@ export type Key = | 'showOracle' | 'poolCreationType' | 'podAdminSeed' - | 'holdersReport' + | 'assetSnapshots' export const flagsConfig: Record = { address: { @@ -93,7 +93,7 @@ export const flagsConfig: Record = { default: '', type: 'text', }, - holdersReport: { + assetSnapshots: { type: 'checkbox', default: false, }, diff --git a/centrifuge-app/src/components/Report/InvestorList.tsx b/centrifuge-app/src/components/Report/InvestorList.tsx index 66a929faa7..875b7501fb 100644 --- a/centrifuge-app/src/components/Report/InvestorList.tsx +++ b/centrifuge-app/src/components/Report/InvestorList.tsx @@ -1,13 +1,14 @@ -import { Pool, isSameAddress } from '@centrifuge/centrifuge-js' +import { CurrencyBalance, Pool, isSameAddress } from '@centrifuge/centrifuge-js' import { useCentrifugeUtils } from '@centrifuge/centrifuge-react' import { Text } from '@centrifuge/fabric' import { isAddress } from '@polkadot/util-crypto' +import BN from 'bn.js' import * as React from 'react' import { evmChains } from '../../config' -import { formatBalance } from '../../utils/formatting' +import { formatBalance, formatPercentage } from '../../utils/formatting' import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl' import { useInvestorList } from '../../utils/usePools' -import { DataTable } from '../DataTable' +import { DataTable, SortableTableHeader } from '../DataTable' import { Spinner } from '../Spinner' import { ReportContext } from './ReportContext' import { UserFeedback } from './UserFeedback' @@ -41,6 +42,13 @@ export function InvestorList({ pool }: { pool: Pool }) { csvOnly: false, formatter: (v: any, row: any) => (typeof v === 'number' ? formatBalance(v, row.token.currency.symbol, 5) : '-'), }, + { + header: 'Pool %', + align: 'right', + sortable: true, + csvOnly: false, + formatter: (v: any, row: any) => (typeof v === 'number' ? formatPercentage(v * 100, true, {}, 2) : '-'), + }, { header: 'Position currency', align: 'left', @@ -76,8 +84,9 @@ export function InvestorList({ pool }: { pool: Pool }) { const columns = columnConfig .map((col, index) => ({ align: col.align, - header: col.header, + header: col.sortable ? : col.header, cell: (row: TableDataRow) => {col.formatter((row.value as any)[index], row)}, + sortKey: col.sortable ? `value[${index}]` : undefined, csvOnly: col.csvOnly, })) .filter((col) => !col.csvOnly) @@ -86,6 +95,14 @@ export function InvestorList({ pool }: { pool: Pool }) { if (!investors) { return [] } + + const totalPositions = new CurrencyBalance( + investors.reduce((sum: BN, investor) => { + return sum.add(investor.balance).add(investor.claimableTrancheTokens) + }, new BN(0)), + investors[0].balance.decimals || 18 + ).toFloat() + return investors .filter((investor) => !investor.balance.isZero() || !investor.claimableTrancheTokens.isZero()) .filter((tx) => { @@ -100,6 +117,7 @@ export function InvestorList({ pool }: { pool: Pool }) { (evmChains as any)[investor.chainId]?.name || 'Centrifuge', investor.evmAddress || utils.formatAddress(investor.accountId), investor.balance.toFloat() + investor.claimableTrancheTokens.toFloat(), + (investor.balance.toFloat() + investor.claimableTrancheTokens.toFloat()) / totalPositions, token.currency.symbol, investor.pendingInvestCurrency.toFloat(), pool.currency.symbol, @@ -149,7 +167,7 @@ export function InvestorList({ pool }: { pool: Pool }) { } return data.length > 0 ? ( - + ) : ( ) diff --git a/centrifuge-app/src/components/Report/ReportFilter.tsx b/centrifuge-app/src/components/Report/ReportFilter.tsx index bf42ceb21b..71cc2f45e3 100644 --- a/centrifuge-app/src/components/Report/ReportFilter.tsx +++ b/centrifuge-app/src/components/Report/ReportFilter.tsx @@ -7,7 +7,6 @@ import { useActiveDomains } from '../../utils/useLiquidityPools' import { useLoans } from '../../utils/useLoans' import { useMetadata } from '../../utils/useMetadata' import { useCentNFT } from '../../utils/useNFTs' -import { useDebugFlags } from '../DebugFlags' import { GroupBy, Report, ReportContext } from './ReportContext' import { formatPoolFeeTransactionType } from './utils' @@ -16,8 +15,6 @@ type ReportFilterProps = { } export function ReportFilter({ pool }: ReportFilterProps) { - const { holdersReport } = useDebugFlags() - const { csvData, setStartDate, @@ -52,7 +49,7 @@ export function ReportFilter({ pool }: ReportFilterProps) { { label: 'Pool balance', value: 'pool-balance' }, { label: 'Token price', value: 'token-price' }, { label: 'Asset list', value: 'asset-list' }, - ...(holdersReport === true ? [{ label: 'Investor list', value: 'investor-list' as Report }] : []), + { label: 'Investor list', value: 'investor-list' }, ] return ( diff --git a/centrifuge-app/src/pages/Loan/index.tsx b/centrifuge-app/src/pages/Loan/index.tsx index 43b591800d..7717eb92dd 100644 --- a/centrifuge-app/src/pages/Loan/index.tsx +++ b/centrifuge-app/src/pages/Loan/index.tsx @@ -7,11 +7,13 @@ import { TinlakeLoan, } from '@centrifuge/centrifuge-js' import { + AnchorButton, Box, Button, Drawer, Flex, IconChevronLeft, + IconDownload, Shelf, Stack, Text, @@ -22,6 +24,7 @@ import { import * as React from 'react' import { useParams, useRouteMatch } from 'react-router' import { AssetSummary } from '../../components/AssetSummary' +import { useDebugFlags } from '../../components/DebugFlags' import { LabelValueStack } from '../../components/LabelValueStack' import { LayoutBase } from '../../components/LayoutBase' import { LoadBoundary } from '../../components/LoadBoundary' @@ -36,12 +39,13 @@ import { Dec } from '../../utils/Decimal' import { copyToClipboard } from '../../utils/copyToClipboard' import { daysBetween, formatDate, isValidDate } from '../../utils/date' import { formatBalance, formatPercentage, truncateText } from '../../utils/formatting' +import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl' import { useLoan, useNftDocumentId } from '../../utils/useLoans' import { useMetadata } from '../../utils/useMetadata' import { useCentNFT } from '../../utils/useNFTs' import { useCanBorrowAsset } from '../../utils/usePermissions' import { usePodDocument } from '../../utils/usePodDocument' -import { useBorrowerAssetTransactions, usePool, usePoolMetadata } from '../../utils/usePools' +import { useAssetSnapshots, useBorrowerAssetTransactions, usePool, usePoolMetadata } from '../../utils/usePools' import { FinanceForm } from './FinanceForm' import { FinancingRepayment } from './FinancingRepayment' import { HoldingsValues } from './HoldingsValues' @@ -108,6 +112,23 @@ function Loan() { const metadataIsLoading = poolMetadataIsLoading || nftMetadataIsLoading const borrowerAssetTransactions = useBorrowerAssetTransactions(poolId, loanId) + const { assetSnapshots: showAssetSnapshots } = useDebugFlags() + const assetSnapshots = useAssetSnapshots(poolId, loanId) + + const dataUrl: any = React.useMemo(() => { + if (!assetSnapshots || !assetSnapshots?.length) { + return undefined + } + + const formatted = assetSnapshots.map((snapshot) => { + return { + ...snapshot, + } + }) + + return getCSVDownloadUrl(formatted as any) + }, [assetSnapshots, pool.currency.symbol]) + const currentFace = loan?.pricing && 'outstandingQuantity' in loan.pricing ? loan.pricing.outstandingQuantity.toDecimal().mul(loan.pricing.notional.toDecimal()) @@ -291,7 +312,22 @@ function Loan() { ) : null} {'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'oracle' && ( - Holdings}> + Holdings} + headerRight={ + showAssetSnapshots && ( + + Timeseries + + ) + } + > cent.pools.getAssetSnapshots([poolId, loanId, from, to]), + { + enabled: !poolId.startsWith('0x'), + } + ) + + return result +} + export function usePoolFees(poolId: string) { const [result] = useCentrifugeQuery(['poolFees', poolId], (cent) => cent.pools.getPoolFees([poolId]), { enabled: !poolId.startsWith('0x'), diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index 9682e6d27a..82470dfa56 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -14,6 +14,7 @@ import { AssetType, InvestorTransactionType, PoolFeeTransactionType, + SubqueryAssetSnapshot, SubqueryAssetTransaction, SubqueryCurrencyBalances, SubqueryInvestorTransaction, @@ -787,6 +788,25 @@ export type AssetTransaction = { } } +export type AssetSnapshot = { + timestamp: Date + asset: { + id: string + name: string + metadata: string + type: AssetType + } + presentValue: CurrencyBalance | undefined + outstandingPrincipal: CurrencyBalance | undefined + outstandingInterest: CurrencyBalance | undefined + outstandingDebt: CurrencyBalance | undefined + outstandingQuantity: CurrencyBalance | undefined + totalBorrowed: CurrencyBalance | undefined + totalRepaidPrincipal: CurrencyBalance | undefined + totalRepaidInterest: CurrencyBalance | undefined + totalRepaidUnscheduled: CurrencyBalance | undefined +} + export type PoolFeeTransaction = { id: string timestamp: Date @@ -2775,6 +2795,77 @@ export function getPoolsModule(inst: Centrifuge) { ) } + function getAssetSnapshots(args: [poolId: string, loanId: string, from?: Date, to?: Date]) { + const [poolId, loanId, from, to] = args + + const $query = inst.getSubqueryObservable<{ + assetSnapshots: { nodes: SubqueryAssetSnapshot[] } + }>( + `query($assetId: String!, $from: Datetime!, $to: Datetime!) { + assetSnapshots( + first: 1000, + orderBy: TIMESTAMP_ASC, + filter: { + assetId: { equalTo: $assetId }, + timestamp: { greaterThan: $from, lessThan: $to } + } + ) { + nodes { + assetId + timestamp + presentValue + outstandingPrincipal + outstandingInterest + outstandingDebt + outstandingQuantity + totalBorrowed + totalRepaidPrincipal + totalRepaidInterest + totalRepaidUnscheduled + } + } + } + `, + { + assetId: `${poolId}-${loanId}`, + from: from ? from.toISOString() : getDateYearsFromNow(-10).toISOString(), + to: to ? to.toISOString() : new Date().toISOString(), + }, + false + ) + + return $query.pipe( + switchMap(() => combineLatest([$query, getPoolCurrency([poolId])])), + map(([data, currency]) => { + return data!.assetSnapshots.nodes.map((tx) => ({ + ...tx, + presentValue: tx.presentValue ? new CurrencyBalance(tx.presentValue, currency.decimals) : undefined, + outstandingPrincipal: tx.outstandingPrincipal + ? new CurrencyBalance(tx.outstandingPrincipal, currency.decimals) + : undefined, + outstandingInterest: tx.outstandingInterest + ? new CurrencyBalance(tx.outstandingInterest, currency.decimals) + : undefined, + outstandingDebt: tx.outstandingDebt ? new CurrencyBalance(tx.outstandingDebt, currency.decimals) : undefined, + outstandingQuantity: tx.outstandingQuantity + ? new CurrencyBalance(tx.outstandingQuantity, currency.decimals) + : undefined, + totalBorrowed: tx.totalBorrowed ? new CurrencyBalance(tx.totalBorrowed, currency.decimals) : undefined, + totalRepaidPrincipal: tx.totalRepaidPrincipal + ? new CurrencyBalance(tx.totalRepaidPrincipal, currency.decimals) + : undefined, + totalRepaidInterest: tx.totalRepaidInterest + ? new CurrencyBalance(tx.totalRepaidInterest, currency.decimals) + : undefined, + totalRepaidUnscheduled: tx.totalRepaidUnscheduled + ? new CurrencyBalance(tx.totalRepaidUnscheduled, currency.decimals) + : undefined, + timestamp: new Date(`${tx.timestamp}+00:00`), + })) satisfies AssetSnapshot[] + }) + ) + } + function getInvestors(args: [poolId: string, trancheId?: string]) { const [poolId, trancheId] = args const $query = inst.getApi().pipe( @@ -3806,6 +3897,7 @@ export function getPoolsModule(inst: Centrifuge) { getInvestorTransactions, getAssetTransactions, getFeeTransactions, + getAssetSnapshots, getNativeCurrency, getCurrencies, getDailyTrancheStates, diff --git a/centrifuge-js/src/types/subquery.ts b/centrifuge-js/src/types/subquery.ts index 427d122f7e..90fd7af9b3 100644 --- a/centrifuge-js/src/types/subquery.ts +++ b/centrifuge-js/src/types/subquery.ts @@ -110,6 +110,26 @@ export type SubqueryAssetTransaction = { } } +export type SubqueryAssetSnapshot = { + __typename?: 'AssetSnapshot' + asset: { + id: string + metadata: string + name: string + type: AssetType + } + timestamp: string + presentValue: string + outstandingPrincipal: string + outstandingInterest: string + outstandingDebt: string + outstandingQuantity: string + totalBorrowed: string + totalRepaidPrincipal: string + totalRepaidInterest: string + totalRepaidUnscheduled: string +} + export type PoolFeeTransactionType = 'PROPOSED' | 'ADDED' | 'REMOVED' | 'CHARGED' | 'UNCHARGED' | 'PAID' | 'ACCRUED' export type SubqueryPoolFeeTransaction = { From f34fd0e942258ca0484230613b31f6b3cb0732f3 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Wed, 29 May 2024 12:45:49 +0200 Subject: [PATCH 08/10] fixes --- centrifuge-app/src/components/Report/FeeTransactions.tsx | 2 +- centrifuge-app/src/components/Report/TokenPrice.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/centrifuge-app/src/components/Report/FeeTransactions.tsx b/centrifuge-app/src/components/Report/FeeTransactions.tsx index fa860f1d91..950cb44bfb 100644 --- a/centrifuge-app/src/components/Report/FeeTransactions.tsx +++ b/centrifuge-app/src/components/Report/FeeTransactions.tsx @@ -39,7 +39,7 @@ export function FeeTransactions({ pool }: { pool: Pool }) { formatter: noop, }, { - header: 'Currency ammount', + header: 'Currency amount', align: 'right', csvOnly: false, formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, pool.currency.symbol, 5) : '-'), diff --git a/centrifuge-app/src/components/Report/TokenPrice.tsx b/centrifuge-app/src/components/Report/TokenPrice.tsx index 623dfd5265..fbdfd7e3eb 100644 --- a/centrifuge-app/src/components/Report/TokenPrice.tsx +++ b/centrifuge-app/src/components/Report/TokenPrice.tsx @@ -100,7 +100,7 @@ export function TokenPrice({ pool }: { pool: Pool }) { name: `\u00A0 \u00A0 ${token.currency.name.split(' ').at(-1)} tranche`, value: poolStates?.map((state) => state.tranches[token.id].tokenSupply.toFloat()) || [], heading: false, - formatter: (v: any) => formatBalance(v, '', 5), + formatter: (v: any) => formatBalance(v, '', 2), })) || []), ] }, [poolStates, pool]) From dd5bcfcdb31495892bc53ecb326972b69a022cbe Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Wed, 29 May 2024 13:51:34 +0200 Subject: [PATCH 09/10] Reports feedback (#2159) --- .../src/components/Report/AssetList.tsx | 54 ++++++++++++++++--- .../components/Report/AssetTransactions.tsx | 4 +- .../src/components/Report/FeeTransactions.tsx | 2 +- .../src/components/Report/InvestorList.tsx | 6 +-- .../Report/InvestorTransactions.tsx | 6 +-- .../src/components/Report/PoolBalance.tsx | 4 +- .../src/components/Report/TokenPrice.tsx | 2 +- 7 files changed, 59 insertions(+), 19 deletions(-) diff --git a/centrifuge-app/src/components/Report/AssetList.tsx b/centrifuge-app/src/components/Report/AssetList.tsx index 387bf2c2fd..0039543695 100644 --- a/centrifuge-app/src/components/Report/AssetList.tsx +++ b/centrifuge-app/src/components/Report/AssetList.tsx @@ -5,13 +5,19 @@ import { formatDate } from '../../utils/date' import { formatBalance, formatPercentage } from '../../utils/formatting' import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl' import { useLoans } from '../../utils/useLoans' -import { DataTable } from '../DataTable' +import { DataTable, SortableTableHeader } from '../DataTable' import { Spinner } from '../Spinner' import { ReportContext } from './ReportContext' import { UserFeedback } from './UserFeedback' import type { TableDataRow } from './index' const noop = (v: any) => v +const valuationLabels = { + discountedCashFlow: 'Non-fungible asset - DCF', + outstandingDebt: 'Non-fungible asset - at par', + oracle: 'Fungible asset - external pricing', + cash: 'Cash', +} export function AssetList({ pool }: { pool: Pool }) { const loans = useLoans(pool.id) as Loan[] @@ -31,11 +37,25 @@ export function AssetList({ pool }: { pool: Pool }) { csvOnly: false, formatter: noop, }, + { + header: 'Value', + align: 'right', + csvOnly: false, + sortable: true, + formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, symbol, 2) : '-'), + }, + { + header: 'Value currency', + align: 'left', + csvOnly: true, + formatter: noop, + }, { header: 'Outstanding', align: 'right', csvOnly: false, - formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, symbol, 5) : '-'), + sortable: true, + formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, symbol, 2) : '-'), }, { header: 'Outstanding currency', @@ -47,7 +67,8 @@ export function AssetList({ pool }: { pool: Pool }) { header: 'Total financed', align: 'right', csvOnly: false, - formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, symbol, 5) : '-'), + sortable: true, + formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, symbol, 2) : '-'), }, { header: 'Total financed currency', @@ -58,8 +79,9 @@ export function AssetList({ pool }: { pool: Pool }) { { header: 'Total repaid', align: 'right', + sortable: true, csvOnly: false, - formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, symbol, 5) : '-'), + formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, symbol, 2) : '-'), }, { header: 'Total repaid currency', @@ -70,6 +92,7 @@ export function AssetList({ pool }: { pool: Pool }) { { header: 'Financing date', align: 'left', + sortable: true, csvOnly: false, formatter: (v: any) => (v !== '-' ? formatDate(v) : v), }, @@ -77,20 +100,28 @@ export function AssetList({ pool }: { pool: Pool }) { header: 'Maturity date', align: 'left', csvOnly: false, + sortable: true, formatter: formatDate, }, { header: 'Interest rate', align: 'left', csvOnly: false, - formatter: (v: any) => (typeof v === 'number' ? formatPercentage(v, true, undefined, 5) : '-'), + formatter: (v: any) => (typeof v === 'number' ? formatPercentage(v, true, undefined, 2) : '-'), + }, + { + header: 'Valuation method', + align: 'left', + csvOnly: false, + formatter: noop, }, ] const columns = columnConfig .map((col, index) => ({ align: col.align, - header: col.header, + header: col.sortable ? : col.header, + sortKey: col.sortable ? `value[${index}]` : undefined, cell: (row: TableDataRow) => {col.formatter((row.value as any)[index])}, csvOnly: col.csvOnly, })) @@ -107,7 +138,15 @@ export function AssetList({ pool }: { pool: Pool }) { name: '', value: [ loan.id, - loan.status === 'Closed' ? 'Repaid' : new Date() > new Date(loan.pricing.maturityDate) ? 'Overdue' : 'Active', + loan.status === 'Closed' + ? 'Repaid' + : new Date() > new Date(loan.pricing.maturityDate) + ? loan.outstandingDebt.isZero() + ? 'Repaid' + : 'Overdue' + : 'Active', + 'presentValue' in loan ? loan.presentValue.toFloat() : '-', + symbol, 'outstandingDebt' in loan ? loan.outstandingDebt.toFloat() : '-', symbol, 'totalBorrowed' in loan ? loan.totalBorrowed.toFloat() : '-', @@ -117,6 +156,7 @@ export function AssetList({ pool }: { pool: Pool }) { 'originationDate' in loan ? loan.originationDate : '-', loan.pricing.maturityDate, 'interestRate' in loan.pricing ? loan.pricing.interestRate.toPercent().toNumber() : '-', + valuationLabels[loan.pricing.valuationMethod], ], heading: false, })) diff --git a/centrifuge-app/src/components/Report/AssetTransactions.tsx b/centrifuge-app/src/components/Report/AssetTransactions.tsx index e2e8953366..b7aa721936 100644 --- a/centrifuge-app/src/components/Report/AssetTransactions.tsx +++ b/centrifuge-app/src/components/Report/AssetTransactions.tsx @@ -55,12 +55,12 @@ export function AssetTransactions({ pool }: { pool: Pool }) { header: 'Currency amount', align: 'left', csvOnly: false, - formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, pool.currency.symbol, 5) : '-'), + formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, pool.currency.symbol, 2) : '-'), }, { header: 'Currency', align: 'right', - csvOnly: false, + csvOnly: true, formatter: noop, }, { diff --git a/centrifuge-app/src/components/Report/FeeTransactions.tsx b/centrifuge-app/src/components/Report/FeeTransactions.tsx index 950cb44bfb..2021b7daa7 100644 --- a/centrifuge-app/src/components/Report/FeeTransactions.tsx +++ b/centrifuge-app/src/components/Report/FeeTransactions.tsx @@ -42,7 +42,7 @@ export function FeeTransactions({ pool }: { pool: Pool }) { header: 'Currency amount', align: 'right', csvOnly: false, - formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, pool.currency.symbol, 5) : '-'), + formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, pool.currency.symbol, 2) : '-'), }, { header: 'Currency', diff --git a/centrifuge-app/src/components/Report/InvestorList.tsx b/centrifuge-app/src/components/Report/InvestorList.tsx index 875b7501fb..c22812f1d5 100644 --- a/centrifuge-app/src/components/Report/InvestorList.tsx +++ b/centrifuge-app/src/components/Report/InvestorList.tsx @@ -40,7 +40,7 @@ export function InvestorList({ pool }: { pool: Pool }) { header: 'Position', align: 'right', csvOnly: false, - formatter: (v: any, row: any) => (typeof v === 'number' ? formatBalance(v, row.token.currency.symbol, 5) : '-'), + formatter: (v: any, row: any) => (typeof v === 'number' ? formatBalance(v, row.token.currency.symbol, 2) : '-'), }, { header: 'Pool %', @@ -59,7 +59,7 @@ export function InvestorList({ pool }: { pool: Pool }) { header: 'Pending invest order', align: 'right', csvOnly: false, - formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, pool.currency.symbol, 5) : '-'), + formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, pool.currency.symbol, 2) : '-'), }, { header: 'Pending invest order currency', @@ -71,7 +71,7 @@ export function InvestorList({ pool }: { pool: Pool }) { header: 'Pending redeem order', align: 'right', csvOnly: false, - formatter: (v: any, row: any) => (typeof v === 'number' ? formatBalance(v, row.token.currency.symbol, 5) : '-'), + formatter: (v: any, row: any) => (typeof v === 'number' ? formatBalance(v, row.token.currency.symbol, 2) : '-'), }, { header: 'Pending redeem order currency', diff --git a/centrifuge-app/src/components/Report/InvestorTransactions.tsx b/centrifuge-app/src/components/Report/InvestorTransactions.tsx index cdbe68f1f8..111525bf0f 100644 --- a/centrifuge-app/src/components/Report/InvestorTransactions.tsx +++ b/centrifuge-app/src/components/Report/InvestorTransactions.tsx @@ -71,7 +71,7 @@ export function InvestorTransactions({ pool }: { pool: Pool }) { align: 'right', sortable: true, csvOnly: false, - formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, pool.currency.symbol, 5) : '-'), + formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, pool.currency.symbol, 2) : '-'), }, { header: 'Currency', @@ -85,7 +85,7 @@ export function InvestorTransactions({ pool }: { pool: Pool }) { align: 'right', sortable: true, csvOnly: false, - formatter: (v: any, row: any) => (typeof v === 'number' ? formatBalance(v, row[9], 5) : '-'), + formatter: (v: any, row: any) => (typeof v === 'number' ? formatBalance(v, row[9], 2) : '-'), }, { header: 'Token currency', @@ -99,7 +99,7 @@ export function InvestorTransactions({ pool }: { pool: Pool }) { align: 'right', sortable: true, csvOnly: false, - formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, pool.currency.symbol, 5) : '-'), + formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, pool.currency.symbol, 6) : '-'), }, { header: 'Price currency', diff --git a/centrifuge-app/src/components/Report/PoolBalance.tsx b/centrifuge-app/src/components/Report/PoolBalance.tsx index e90615b11d..74c397f7c3 100644 --- a/centrifuge-app/src/components/Report/PoolBalance.tsx +++ b/centrifuge-app/src/components/Report/PoolBalance.tsx @@ -59,7 +59,7 @@ export function PoolBalance({ pool }: { pool: Pool }) { {(row.value as any)[index] !== '' && (row.formatter ? row.formatter((row.value as any)[index]) - : formatBalance((row.value as any)[index], pool.currency.symbol, 5))} + : formatBalance((row.value as any)[index], pool.currency.symbol, 2))} ), width: '200px', @@ -91,7 +91,7 @@ export function PoolBalance({ pool }: { pool: Pool }) { return change < 0 ? change : `+${change}` }) || [], heading: false, - formatter: (v: any) => `${v < 0 ? '' : '+'}${formatPercentage(v, true, {}, 5)}`, + formatter: (v: any) => `${v < 0 ? '' : '+'}${formatPercentage(v, true, {}, 2)}`, }, { name: 'Asset value', diff --git a/centrifuge-app/src/components/Report/TokenPrice.tsx b/centrifuge-app/src/components/Report/TokenPrice.tsx index fbdfd7e3eb..f395826b7a 100644 --- a/centrifuge-app/src/components/Report/TokenPrice.tsx +++ b/centrifuge-app/src/components/Report/TokenPrice.tsx @@ -86,7 +86,7 @@ export function TokenPrice({ pool }: { pool: Pool }) { state.tranches[token.id]?.price ? state.tranches[token.id].price!.toFloat() : 1 ) || [], heading: false, - formatter: (v: any) => formatBalance(v, pool.currency.symbol, 5), + formatter: (v: any) => formatBalance(v, pool.currency.symbol, 6), })) || []), { name: 'Token supply', From f2a9a889910779c367448d8a6ba6d4c3bdd8234b Mon Sep 17 00:00:00 2001 From: Jeroen <1748621+hieronx@users.noreply.github.com> Date: Wed, 29 May 2024 15:38:21 +0200 Subject: [PATCH 10/10] Change from date (#2162) --- centrifuge-app/src/components/Report/ReportContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/centrifuge-app/src/components/Report/ReportContext.tsx b/centrifuge-app/src/components/Report/ReportContext.tsx index 5024aff63a..a725e9e59c 100644 --- a/centrifuge-app/src/components/Report/ReportContext.tsx +++ b/centrifuge-app/src/components/Report/ReportContext.tsx @@ -58,7 +58,7 @@ export function ReportContextProvider({ children }: { children: React.ReactNode // Global filters const [startDate, setStartDate] = React.useState( - new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10) + new Date(new Date().getFullYear(), 0, 1, 1).toISOString().slice(0, 10) ) const [endDate, setEndDate] = React.useState(new Date().toISOString().slice(0, 10)) const [report, setReport] = React.useState('investor-tx')