Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add orders report #2578

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 24 additions & 13 deletions centrifuge-app/src/components/Report/DataFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function DataFilter({ poolId }: ReportFilterProps) {
{ label: 'Token price', value: 'token-price' },
{ label: 'Asset list', value: 'asset-list' },
{ label: 'Investor list', value: 'investor-list' },
{ label: 'Orders', value: 'orders' },
]

return (
Expand Down Expand Up @@ -228,19 +229,29 @@ export function DataFilter({ poolId }: ReportFilterProps) {
/>
)}

<DateInput label="From" value={startDate} max={endDate} onChange={(e) => setStartDate(e.target.value)} />
<DateInput label="To" value={endDate} min={startDate} onChange={(e) => setEndDate(e.target.value)} />
{report !== 'orders' && (
<>
<DateInput label="From" value={startDate} max={endDate} onChange={(e) => setStartDate(e.target.value)} />
<DateInput label="To" value={endDate} min={startDate} onChange={(e) => setEndDate(e.target.value)} />
</>
)}
</Grid>
<Box display="flex" alignItems="center" justifyContent="space-between" margin={2}>
<Box display="flex" alignItems="center">
<Text variant="body3" style={{ marginRight: 4, fontWeight: 600 }}>
{formatDate(startDate)}
</Text>
<Text variant="body3">-</Text>
<Text variant="body3" style={{ marginLeft: 4, fontWeight: 600 }}>
{formatDate(endDate)}
</Text>
</Box>
<Grid
gridTemplateColumns={[report === 'orders' ? '130px' : '1fr 130px']}
margin={2}
justifyContent={report === 'orders' ? 'end' : 'space-between'}
>
{report !== 'orders' && (
<Box display="flex" alignItems="center" justifyContent="flex-start">
<Text variant="body3" style={{ marginRight: 4, fontWeight: 600 }}>
{formatDate(startDate)}
</Text>
<Text variant="body3">-</Text>
<Text variant="body3" style={{ marginLeft: 4, fontWeight: 600 }}>
{formatDate(endDate)}
</Text>
</Box>
)}
<AnchorButton
disabled={!csvData}
download={csvData?.fileName}
Expand All @@ -251,7 +262,7 @@ export function DataFilter({ poolId }: ReportFilterProps) {
>
Download
</AnchorButton>
</Box>
</Grid>
</Stack>
)
}
Expand Down
151 changes: 151 additions & 0 deletions centrifuge-app/src/components/Report/Orders.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { Pool } from '@centrifuge/centrifuge-js/dist/modules/pools'
import { Box, Text } from '@centrifuge/fabric'
import { useContext, useEffect, useMemo } from 'react'
import { TableDataRow } from '.'
import { formatDateAndTime } from '../../../src/utils/date'
import { formatBalance } from '../../../src/utils/formatting'
import { getCSVDownloadUrl } from '../../../src/utils/getCSVDownloadUrl'
import { usePoolOrdersByPoolId } from '../../../src/utils/usePools'
import { DataTable, SortableTableHeader } from '../DataTable'
import { ReportContext } from './ReportContext'
import { convertCSV } from './utils'

const noop = (v: any) => v

