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/Charts/CashflowsChart.tsx b/centrifuge-app/src/components/Charts/CashflowsChart.tsx
index bdf281cb55..f2cdf76b0f 100644
--- a/centrifuge-app/src/components/Charts/CashflowsChart.tsx
+++ b/centrifuge-app/src/components/Charts/CashflowsChart.tsx
@@ -39,16 +39,14 @@ export const CashflowsChart = ({ poolStates, pool }: Props) => {
const data = React.useMemo(
() =>
poolStates?.map((day) => {
- const purchases = day.sumBorrowedAmountByPeriod
- ? new CurrencyBalance(day.sumBorrowedAmountByPeriod, pool.currency.decimals).toDecimal().toNumber()
- : 0
- const principalRepayments = day.sumRepaidAmountByPeriod
- ? new CurrencyBalance(day.sumRepaidAmountByPeriod, pool.currency.decimals).toDecimal().toNumber()
- : 0
- const interest = day.sumInterestRepaidAmountByPeriod
- ? new CurrencyBalance(day.sumInterestRepaidAmountByPeriod, pool.currency.decimals).toDecimal().toNumber()
- : 0
- return { name: new Date(day.timestamp), purchases, principalRepayments, interest }
+ const purchases = new CurrencyBalance(day.sumBorrowedAmountByPeriod, pool.currency.decimals).toFloat()
+ const principalRepayments = new CurrencyBalance(day.sumRepaidAmountByPeriod, pool.currency.decimals).toFloat()
+
+ const interest = new CurrencyBalance(day.sumInterestRepaidAmountByPeriod, pool.currency.decimals).toFloat()
+ const fees =
+ 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]
)
@@ -59,6 +57,7 @@ export const CashflowsChart = ({ poolStates, pool }: Props) => {
totalPurchases: data.reduce((acc, cur) => acc + cur.purchases, 0),
interest: data.reduce((acc, cur) => acc + cur.interest, 0),
principalRepayments: data.reduce((acc, cur) => acc + cur.principalRepayments, 0),
+ fees: data.reduce((acc, cur) => acc + cur.fees, 0),
}
const getXAxisInterval = () => {
@@ -152,8 +151,8 @@ export const CashflowsChart = ({ poolStates, pool }: Props) => {
/>
-
- {/* */}
+
+
@@ -168,6 +167,7 @@ function CustomLegend({
totalPurchases: number
principalRepayments: number
interest: number
+ fees: number
}
}) {
const theme = useTheme()
@@ -195,7 +195,7 @@ function CustomLegend({
borderLeftWidth="3px"
pl={1}
borderLeftStyle="solid"
- borderLeftColor={theme.colors.borderPrimary}
+ borderLeftColor={theme.colors.borderSecondary}
gap="4px"
>
@@ -205,7 +205,7 @@ function CustomLegend({
{formatBalance(data.interest, 'USD', 2)}
- {/*
Fees
- {formatBalance(0, 'USD', 2)}
- */}
+ {formatBalance(data.fees, 'USD', 2)}
+
)
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/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/InvestRedeem/InvestRedeemLiquidityPoolsProvider.tsx b/centrifuge-app/src/components/InvestRedeem/InvestRedeemLiquidityPoolsProvider.tsx
index 41bd365988..b0b7b90c16 100644
--- a/centrifuge-app/src/components/InvestRedeem/InvestRedeemLiquidityPoolsProvider.tsx
+++ b/centrifuge-app/src/components/InvestRedeem/InvestRedeemLiquidityPoolsProvider.tsx
@@ -22,7 +22,7 @@ export function InvestRedeemLiquidityPoolsProvider({ poolId, trancheId, children
const centAddress = useAddress('substrate')
const evmAddress = useAddress('evm')
const {
- evm: { isSmartContractWallet },
+ evm: { isSmartContractWallet, selectedWallet },
} = useWallet()
const consts = useCentrifugeConsts()
const [lpIndex, setLpIndex] = React.useState(0)
@@ -144,7 +144,7 @@ export function InvestRedeemLiquidityPoolsProvider({ poolId, trancheId, children
}
}, [lps])
- const supportsPermits = lpInvest?.currencySupportsPermit && !isSmartContractWallet
+ const supportsPermits = lpInvest?.currencySupportsPermit && !isSmartContractWallet && selectedWallet?.id !== 'finoa'
const state: InvestRedeemState = {
poolId,
diff --git a/centrifuge-app/src/components/PoolFees/ChargeFeesDrawer.tsx b/centrifuge-app/src/components/PoolFees/ChargeFeesDrawer.tsx
index 8618b22f39..efa6bed9c9 100644
--- a/centrifuge-app/src/components/PoolFees/ChargeFeesDrawer.tsx
+++ b/centrifuge-app/src/components/PoolFees/ChargeFeesDrawer.tsx
@@ -8,7 +8,7 @@ import { useLocation, useParams } from 'react-router'
import { CopyToClipboard } from '../../utils/copyToClipboard'
import { Dec } from '../../utils/Decimal'
import { formatBalance, formatBalanceAbbreviated, formatPercentage } from '../../utils/formatting'
-import { usePool, usePoolMetadata } from '../../utils/usePools'
+import { usePool, usePoolFees, usePoolMetadata } from '../../utils/usePools'
import { ButtonGroup } from '../ButtonGroup'
type ChargeFeesProps = {
@@ -19,12 +19,13 @@ type ChargeFeesProps = {
export const ChargeFeesDrawer = ({ onClose, isOpen }: ChargeFeesProps) => {
const { pid: poolId } = useParams<{ pid: string }>()
const pool = usePool(poolId)
+ const poolFees = usePoolFees(poolId)
const { data: poolMetadata } = usePoolMetadata(pool)
const { search } = useLocation()
const params = new URLSearchParams(search)
const feeIndex = params.get('charge')
const feeMetadata = feeIndex ? poolMetadata?.pool?.poolFees?.find((f) => f.id.toString() === feeIndex) : undefined
- const feeChainData = feeIndex ? pool?.poolFees?.find((f) => f.id.toString() === feeIndex) : undefined
+ const feeChainData = feeIndex ? poolFees?.find((f) => f.id.toString() === feeIndex) : undefined
const maxCharge = feeChainData?.amounts.percentOfNav.toDecimal().mul(pool.nav.aum.toDecimal()).div(100)
const [updateCharge, setUpdateCharge] = React.useState(false)
const address = useAddress()
diff --git a/centrifuge-app/src/components/PoolFees/EditFeesDrawer.tsx b/centrifuge-app/src/components/PoolFees/EditFeesDrawer.tsx
index 293faae74f..929e1c1f88 100644
--- a/centrifuge-app/src/components/PoolFees/EditFeesDrawer.tsx
+++ b/centrifuge-app/src/components/PoolFees/EditFeesDrawer.tsx
@@ -4,12 +4,14 @@ import {
Box,
Button,
Drawer,
+ Flex,
Grid,
IconButton,
IconCopy,
IconMinusCircle,
IconPlusCircle,
NumberInput,
+ Select,
Shelf,
Stack,
Text,
@@ -19,11 +21,11 @@ import { Field, FieldArray, FieldProps, Form, FormikProvider, useFormik } from '
import React from 'react'
import { useParams } from 'react-router'
import { Dec } from '../../utils/Decimal'
-import { isEvmAddress, isSubstrateAddress } from '../../utils/address'
import { copyToClipboard } from '../../utils/copyToClipboard'
import { formatPercentage } from '../../utils/formatting'
import { usePoolAdmin, useSuitableAccounts } from '../../utils/usePermissions'
-import { usePool, usePoolMetadata } from '../../utils/usePools'
+import { usePool, usePoolFees, usePoolMetadata } from '../../utils/usePools'
+import { combine, max, positiveNumber, required, substrateAddress } from '../../utils/validation'
import { ButtonGroup } from '../ButtonGroup'
type ChargeFeesProps = {
@@ -31,81 +33,56 @@ type ChargeFeesProps = {
isOpen: boolean
}
+type FormValues = {
+ poolFees: {
+ feeName: string
+ percentOfNav: number | ''
+ receivingAddress: string
+ feeId: number | undefined
+ type: 'fixed' | 'chargedUpTo'
+ }[]
+}
+
export const EditFeesDrawer = ({ onClose, isOpen }: ChargeFeesProps) => {
const { pid: poolId } = useParams<{ pid: string }>()
const pool = usePool(poolId)
+ const poolFees = usePoolFees(poolId)
const { data: poolMetadata, isLoading } = usePoolMetadata(pool)
const poolAdmin = usePoolAdmin(poolId)
const account = useSuitableAccounts({ poolId, poolRole: ['PoolAdmin'] })[0]
const initialFormData = React.useMemo(() => {
- return pool.poolFees
- ?.filter((poolFees) => poolFees.type !== 'fixed')
- .map((feeChainData) => {
+ return poolFees
+ ?.filter((poolFees) => !('root' in poolFees.editor))
+ ?.map((feeChainData) => {
const feeMetadata = poolMetadata?.pool?.poolFees?.find((f) => f.id === feeChainData.id)
return {
- percentOfNav: parseFloat(feeChainData?.amounts.percentOfNav.toDecimal().toFixed(2)) ?? undefined,
+ percentOfNav: feeChainData?.amounts.percentOfNav.toPercent().toNumber() ?? undefined,
feeName: feeMetadata?.name || '',
receivingAddress: feeChainData?.destination || '',
- feeId: feeChainData?.id || 0,
+ feeId: feeChainData.id || 0,
+ type: feeChainData.type,
}
})
- }, [pool.poolFees, poolMetadata?.pool?.poolFees])
+ }, [poolFees, poolMetadata?.pool?.poolFees])
React.useEffect(() => {
- if (!isLoading) {
+ if (!isLoading && isOpen) {
form.setValues({ poolFees: initialFormData || [] })
}
- }, [isLoading, initialFormData])
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isLoading, initialFormData, isOpen])
const { execute: updateFeesTx, isLoading: updateFeeTxLoading } = useCentrifugeTransaction(
'Update fees',
(cent) => cent.pools.updateFees
)
- const form = useFormik<{
- poolFees: { feeName: string; percentOfNav?: number; receivingAddress: string; feeId: number }[]
- }>({
+ const form = useFormik({
initialValues: {
poolFees: initialFormData || [],
},
validateOnChange: false,
- validate(values) {
- let errors: { poolFees?: { feeName?: string; percentOfNav?: string; receivingAddress?: string }[] } = {}
- values.poolFees.forEach((fee, index) => {
- if (!fee.feeName) {
- errors.poolFees = errors.poolFees || []
- errors.poolFees[index] = errors.poolFees[index] || {}
- errors.poolFees[index].feeName = 'Required'
- }
- if (!fee.percentOfNav) {
- errors.poolFees = errors.poolFees || []
- errors.poolFees[index] = errors.poolFees[index] || {}
- errors.poolFees[index].percentOfNav = 'Required'
- }
- if (fee.percentOfNav && fee.percentOfNav <= 0) {
- errors.poolFees = errors.poolFees || []
- errors.poolFees[index] = errors.poolFees[index] || {}
- errors.poolFees[index].percentOfNav = 'Must be greater than 0%'
- }
- if (fee.percentOfNav && fee.percentOfNav >= 100) {
- errors.poolFees = errors.poolFees || []
- errors.poolFees[index] = errors.poolFees[index] || {}
- errors.poolFees[index].percentOfNav = 'Must be less than 100%'
- }
- if (!fee.receivingAddress) {
- errors.poolFees = errors.poolFees || []
- errors.poolFees[index] = errors.poolFees[index] || {}
- errors.poolFees[index].receivingAddress = 'Required'
- }
- if (fee.receivingAddress && !isEvmAddress(fee.receivingAddress) && !isSubstrateAddress(fee.receivingAddress)) {
- errors.poolFees = errors.poolFees || []
- errors.poolFees[index] = errors.poolFees[index] || {}
- errors.poolFees[index].receivingAddress = 'Invalid address'
- }
- })
- return errors
- },
onSubmit: (values) => {
if (!poolMetadata) throw new Error('poolMetadata not found')
// find fees that have been updated so they can be removed (and re-added)
@@ -119,7 +96,8 @@ export const EditFeesDrawer = ({ onClose, isOpen }: ChargeFeesProps) => {
return (
initialFee.feeName !== fee?.feeName ||
parseFloat(initialFee?.percentOfNav?.toString() || '0') !== parseFloat(newPercent) ||
- initialFee.receivingAddress !== fee?.receivingAddress
+ initialFee.receivingAddress !== fee?.receivingAddress ||
+ initialFee.type !== fee?.type
)
})
.map((initialFee) => initialFee.feeId) || []
@@ -134,7 +112,8 @@ export const EditFeesDrawer = ({ onClose, isOpen }: ChargeFeesProps) => {
return !(
initialFee?.feeName === fee.feeName &&
parseFloat(initialFee?.percentOfNav?.toString() || '0') === parseFloat(newPercent) &&
- initialFee?.receivingAddress === fee.receivingAddress
+ initialFee?.receivingAddress === fee.receivingAddress &&
+ initialFee?.type === fee.type
)
})
.map((fee) => {
@@ -145,14 +124,13 @@ export const EditFeesDrawer = ({ onClose, isOpen }: ChargeFeesProps) => {
destination: fee.receivingAddress,
amount: Rate.fromPercent(Dec(fee?.percentOfNav || 0)),
feeId: fee.feeId,
- feeType: 'chargedUpTo',
+ feeType: fee.type,
limit: 'ShareOfPortfolioValuation',
account: account.actingAddress,
feePosition: 'Top of waterfall',
},
}
})
-
updateFeesTx([add, remove, poolId, poolMetadata as PoolMetadata], { account })
},
})
@@ -164,9 +142,9 @@ export const EditFeesDrawer = ({ onClose, isOpen }: ChargeFeesProps) => {
Fee structure
-
- {pool.poolFees
- ?.filter((poolFees) => poolFees.type === 'fixed')
+
+ {poolFees
+ ?.filter((poolFees) => 'root' in poolFees.editor)
.map((feeChainData, index) => {
const feeMetadata = poolMetadata?.pool?.poolFees?.find((f) => f.id === feeChainData.id)
return (
@@ -200,40 +178,80 @@ export const EditFeesDrawer = ({ onClose, isOpen }: ChargeFeesProps) => {
- Direct charge
+ Other fees
{form.values.poolFees.map((values, index) => {
return (
-
-
+
+
+
+ {({ field, meta }: FieldProps) => {
+ return (
+
+ )
+ }}
+
-
- {({ field, meta }: FieldProps) => {
- return (
-
+
+ {({ field, form, meta }: FieldProps) => (
+
-
- {({ 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/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/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..0039543695 100644
--- a/centrifuge-app/src/components/Report/AssetList.tsx
+++ b/centrifuge-app/src/components/Report/AssetList.tsx
@@ -2,40 +2,130 @@ 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 { 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'
-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
+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[]
- 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: '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,
+ sortable: true,
+ formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, symbol, 2) : '-'),
+ },
+ {
+ header: 'Outstanding currency',
+ align: 'left',
+ csvOnly: true,
+ formatter: noop,
+ },
+ {
+ header: 'Total financed',
+ align: 'right',
+ csvOnly: false,
+ sortable: true,
+ formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, symbol, 2) : '-'),
+ },
+ {
+ header: 'Total financed currency',
+ align: 'left',
+ csvOnly: true,
+ formatter: noop,
+ },
+ {
+ header: 'Total repaid',
+ align: 'right',
+ sortable: true,
+ csvOnly: false,
+ formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, symbol, 2) : '-'),
+ },
+ {
+ header: 'Total repaid currency',
+ align: 'left',
+ csvOnly: true,
+ formatter: noop,
+ },
+ {
+ header: 'Financing date',
+ align: 'left',
+ sortable: true,
+ csvOnly: false,
+ formatter: (v: any) => (v !== '-' ? formatDate(v) : v),
+ },
+ {
+ 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, 2) : '-'),
+ },
+ {
+ header: 'Valuation method',
+ align: 'left',
+ csvOnly: false,
+ formatter: noop,
+ },
+ ]
+
+ const columns = columnConfig
+ .map((col, index) => ({
+ align: col.align,
+ header: col.sortable ? : col.header,
+ sortKey: col.sortable ? `value[${index}]` : undefined,
+ 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 +138,52 @@ 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)
+ ? 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() : '-',
+ 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() : '-',
+ valuationLabels[loan.pricing.valuationMethod],
],
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..b7aa721936 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: false,
+ formatter: formatDate,
+ },
+ {
+ header: 'Transaction type',
+ align: 'right',
+ csvOnly: false,
+ formatter: noop,
+ },
+ {
+ header: 'Currency amount',
+ align: 'left',
+ csvOnly: false,
+ formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, pool.currency.symbol, 2) : '-'),
+ },
+ {
+ header: 'Currency',
+ align: 'right',
+ csvOnly: true,
+ 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 ?? `Asset ${tx.asset.id.split('-').at(-1)!}`,
+ 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..2021b7daa7 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 amount',
+ align: 'right',
+ csvOnly: false,
+ formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, pool.currency.symbol, 2) : '-'),
+ },
+ {
+ 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
deleted file mode 100644
index f0b7d51378..0000000000
--- a/centrifuge-app/src/components/Report/Holders.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-import { Pool } from '@centrifuge/centrifuge-js'
-import { useCentrifugeUtils } from '@centrifuge/centrifuge-react'
-import { Text } from '@centrifuge/fabric'
-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 { DataTable } from '../DataTable'
-import { Spinner } from '../Spinner'
-import type { TableDataRow } from './index'
-import { ReportContext } from './ReportContext'
-import { UserFeedback } from './UserFeedback'
-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 utils = useCentrifugeUtils()
- const holders = useHolders(pool.id, activeTranche === 'all' ? undefined : activeTranche)
-
- 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])
-
- const dataUrl = React.useMemo(() => {
- 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}-holders.csv`,
- }
- : undefined
- )
-
- return () => setCsvData(undefined)
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [dataUrl, pool.id])
-
- if (!holders) {
- return
- }
-
- return data.length > 0 ? :
-}
diff --git a/centrifuge-app/src/components/Report/InvestorList.tsx b/centrifuge-app/src/components/Report/InvestorList.tsx
new file mode 100644
index 0000000000..c22812f1d5
--- /dev/null
+++ b/centrifuge-app/src/components/Report/InvestorList.tsx
@@ -0,0 +1,174 @@
+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, formatPercentage } from '../../utils/formatting'
+import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl'
+import { useInvestorList } from '../../utils/usePools'
+import { DataTable, SortableTableHeader } from '../DataTable'
+import { Spinner } from '../Spinner'
+import { ReportContext } from './ReportContext'
+import { UserFeedback } from './UserFeedback'
+import type { TableDataRow } from './index'
+import { copyable } from './utils'
+
+const noop = (v: any) => v
+
+export function InvestorList({ pool }: { pool: Pool }) {
+ const { activeTranche, setCsvData, network, address } = React.useContext(ReportContext)
+
+ const utils = useCentrifugeUtils()
+ const investors = useInvestorList(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, 2) : '-'),
+ },
+ {
+ 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',
+ csvOnly: true,
+ formatter: noop,
+ },
+ {
+ header: 'Pending invest order',
+ align: 'right',
+ csvOnly: false,
+ formatter: (v: any) => (typeof v === 'number' ? formatBalance(v, pool.currency.symbol, 2) : '-'),
+ },
+ {
+ 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, 2) : '-'),
+ },
+ {
+ header: 'Pending redeem order currency',
+ align: 'left',
+ csvOnly: true,
+ formatter: noop,
+ },
+ ]
+
+ const columns = columnConfig
+ .map((col, index) => ({
+ align: col.align,
+ 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)
+
+ const data: TableDataRow[] = React.useMemo(() => {
+ 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) => {
+ if (!network || network === 'all') return true
+ return network === (tx.chainId || 'centrifuge')
+ })
+ .map((investor) => {
+ const token = pool.tranches.find((t) => t.id === investor.trancheId)!
+ return {
+ name: '',
+ value: [
+ (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,
+ investor.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
+ }, [investors, network, pool, address])
+
+ const dataUrl = React.useMemo(() => {
+ if (!data.length) {
+ return
+ }
+
+ const formatted = data
+ .map(({ value }) => value as string[])
+ .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(() => {
+ setCsvData(
+ dataUrl
+ ? {
+ dataUrl,
+ fileName: `${pool.id}-investors.csv`,
+ }
+ : undefined
+ )
+
+ return () => setCsvData(undefined)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [dataUrl, pool.id])
+
+ if (!investors) {
+ return
+ }
+
+ return data.length > 0 ? (
+
+ ) : (
+
+ )
+}
diff --git a/centrifuge-app/src/components/Report/InvestorTransactions.tsx b/centrifuge-app/src/components/Report/InvestorTransactions.tsx
index 62a029e1ee..111525bf0f 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, 2) : '-'),
+ },
+ {
+ 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], 2) : '-'),
+ },
+ {
+ 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, 6) : '-'),
+ },
+ {
+ 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..74c397f7c3 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, 2))}
+
+ ),
+ 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, {}, 2)}`,
},
{
- 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..a725e9e59c 100644
--- a/centrifuge-app/src/components/Report/ReportContext.tsx
+++ b/centrifuge-app/src/components/Report/ReportContext.tsx
@@ -1,24 +1,25 @@
-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'
+ | 'investor-list'
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 +27,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 +51,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(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')
// 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..71cc2f45e3 100644
--- a/centrifuge-app/src/components/Report/ReportFilter.tsx
+++ b/centrifuge-app/src/components/Report/ReportFilter.tsx
@@ -1,40 +1,55 @@
-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 { useDebugFlags } from '../DebugFlags'
-import { GroupBy, InvestorTxType, Report, ReportContext } from './ReportContext'
+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 { GroupBy, Report, ReportContext } from './ReportContext'
+import { formatPoolFeeTransactionType } from './utils'
type ReportFilterProps = {
pool: Pool
}
export function ReportFilter({ pool }: ReportFilterProps) {
- const { holdersReport } = useDebugFlags()
-
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 }] : []),
+ { label: 'Investor list', value: 'investor-list' },
]
return (
@@ -46,12 +61,11 @@ export function ReportFilter({ pool }: ReportFilterProps) {
borderWidth={0}
borderBottomWidth={1}
borderStyle="solid"
- borderColor="borderSecondary"
+ borderColor="borderPrimary"
>