Skip to content

Commit

Permalink
Use Centrifuge SDK in cashflow report
Browse files Browse the repository at this point in the history
  • Loading branch information
sophialittlejohn committed Nov 29, 2024
1 parent cbc95e2 commit e3da824
Show file tree
Hide file tree
Showing 3 changed files with 4,471 additions and 4,098 deletions.
2 changes: 2 additions & 0 deletions centrifuge-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@centrifuge/centrifuge-js": "workspace:*",
"@centrifuge/centrifuge-react": "workspace:*",
"@centrifuge/fabric": "workspace:*",
"@centrifuge/sdk": "file:../../centrifuge-sdk",
"@makerdao/multicall": "^0.12.0",
"@polkadot/react-identicon": "~3.1.4",
"@styled-system/css": "^5.1.5",
Expand All @@ -48,6 +49,7 @@
"styled-components": "^5.3.5",
"styled-system": "^5.1.5",
"typescript": "~5.3.3",
"viem": "^2.21.53",
"vitest": "^1.2.1",
"ws": "^8.18.0"
},
Expand Down
195 changes: 68 additions & 127 deletions centrifuge-app/src/components/Report/CashflowStatement.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import { CurrencyBalance } from '@centrifuge/centrifuge-js'
import { DailyPoolState, Pool } from '@centrifuge/centrifuge-js/dist/modules/pools'
import { Pool } from '@centrifuge/centrifuge-js/dist/modules/pools'
import { formatBalance } from '@centrifuge/centrifuge-react'
import { Text, Tooltip } from '@centrifuge/fabric'
import Centrifuge from '@centrifuge/sdk'
import * as React from 'react'
import { useQuery } from 'react-query'
import { formatDate } from '../../utils/date'
import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl'
import {
useAggregatedPoolStatesByGroup,
usePoolFeeStatesByGroup,
usePoolMetadata,
usePoolStatesByGroup,
} from '../../utils/usePools'
import { usePoolMetadata } from '../../utils/usePools'
import { DataTable } from '../DataTable'
import { DataTableGroup } from '../DataTableGroup'
import { Spinner } from '../Spinner'
Expand All @@ -25,6 +21,11 @@ type Row = TableDataRow & {
bold?: boolean
}

const centrifuge = new Centrifuge({
environment: 'mainnet',
indexerUrl: 'https://api.centrifuge.io/',
})

export function CashflowStatement({ pool }: { pool: Pool }) {
const { startDate, endDate, groupBy, setCsvData, setReportData } = React.useContext(ReportContext)
const { data: poolMetadata } = usePoolMetadata(pool)
Expand Down Expand Up @@ -58,29 +59,26 @@ export function CashflowStatement({ pool }: { pool: Pool }) {
}
}, [groupBy, startDate, endDate, pool.createdAt])

const poolStates = useAggregatedPoolStatesByGroup(
pool.id,
adjustedStartDate,
adjustedEndDate,
groupBy === 'daily' ? 'day' : groupBy
)

const poolStatesNotAggregated = usePoolStatesByGroup(
pool.id,
adjustedStartDate,
adjustedEndDate,
groupBy === 'daily' ? 'day' : groupBy
)

const poolFeeStates = usePoolFeeStatesByGroup(
pool.id,
adjustedStartDate,
adjustedEndDate,
groupBy === 'daily' ? 'day' : groupBy
)
const { data } = useQuery({
queryKey: ['cashflow', pool.id, startDate, endDate, groupBy],
queryFn: async () => {
const sdkPool = await centrifuge.pool(pool.id, pool.metadata)
const group = groupBy === 'day' || groupBy === 'daily' ? 'day' : groupBy
const report = await sdkPool.reports.cashflow({
from: adjustedStartDate.toISOString(),
to: adjustedEndDate.toISOString(),
groupBy: group,
})
if (groupBy === 'day') {
return [report?.[0]]
}
return report
},
enabled: !!pool.id,
})

const columns = React.useMemo(() => {
if (!poolStates || !poolStates.length) {
if (!data || !data.length) {
return []
}

Expand All @@ -100,7 +98,7 @@ export function CashflowStatement({ pool }: { pool: Pool }) {
},
]
.concat(
poolStates.map((state, index) => ({
data?.map((state, index) => ({
align: 'right',
timestamp: state.timestamp,
header: getColumnHeader(state.timestamp, groupBy),
Expand All @@ -110,26 +108,21 @@ export function CashflowStatement({ pool }: { pool: Pool }) {
</Text>
),
width: '170px',
}))
})) || []
)
.concat({
align: 'left',
header: '',
cell: () => <span />,
width: '1fr',
})
}, [poolStates, groupBy])
}, [data, groupBy])