const Orders = ({ pool }: { pool: Pool }) => {
const { setCsvData, setStartDate } = useContext(ReportContext)
const orders = usePoolOrdersByPoolId(pool.id)

useEffect(() => {
if (!orders?.length) return
const dateStrings = orders?.map((order) => order.closedAt).filter(Boolean)
const oldestTimestamp = Math.min(...dateStrings.map((date) => new Date(date).getTime()))
const oldestDate = new Date(oldestTimestamp).toISOString().split('T')[0]
setStartDate(oldestDate)
}, [])

Check warning on line 25 in centrifuge-app/src/components/Report/Orders.tsx

View workflow job for this annotation

GitHub Actions / build-app

React Hook useEffect has missing dependencies: 'orders' and 'setStartDate'. Either include them or remove the dependency array

Check warning on line 25 in centrifuge-app/src/components/Report/Orders.tsx

View workflow job for this annotation

GitHub Actions / ff-prod / build-app

React Hook useEffect has missing dependencies: 'orders' and 'setStartDate'. Either include them or remove the dependency array

const columnsConfig = [
{
align: 'left',
header: 'Epoch',
formatter: noop,
sortable: true,
},
{
align: 'left',
header: 'Date & Time',
sortable: true,
formatter: (v: any) => (v ? formatDateAndTime(v) : '-'),
width: '200px',
},
{
align: 'left',
header: 'NAV',
sortable: true,
formatter: (v: any) => (v ? formatBalance(v, pool.currency.symbol) : '-'),
},
{
align: 'left',
header: 'Nav per share',
sortable: true,
formatter: (v: any) => (v ? formatBalance(v, pool.currency.symbol, 6, 6) : '-'),
},
{
align: 'left',
header: 'Investments locked',
sortable: true,
formatter: (v: any) => (v ? formatBalance(v, pool.currency.symbol, 2) : '-'),
},
{
align: 'left',
header: 'Investments executed',
sortable: true,
formatter: (v: any) => (v ? formatBalance(v, pool.currency.symbol, 2) : '-'),
},
{
align: 'left',
header: 'Redemptions locked',
sortable: true,
formatter: (v: any) => (v ? formatBalance(v, pool.currency.symbol, 2) : '-'),
},
{
align: 'left',
header: 'Redemptions executed',
sortable: true,
formatter: (v: any) => (v ? formatBalance(v, pool.currency.symbol, 2) : '-'),
},
{
align: 'left',
header: 'Paid fees',
sortable: true,
formatter: (v: any) => (v ? formatBalance(v, pool.currency.symbol, 2) : '-'),
},
]

const columns = columnsConfig.map((col, index) => ({
align: col.align,
header: col.sortable ? <SortableTableHeader label={col.header} /> : col.header,
cell: (row: TableDataRow) => {
return <Text variant="body3">{col.formatter((row.value as any)[index])}</Text>
},
sortKey: col.sortable ? `value[${index}]` : undefined,
width: col.width,
}))

const data = useMemo(() => {
if (!orders?.length) return []
else {
return orders.map((order) => {
const epoch = order.epochId.split('-')
return {
name: '',
value: [
epoch[1],
order.closedAt,
order.netAssetValue,
order.tokenPrice,
order.sumOutstandingInvestOrders,
order.sumFulfilledInvestOrders,
order.sumOutstandingRedeemOrders,
order.sumFulfilledRedeemOrders,
order.paidFees,
],
heading: false,
}
})
}
}, [orders])

const dataUrl = useMemo(() => {
if (!data.length) {
return
}

const formatted = data.map(({ value: values }) => convertCSV(values, columnsConfig))

return getCSVDownloadUrl(formatted)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data])

useEffect(() => {
setCsvData(
dataUrl
? {
dataUrl,
fileName: `${pool.id}-orders.csv`,
}
: undefined
)

return () => setCsvData(undefined)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataUrl, pool.id])

return (
<Box paddingX={2}>
<DataTable data={data} columns={columns} scrollable defaultSortKey="value[1]" defaultSortOrder="desc" />
</Box>
)
}

export default Orders
1 change: 1 addition & 0 deletions centrifuge-app/src/components/Report/ReportContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type Report =
| 'balance-sheet'
| 'cash-flow-statement'
| 'profit-and-loss'
| 'orders'

export type ReportContextType = {
csvData?: CsvDataProps
Expand Down
2 changes: 2 additions & 0 deletions centrifuge-app/src/components/Report/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { FeeTransactions } from './FeeTransactions'
import { InvestorList } from './InvestorList'
import { InvestorTransactions } from './InvestorTransactions'
import { OracleTransactions } from './OracleTransactions'
import Orders from './Orders'
import { PoolBalance } from './PoolBalance'
import { ProfitAndLoss } from './ProfitAndLoss'
import { ReportContext } from './ReportContext'
Expand Down Expand Up @@ -38,6 +39,7 @@ export function ReportComponent({ pool }: { pool: Pool }) {
{report === 'cash-flow-statement' && <CashflowStatement pool={pool} />}
{report === 'oracle-tx' && <OracleTransactions pool={pool} />}
{report === 'profit-and-loss' && <ProfitAndLoss pool={pool} />}
{report === 'orders' && <Orders pool={pool} />}
</Box>
</Box>
)
Expand Down
13 changes: 13 additions & 0 deletions centrifuge-app/src/utils/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ export function formatDate(timestamp: number | string | Date, options?: Intl.Dat
})
}

export function formatDateAndTime(timestamp: number | string | Date, options?: Intl.DateTimeFormatOptions) {
return new Date(timestamp).toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
timeZone: 'UTC',
...options,
})
}

export function formatDateTechnical(timestamp: number | string) {
return new Date(timestamp).toLocaleDateString('en-US')
}
Expand Down
5 changes: 5 additions & 0 deletions centrifuge-app/src/utils/usePools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,11 @@ export function usePoolOrders(poolId: string) {
return result
}

export function usePoolOrdersByPoolId(poolId: string) {
const [result] = useCentrifugeQuery(['poolOrdersByPoolId', poolId], (cent) => cent.pools.getPoolOrdersById([poolId]))
return result
}

export function useOrder(poolId: string, trancheId: string, address?: string) {
const [result] = useCentrifugeQuery(
['order', trancheId, address],
Expand Down
79 changes: 79 additions & 0 deletions centrifuge-js/src/modules/pools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
SubqueryPoolAssetSnapshot,
SubqueryPoolFeeSnapshot,
SubqueryPoolFeeTransaction,
SubqueryPoolOrdersById,
SubqueryPoolSnapshot,
SubqueryTrancheBalances,
SubqueryTrancheSnapshot,
Expand Down Expand Up @@ -3941,6 +3942,83 @@ export function getPoolsModule(inst: Centrifuge) {
)
}

function getPoolOrdersById(args: [poolId: string]) {
const [poolId] = args

const $query = inst.getSubqueryObservable<{
epoches: { nodes: SubqueryPoolOrdersById[] }
}>(
`query($poolId: String!) {
epoches(
filter: {
poolId: { equalTo: $poolId }
}
) {
nodes {
poolId
id
sumPoolFeesPaidAmount
closedAt
epochStates{
nodes{
tokenPrice
sumOutstandingInvestOrders
sumFulfilledInvestOrders
sumOutstandingRedeemOrders
sumFulfilledRedeemOrders
}
}
poolSnapshots{
nodes{
netAssetValue
}
}
}
}
}
`,
{
poolId,
},
false
)

return $query.pipe(
combineLatestWith(getPoolCurrency([poolId])),
map(([data, poolCurrency]) => {
return data?.epoches?.nodes.map((order) => {
const index = order.epochStates.nodes.length > 1 ? order.epochStates.nodes.length - 1 : 0
const snapshotIndex = order.poolSnapshots.nodes.length > 1 ? order.poolSnapshots.nodes.length - 1 : 0
return {
epochId: order.id,
closedAt: order.closedAt,
paidFees: order.sumPoolFeesPaidAmount
? new CurrencyBalance(order.sumPoolFeesPaidAmount, poolCurrency.decimals)
: null,
tokenPrice: order.epochStates.nodes[index].tokenPrice
? new Price(order.epochStates.nodes[index].tokenPrice)
: null,
sumOutstandingInvestOrders: order.epochStates.nodes[index].sumOutstandingInvestOrders
? new CurrencyBalance(order.epochStates.nodes[index].sumOutstandingInvestOrders, poolCurrency.decimals)
: null,
sumFulfilledInvestOrders: order.epochStates.nodes[index].sumFulfilledInvestOrders
? new CurrencyBalance(order.epochStates.nodes[index].sumFulfilledInvestOrders, poolCurrency.decimals)
: null,
sumOutstandingRedeemOrders: order.epochStates.nodes[index].sumOutstandingRedeemOrders
? new CurrencyBalance(order.epochStates.nodes[index].sumOutstandingRedeemOrders, poolCurrency.decimals)
: null,
sumFulfilledRedeemOrders: order.epochStates.nodes[index].sumFulfilledRedeemOrders
? new CurrencyBalance(order.epochStates.nodes[index].sumFulfilledRedeemOrders, poolCurrency.decimals)
: null,
netAssetValue: order.poolSnapshots.nodes.length
? new CurrencyBalance(order.poolSnapshots.nodes[snapshotIndex].netAssetValue, poolCurrency.decimals)
: null,
}
})
})
)
}

function getLoans(args: [poolId: string]) {
const [poolId] = args
const $api = inst.getApi()
Expand Down Expand Up @@ -4631,6 +4709,7 @@ export function getPoolsModule(inst: Centrifuge) {
getBalances,
getOrder,
getPoolOrders,
getPoolOrdersById,
getPoolAccountOrders,
getPortfolio,
getLoans,
Expand Down
21 changes: 21 additions & 0 deletions centrifuge-js/src/types/subquery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,27 @@ export type SubqueryPoolAssetSnapshot = {
totalRepaidUnscheduled: string | undefined
}

export type SubqueryPoolOrdersById = {
__typename?: 'Epoches'
id: string
sumPoolFeesPaidAmount: string
closedAt: string
epochStates: {
nodes: {
tokenPrice: string
sumOutstandingInvestOrders: string
sumFulfilledInvestOrders: string
sumOutstandingRedeemOrders: string
sumFulfilledRedeemOrders: string
}[]
}
poolSnapshots: {
nodes: {
netAssetValue: string
}[]
}
}

export type PoolFeeTransactionType = 'PROPOSED' | 'ADDED' | 'REMOVED' | 'CHARGED' | 'UNCHARGED' | 'PAID' | 'ACCRUED'

export type SubqueryPoolFeeTransaction = {
Expand Down
Loading