const grossCashflowRecords: Row[] = React.useMemo(() => {
return [
{
name: 'Principal payments',
value:
poolStates?.map(({ poolState }) =>
poolState?.sumPrincipalRepaidAmountByPeriod
.toDecimal()
.sub(poolState.sumRealizedProfitFifoByPeriod.toDecimal())
) || [],
value: data?.map((day) => day.principalPayments.toDecimal()) || [],
heading: false,
formatter: (v: any) => (v ? formatBalance(v, pool.currency.displayName, 2) : ''),
},
Expand All @@ -138,147 +131,107 @@ export function CashflowStatement({ pool }: { pool: Pool }) {
{
name: 'Realized profit / loss',
nameTooltip: 'Based on first-in, first-out calculation of the transactions of each individual asset',
value: poolStates?.map(({ poolState }) => poolState.sumRealizedProfitFifoByPeriod.toDecimal()) || [],
value: data?.map((day) => (day.subtype === 'publicCredit' ? day?.realizedPL?.toDecimal() : 0)) || [],
heading: false,
formatter: (v: any) => (v ? formatBalance(v, pool.currency.displayName, 2) : ''),
},
]
: []),
{
name: 'Interest payments',
value:
poolStates?.map(({ poolState }) =>
poolState.sumInterestRepaidAmountByPeriod
.toDecimal()
.add(poolState.sumUnscheduledRepaidAmountByPeriod.toDecimal())
) || [],
value: data?.map((day) => day.interestPayments.toDecimal()) || [],
heading: false,
formatter: (v: any) => (v ? formatBalance(v, pool.currency.displayName, 2) : ''),
},
{
name: poolMetadata?.pool?.asset.class === 'Private credit' ? 'Asset financings' : 'Asset purchases',
value: poolStates?.map(({ poolState }) => poolState.sumBorrowedAmountByPeriod.toDecimal().neg()) || [],
value: data?.map((day) => day.assetAcquisitions.toDecimal().neg()) || [],
heading: false,
formatter: (v: any) => `${formatBalance(v, pool.currency.displayName, 2)}`,
},
{
name: 'Net cash flow from assets',
value:
poolStates?.map(({ poolState }) =>
poolState.sumPrincipalRepaidAmountByPeriod
.toDecimal()
.sub(poolState.sumBorrowedAmountByPeriod.toDecimal())
.add(poolState.sumInterestRepaidAmountByPeriod.toDecimal())
.add(poolState.sumUnscheduledRepaidAmountByPeriod.toDecimal())
) || [],
value: data?.map((state) => state.netCashflowAsset.toDecimal()) || [],
heading: false,
bold: true,
formatter: (v: any) => (v ? formatBalance(v, pool.currency.displayName, 2) : ''),
},
]
}, [pool.currency.displayName, poolMetadata?.pool?.asset.class, poolStates])
}, [pool.currency.displayName, poolMetadata?.pool?.asset.class, data])

const netCashflowRecords: Row[] = React.useMemo(() => {
const feeRows =
data?.reduce<Row[]>((acc, state, stateIndex) => {
state.fees.forEach((fee) => {
const existingFee = acc.find((row) => row.name === fee.name)

if (existingFee) {
;(existingFee.value as any[])[stateIndex] = fee.amount.toDecimal().neg()
} else {
acc.push({
name: fee.name,
value: Array(data.length)
.fill(null)
.map((_, i) => (i === stateIndex ? fee.amount.toDecimal().neg() : 0)),
formatter: (v: any) => (v ? formatBalance(v || 0, pool.currency.displayName, 2) : 'n/a'),
})
}
})
return acc
}, []) ?? []

return [
...(Object.entries(poolFeeStates || {})?.flatMap(([, feeState]) => {
// some fee data may be incomplete since fees may have been added sometime after pool creation
// this fill the nonexistant fee data with zero values
let missingStates: {
timestamp: string
sumPaidAmountByPeriod: CurrencyBalance
}[] = []
if (feeState.length !== poolStates?.length) {
const missingTimestamps = poolStates
?.map((state) => state.timestamp)
.filter((timestamp) => !feeState.find((state) => state.timestamp.slice(0, 10) === timestamp.slice(0, 10)))
missingStates =
missingTimestamps?.map((timestamp) => {
return {
timestamp,
sumPaidAmountByPeriod: CurrencyBalance.fromFloat(0, pool.currency.decimals),
}
}) || []
}
return {
name: feeState[0].poolFee.name,
value: [...missingStates, ...feeState].map((state) => state.sumPaidAmountByPeriod.toDecimal().neg()),
formatter: (v: any) => `${formatBalance(v, pool.currency.displayName, 2)}`,
}
}) || []),
...feeRows,
{
name: 'Net cash flow after fees',
value:
poolStates?.map(({ poolState }) =>
poolState.sumPrincipalRepaidAmountByPeriod
.toDecimal()
.sub(poolState.sumBorrowedAmountByPeriod.toDecimal())
.add(poolState.sumInterestRepaidAmountByPeriod.toDecimal())
.add(poolState.sumUnscheduledRepaidAmountByPeriod.toDecimal())
.sub(poolState.sumPoolFeesPaidAmountByPeriod.toDecimal())
) || [],
value: data?.map((state) => state.netCashflowAfterFees.toDecimal()) || [],
formatter: (v: any) => (v ? formatBalance(v, pool.currency.displayName, 2) : ''),
heading: false,
bold: true,
},
]
}, [poolStates, pool, poolFeeStates])
}, [data, pool.currency.displayName])

const investRedeemRecords: Row[] = React.useMemo(() => {
return [
{
name: 'Pool investments',
value: poolStates?.map(({ poolState }) => poolState.sumInvestedAmountByPeriod.toDecimal()) || [],
value: data?.map((state) => state.investments.toDecimal()) || [],
heading: false,
formatter: (v: any) => (v ? formatBalance(v, pool.currency.displayName, 2) : ''),
},
{
name: 'Pool redemptions',
value: poolStates?.map(({ poolState }) => poolState.sumRedeemedAmountByPeriod.toDecimal().neg()) || [],
value: data?.map((state) => state.redemptions.toDecimal().neg()) || [],
heading: false,
formatter: (v: any) => `${formatBalance(v, pool.currency.displayName, 2)}`,
},
{
name: 'Cash flow from investment activities',
value:
poolStates?.map(({ poolState }) =>
poolState.sumInvestedAmountByPeriod.toDecimal().sub(poolState.sumRedeemedAmountByPeriod.toDecimal())
) || [],
value: data?.map((state) => state.activitiesCashflow.toDecimal()) || [],
formatter: (v: any) => (v ? formatBalance(v, pool.currency.displayName, 2) : ''),
heading: false,
bold: true,
},
]
}, [poolStates, pool])
}, [data, pool.currency.displayName])

const endCashflowRecords = React.useMemo(() => {
return [
{
name: 'Total cash flow',
value:
poolStates?.map(({ poolState }) =>
poolState.sumPrincipalRepaidAmountByPeriod
.toDecimal()
.sub(poolState.sumBorrowedAmountByPeriod.toDecimal())
.add(poolState.sumInterestRepaidAmountByPeriod.toDecimal())
.add(poolState.sumUnscheduledRepaidAmountByPeriod.toDecimal())
.sub(poolState.sumPoolFeesPaidAmountByPeriod.toDecimal())
.add(poolState.sumInvestedAmountByPeriod.toDecimal())
.sub(poolState.sumRedeemedAmountByPeriod.toDecimal())
) || [],
value: data?.map((state) => state.totalCashflow.toDecimal()) || [],
heading: true,
formatter: (v: any) => (v ? formatBalance(v, pool.currency.displayName, 2) : ''),
},
{
name: 'End cash balance',
value:
poolStatesNotAggregated?.map(({ poolState }) =>
poolState.totalReserve.toDecimal().add(poolState.offchainCashValue.toDecimal())
) || [],
value: data?.map((state) => state.endCashBalance.balance.toDecimal()) || [],
bold: true,
formatter: (v: any) => (v ? formatBalance(v, pool.currency.displayName, 2) : ''),
},
]
}, [poolStates, poolStatesNotAggregated, pool.currency.displayName])
}, [data, pool.currency.displayName])

const headers = columns.slice(0, -1).map(({ header }) => header)

Expand Down Expand Up @@ -323,23 +276,11 @@ export function CashflowStatement({ pool }: { pool: Pool }) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [grossCashflowRecords, netCashflowRecords])

React.useEffect(() => {
if (poolStates && Object.keys(poolStates).length > 0) {
const fullPoolStates: DailyPoolState[] = Object.values(poolStates).map((partialState) => {
return {
...partialState,
} as DailyPoolState
})

setReportData(fullPoolStates)
}
}, [poolStates, setReportData])

if (!poolStates) {
if (!data) {
return <Spinner mt={2} />
}

return poolStates?.length > 0 ? (
return data?.length > 0 ? (
<DataTableGroup>
<DataTable data={grossCashflowRecords} columns={columns} hoverable />
<DataTable data={netCashflowRecords} columns={columns} hoverable />
Expand Down
Loading

0 comments on commit e3da824

Please sign in to comment.