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) => ( + { @@ -61,35 +75,26 @@ export function ReportFilter({ pool }: ReportFilterProps) { }} /> - {report !== 'holders' && ( - { - setRange(range) - setStartDate(start) - setEndDate(end) - }} - /> + {!['investor-list', 'asset-list'].includes(report) && ( + <> + setStartDate(e.target.value)} /> + setEndDate(e.target.value)} /> + )} - {report === 'pool-balance' && ( + {['pool-balance', 'token-price'].includes(report) && ( { + setLoanStatus(event.target.value) + }} + /> + )} + + {(report === 'investor-list' || report === 'investor-tx') && ( { + setLoan(event.target.value) + }} + value={loan} options={[ - { - label: 'All', - value: 'all', - }, - { - label: 'Submitted orders', - value: 'orders', - }, - { - label: 'Executed orders', - value: 'executions', - }, - { - label: 'Transfers', - value: 'transfers', - }, + { label: 'All', value: 'all' }, + ...(loans?.map((l) => ({ value: l.id, label: })) ?? []), ]} - value={investorTxType} + /> + )} + + {['investor-tx', 'asset-tx', 'fee-tx'].includes(report) && ( + { + return { + label: getNetworkName(domain.chainId), + value: String(domain.chainId), + } + }), + ]} + value={network} + onChange={(e) => { + const { value } = e.target + if (value) { + setNetwork(isNaN(Number(value)) ? value : Number(value)) + } + }} + /> + setAddress(e.target.value)} + /> + + )} Export CSV @@ -165,3 +301,13 @@ export function ReportFilter({ pool }: ReportFilterProps) { ) } + +function LoanOption({ loan }: { loan: Loan }) { + const nft = useCentNFT(loan.asset.collectionId, loan.asset.nftId, false, false) + const { data: metadata } = useMetadata(nft?.metadataUri, nftMetadataSchema) + return ( + + ) +} diff --git a/centrifuge-app/src/components/Report/TokenPrice.tsx b/centrifuge-app/src/components/Report/TokenPrice.tsx new file mode 100644 index 0000000000..f395826b7a --- /dev/null +++ b/centrifuge-app/src/components/Report/TokenPrice.tsx @@ -0,0 +1,153 @@ +import { Pool } from '@centrifuge/centrifuge-js/dist/modules/pools' +import { Text } from '@centrifuge/fabric' +import * as React from 'react' +import { formatDate } from '../../utils/date' +import { formatBalance } from '../../utils/formatting' +import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl' +import { useDailyPoolStates, useMonthlyPoolStates } from '../../utils/usePools' +import { DataTable } from '../DataTable' +import { Spinner } from '../Spinner' +import { ReportContext } from './ReportContext' +import { UserFeedback } from './UserFeedback' +import type { TableDataRow } from './index' + +type Row = TableDataRow & { + formatter?: (v: any) => any +} + +export function TokenPrice({ pool }: { pool: Pool }) { + const { startDate, endDate, groupBy, setCsvData } = React.useContext(ReportContext) + + 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(() => { + if (!poolStates) { + return [] + } + + return [ + { + align: 'left', + header: '', + cell: (row: TableDataRow) => {row.name}, + width: '200px', + }, + ] + .concat( + poolStates.map((state, index) => ({ + align: 'right', + timestamp: state.timestamp, + header: + groupBy === 'day' + ? 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.formatter ? row.formatter((row.value as any)[index]) : (row.value as any)[index]} + + ), + width: '170px', + })) + ) + .concat({ + align: 'left', + header: '', + cell: () => , + width: '1fr', + }) + }, [poolStates, groupBy, pool]) + + const priceRecords: Row[] = React.useMemo(() => { + return [ + { + name: 'Token price', + value: poolStates?.map(() => '' as any) || [], + heading: false, + }, + ...(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 ? state.tranches[token.id].price!.toFloat() : 1 + ) || [], + heading: false, + formatter: (v: any) => formatBalance(v, pool.currency.symbol, 6), + })) || []), + { + name: 'Token supply', + value: poolStates?.map(() => '' as any) || [], + heading: false, + }, + ...(pool?.tranches + .slice() + .reverse() + .map((token) => ({ + name: `\u00A0 \u00A0 ${token.currency.name.split(' ').at(-1)} tranche`, + value: poolStates?.map((state) => state.tranches[token.id].tokenSupply.toFloat()) || [], + heading: false, + formatter: (v: any) => formatBalance(v, '', 2), + })) || []), + ] + }, [poolStates, pool]) + + const headers = columns.slice(0, -1).map(({ header }) => header) + + React.useEffect(() => { + const f = priceRecords.map(({ name, value }) => [name.trim(), ...(value as string[])]) + let formatted = f.map((values) => + Object.fromEntries(headers.map((_, index) => [`"${headers[index]}"`, `"${values[index]}"`])) + ) + + if (!formatted.length) { + return + } + + const dataUrl = getCSVDownloadUrl(formatted) + + setCsvData({ + dataUrl, + fileName: `${pool.id}-token-price-${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 + }, [priceRecords]) + + if (!poolStates) { + return + } + + return poolStates?.length > 0 ? ( + + ) : ( + + ) +} diff --git a/centrifuge-app/src/components/Report/UserFeedback.tsx b/centrifuge-app/src/components/Report/UserFeedback.tsx index 90e5595b20..b1d8d45e88 100644 --- a/centrifuge-app/src/components/Report/UserFeedback.tsx +++ b/centrifuge-app/src/components/Report/UserFeedback.tsx @@ -1,5 +1,4 @@ import { Box, InlineFeedback, Shelf, Text } from '@centrifuge/fabric' -import * as React from 'react' export function UserFeedback({ reportType }: { reportType: string }) { return ( @@ -10,7 +9,7 @@ export function UserFeedback({ reportType }: { reportType: string }) { {reportType} {' '} - data available for this pool. Try to select another report or date range. + data available for this pool. Try to select another report or other filters. diff --git a/centrifuge-app/src/components/Report/index.tsx b/centrifuge-app/src/components/Report/index.tsx index 1206ba5f8e..9ef1e31add 100644 --- a/centrifuge-app/src/components/Report/index.tsx +++ b/centrifuge-app/src/components/Report/index.tsx @@ -5,14 +5,15 @@ import { formatDate } from '../../utils/date' import { AssetList } from './AssetList' import { AssetTransactions } from './AssetTransactions' import { FeeTransactions } from './FeeTransactions' -import { Holders } from './Holders' +import { InvestorList } from './InvestorList' import { InvestorTransactions } from './InvestorTransactions' import { PoolBalance } from './PoolBalance' import { ReportContext } from './ReportContext' +import { TokenPrice } from './TokenPrice' export type TableDataRow = { - name: string | React.ReactElement - value: string[] | React.ReactElement + name: string + value: (string | number)[] heading?: boolean } @@ -23,11 +24,15 @@ export function ReportComponent({ pool }: { pool: Pool }) { - - {' - '} - + {!['investor-list', 'asset-list'].includes(report) && ( + <> + {startDate ? formatDate(startDate) : 'The beginning of time'} + {' - '} + {endDate ? formatDate(endDate) : 'now'} + + )} - {(report === 'pool-balance' || report === 'asset-list') && pool && ( + {['pool-balance', 'asset-list'].includes(report) && pool && ( All amounts are in {pool.currency.symbol} @@ -35,8 +40,9 @@ export function ReportComponent({ pool }: { pool: Pool }) { {report === 'pool-balance' && } + {report === 'token-price' && } {report === 'asset-list' && } - {report === 'holders' && } + {report === 'investor-list' && } {report === 'investor-tx' && } {report === 'asset-tx' && } {report === 'fee-tx' && } diff --git a/centrifuge-app/src/components/Report/utils.tsx b/centrifuge-app/src/components/Report/utils.tsx index 6a3b1951a0..afbe849cbe 100644 --- a/centrifuge-app/src/components/Report/utils.tsx +++ b/centrifuge-app/src/components/Report/utils.tsx @@ -77,6 +77,7 @@ const assetTransactionTypes: { BORROWED: 'Financed', REPAID: 'Repaid', CLOSED: 'Closed', + CASH_TRANSFER: 'Cash transfer', } export function formatAssetTransactionType(type: AssetTransactionType) { diff --git a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx index d870c06220..70e52361c6 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx @@ -451,14 +451,13 @@ function CreatePoolForm() { amount: Rate.fromPercent(fee.percentOfNav), feeType: fee.feeType, limit: 'ShareOfPortfolioValuation', - feeId: feeId + i, account: fee.feeType === 'chargedUpTo' ? fee.walletAddress : undefined, feePosition: fee.feePosition, } }) - metadataValues.poolFees = poolFees.map((fee) => ({ + metadataValues.poolFees = poolFees.map((fee, i) => ({ name: fee.name, - id: fee.feeId, + id: feeId + i, feePosition: fee.feePosition, feeType: fee.feeType, })) diff --git a/centrifuge-app/src/pages/Loan/TransactionTable.tsx b/centrifuge-app/src/pages/Loan/TransactionTable.tsx index b324cf91ee..9837468f4e 100644 --- a/centrifuge-app/src/pages/Loan/TransactionTable.tsx +++ b/centrifuge-app/src/pages/Loan/TransactionTable.tsx @@ -58,62 +58,66 @@ export const TransactionTable = ({ return 0 }) - return sortedTransactions.map((transaction, index, array) => { - const termDays = originationDate - ? daysBetween(originationDate, maturityDate) - : daysBetween(new Date(), maturityDate) - const yearsBetweenDates = termDays / 365 + return sortedTransactions + .filter((transaction) => { + return !transaction.amount?.isZero() + }) + .map((transaction, index, array) => { + const termDays = originationDate + ? daysBetween(originationDate, maturityDate) + : daysBetween(new Date(), maturityDate) + const yearsBetweenDates = termDays / 365 - const faceValue = - transaction.quantity && (pricing as ExternalPricingInfo).notional - ? new CurrencyBalance(transaction.quantity, 18) - .toDecimal() - .mul((pricing as ExternalPricingInfo).notional.toDecimal()) - : null + const faceValue = + transaction.quantity && (pricing as ExternalPricingInfo).notional + ? new CurrencyBalance(transaction.quantity, 18) + .toDecimal() + .mul((pricing as ExternalPricingInfo).notional.toDecimal()) + : null - return { - type: transaction.type, - amount: transaction.amount, - quantity: transaction.quantity ? new CurrencyBalance(transaction.quantity, 18) : null, - transactionDate: transaction.timestamp, - yieldToMaturity: - transaction.amount && faceValue && transaction.type !== 'REPAID' - ? Dec(2) - .mul(faceValue?.sub(transaction.amount.toDecimal())) - .div(Dec(yearsBetweenDates).mul(faceValue.add(transaction.amount.toDecimal()))) - .mul(100) + return { + type: transaction.type, + amount: transaction.amount, + quantity: transaction.quantity ? new CurrencyBalance(transaction.quantity, 18) : null, + transactionDate: transaction.timestamp, + yieldToMaturity: + transaction.amount && faceValue && transaction.type !== 'REPAID' + ? Dec(2) + .mul(faceValue?.sub(transaction.amount.toDecimal())) + .div(Dec(yearsBetweenDates).mul(faceValue.add(transaction.amount.toDecimal()))) + .mul(100) + : null, + settlePrice: transaction.settlementPrice + ? new CurrencyBalance(new BN(transaction.settlementPrice), decimals) : null, - settlePrice: transaction.settlementPrice - ? new CurrencyBalance(new BN(transaction.settlementPrice), decimals) - : null, - faceValue, - position: array.slice(0, index + 1).reduce((sum, trx) => { - if (trx.type === 'BORROWED') { - sum = sum.add( - trx.quantity - ? new CurrencyBalance(trx.quantity, 18) - .toDecimal() - .mul((pricing as ExternalPricingInfo).notional.toDecimal()) - : trx.amount - ? trx.amount.toDecimal() - : Dec(0) - ) - } - if (trx.type === 'REPAID') { - sum = sum.sub( - trx.quantity - ? new CurrencyBalance(trx.quantity, 18) - .toDecimal() - .mul((pricing as ExternalPricingInfo).notional.toDecimal()) - : trx.amount - ? trx.amount.toDecimal() - : Dec(0) - ) - } - return sum - }, Dec(0)), - } - }) + faceValue, + position: array.slice(0, index + 1).reduce((sum, trx) => { + if (trx.type === 'BORROWED') { + sum = sum.add( + trx.quantity + ? new CurrencyBalance(trx.quantity, 18) + .toDecimal() + .mul((pricing as ExternalPricingInfo).notional.toDecimal()) + : trx.amount + ? trx.amount.toDecimal() + : Dec(0) + ) + } + if (trx.type === 'REPAID') { + sum = sum.sub( + trx.quantity + ? new CurrencyBalance(trx.quantity, 18) + .toDecimal() + .mul((pricing as ExternalPricingInfo).notional.toDecimal()) + : trx.amount + ? trx.amount.toDecimal() + : Dec(0) + ) + } + return sum + }, Dec(0)), + } + }) }, [transactions, decimals, pricing]) const getStatusChipType = (type: AssetTransactionType) => { diff --git a/centrifuge-app/src/pages/Loan/TransferDebtForm.tsx b/centrifuge-app/src/pages/Loan/TransferDebtForm.tsx index acf23acfb0..42829f8540 100644 --- a/centrifuge-app/src/pages/Loan/TransferDebtForm.tsx +++ b/centrifuge-app/src/pages/Loan/TransferDebtForm.tsx @@ -81,8 +81,8 @@ export function TransferDebtForm({ loan }: { loan: LoanType }) { let interest = new BN(borrowAmount) let principal = new BN(0) if (interest.gt(outstandingInterest)) { - interest = outstandingInterest principal = interest.sub(outstandingInterest) + interest = outstandingInterest } let repay: any = { principal, interest } if (isExternalLoan(selectedLoan)) { diff --git a/centrifuge-app/src/pages/Loan/index.tsx b/centrifuge-app/src/pages/Loan/index.tsx index 43b591800d..7717eb92dd 100644 --- a/centrifuge-app/src/pages/Loan/index.tsx +++ b/centrifuge-app/src/pages/Loan/index.tsx @@ -7,11 +7,13 @@ import { TinlakeLoan, } from '@centrifuge/centrifuge-js' import { + AnchorButton, Box, Button, Drawer, Flex, IconChevronLeft, + IconDownload, Shelf, Stack, Text, @@ -22,6 +24,7 @@ import { import * as React from 'react' import { useParams, useRouteMatch } from 'react-router' import { AssetSummary } from '../../components/AssetSummary' +import { useDebugFlags } from '../../components/DebugFlags' import { LabelValueStack } from '../../components/LabelValueStack' import { LayoutBase } from '../../components/LayoutBase' import { LoadBoundary } from '../../components/LoadBoundary' @@ -36,12 +39,13 @@ import { Dec } from '../../utils/Decimal' import { copyToClipboard } from '../../utils/copyToClipboard' import { daysBetween, formatDate, isValidDate } from '../../utils/date' import { formatBalance, formatPercentage, truncateText } from '../../utils/formatting' +import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl' import { useLoan, useNftDocumentId } from '../../utils/useLoans' import { useMetadata } from '../../utils/useMetadata' import { useCentNFT } from '../../utils/useNFTs' import { useCanBorrowAsset } from '../../utils/usePermissions' import { usePodDocument } from '../../utils/usePodDocument' -import { useBorrowerAssetTransactions, usePool, usePoolMetadata } from '../../utils/usePools' +import { useAssetSnapshots, useBorrowerAssetTransactions, usePool, usePoolMetadata } from '../../utils/usePools' import { FinanceForm } from './FinanceForm' import { FinancingRepayment } from './FinancingRepayment' import { HoldingsValues } from './HoldingsValues' @@ -108,6 +112,23 @@ function Loan() { const metadataIsLoading = poolMetadataIsLoading || nftMetadataIsLoading const borrowerAssetTransactions = useBorrowerAssetTransactions(poolId, loanId) + const { assetSnapshots: showAssetSnapshots } = useDebugFlags() + const assetSnapshots = useAssetSnapshots(poolId, loanId) + + const dataUrl: any = React.useMemo(() => { + if (!assetSnapshots || !assetSnapshots?.length) { + return undefined + } + + const formatted = assetSnapshots.map((snapshot) => { + return { + ...snapshot, + } + }) + + return getCSVDownloadUrl(formatted as any) + }, [assetSnapshots, pool.currency.symbol]) + const currentFace = loan?.pricing && 'outstandingQuantity' in loan.pricing ? loan.pricing.outstandingQuantity.toDecimal().mul(loan.pricing.notional.toDecimal()) @@ -291,7 +312,22 @@ function Loan() { ) : null} {'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'oracle' && ( - Holdings}> + Holdings} + headerRight={ + showAssetSnapshots && ( + + Timeseries + + ) + } + > () const isTinlakePool = poolId.startsWith('0x') const pool = usePool(poolId) + const poolFees = usePoolFees(poolId) const { data: metadata, isLoading: metadataIsLoading } = usePoolMetadata(pool) const averageMaturity = useAverageMaturity(poolId) const loans = useLoans(poolId) @@ -144,7 +145,7 @@ export function PoolDetailOverview() { poolFees={ metadata?.pool?.poolFees?.map((fee) => { return { - fee: pool.poolFees?.find((f) => f.id === fee.id)?.amounts.percentOfNav ?? Rate.fromFloat(0), + fee: poolFees?.find((f) => f.id === fee.id)?.amounts.percentOfNav ?? Rate.fromFloat(0), name: fee.name, id: fee.id, } diff --git a/centrifuge-app/src/utils/formatting.ts b/centrifuge-app/src/utils/formatting.ts index 53a22ffee6..eb0fe95ce2 100644 --- a/centrifuge-app/src/utils/formatting.ts +++ b/centrifuge-app/src/utils/formatting.ts @@ -46,7 +46,7 @@ export function formatBalanceAbbreviated( } export function formatPercentage( - amount: Perquintill | Decimal | number, + amount: Perquintill | Decimal | number | string, includeSymbol = true, options: Intl.NumberFormatOptions = {}, precision?: number @@ -56,7 +56,7 @@ export function formatPercentage( ? amount.toPercent().toNumber() : amount instanceof Decimal ? amount.toNumber() - : amount + : Number(amount) ).toLocaleString('en', { minimumFractionDigits: precision || 2, maximumFractionDigits: precision || 2, diff --git a/centrifuge-app/src/utils/useLiquidityPools.ts b/centrifuge-app/src/utils/useLiquidityPools.ts index 1d8dc46d25..9c3728f7e0 100644 --- a/centrifuge-app/src/utils/useLiquidityPools.ts +++ b/centrifuge-app/src/utils/useLiquidityPools.ts @@ -12,6 +12,7 @@ export function useDomainRouters(suspense?: boolean) { export type Domain = (ReturnType extends Promise ? T : never) & { chainId: number managerAddress: string + hasDeployedLp: boolean } export function useActiveDomains(poolId: string, suspense?: boolean) { @@ -45,6 +46,9 @@ export function useActiveDomains(poolId: string, suspense?: boolean) { ...pool, chainId: router.chainId, managerAddress: manager, + hasDeployedLp: Object.values(pool.liquidityPools).some( + (tranche) => !!Object.values(tranche).some((p) => !!p) + ), } return domain }) diff --git a/centrifuge-app/src/utils/usePools.ts b/centrifuge-app/src/utils/usePools.ts index 0b14e1866a..d07a4cd866 100644 --- a/centrifuge-app/src/utils/usePools.ts +++ b/centrifuge-app/src/utils/usePools.ts @@ -64,9 +64,9 @@ export function useTransactionsByAddress(address?: string) { return result } -export function useHolders(poolId: string, trancheId?: string) { - const [result] = useCentrifugeQuery(['holders', poolId, trancheId], (cent) => - cent.pools.getHolders([poolId, trancheId]) +export function useInvestorList(poolId: string, trancheId?: string) { + const [result] = useCentrifugeQuery(['investors', poolId, trancheId], (cent) => + cent.pools.getInvestors([poolId, trancheId]) ) return result @@ -92,6 +92,26 @@ export function useAssetTransactions(poolId: string, from?: Date, to?: Date) { return result } +export function useAssetSnapshots(poolId: string, loanId: string, from?: Date, to?: Date) { + const [result] = useCentrifugeQuery( + ['assetSnapshots', poolId, loanId, from, to], + (cent) => cent.pools.getAssetSnapshots([poolId, loanId, from, to]), + { + enabled: !poolId.startsWith('0x'), + } + ) + + return result +} + +export function usePoolFees(poolId: string) { + const [result] = useCentrifugeQuery(['poolFees', poolId], (cent) => cent.pools.getPoolFees([poolId]), { + enabled: !poolId.startsWith('0x'), + }) + + return result +} + export function useFeeTransactions(poolId: string, from?: Date, to?: Date) { const [result] = useCentrifugeQuery( ['feeTransactions', poolId, from, to], diff --git a/centrifuge-js/src/CentrifugeBase.ts b/centrifuge-js/src/CentrifugeBase.ts index 46c28f1dae..d0af496be1 100644 --- a/centrifuge-js/src/CentrifugeBase.ts +++ b/centrifuge-js/src/CentrifugeBase.ts @@ -110,8 +110,8 @@ const parachainTypes = { }, PoolFeesList: 'Vec', PoolFeesOfBucket: { - bucket: 'PoolFeeBucket', - fees: 'Vec', + bucket: 'CfgTraitsFeePoolFeeBucket', + fees: 'Vec', }, PriceCollectionInput: { _enum: ['Empty', 'Custom(BoundedBTreeMap)', 'FromRegistry'], diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index 2980c20404..82470dfa56 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -11,8 +11,10 @@ import { Centrifuge } from '../Centrifuge' import { Account, TransactionOptions } from '../types' import { AssetTransactionType, + AssetType, InvestorTransactionType, PoolFeeTransactionType, + SubqueryAssetSnapshot, SubqueryAssetTransaction, SubqueryCurrencyBalances, SubqueryInvestorTransaction, @@ -36,7 +38,6 @@ const PerquintillBN = new BN(10).pow(new BN(18)) const PriceBN = new BN(10).pow(new BN(18)) const MaxU128 = '340282366920938463463374607431768211455' const SEC_PER_DAY = 24 * 60 * 60 -const SEC_PER_YEAR = SEC_PER_DAY * 365 type AdminRole = | 'PoolAdmin' @@ -335,7 +336,6 @@ export type Pool = { value: CurrencyBalance createdAt: string | null tranches: Token[] - poolFees: ActivePoolFees[] | null reserve: { max: CurrencyBalance available: CurrencyBalance @@ -557,6 +557,7 @@ export type TrancheInput = { export type DailyTrancheState = { id: string price: null | Price + tokenSupply: TokenBalance fulfilledInvestOrders: CurrencyBalance fulfilledRedeemOrders: CurrencyBalance outstandingInvestOrders: CurrencyBalance @@ -571,12 +572,13 @@ export type DailyPoolState = { poolValue: CurrencyBalance timestamp: string tranches: { [trancheId: string]: DailyTrancheState } - - sumBorrowedAmountByPeriod?: string | null - sumInterestRepaidAmountByPeriod?: string | null - sumRepaidAmountByPeriod?: number | null - sumInvestedAmountByPeriod?: number | null - sumRedeemedAmountByPeriod?: number | null + sumPoolFeesChargedAmountByPeriod: string | null + sumPoolFeesAccruedAmountByPeriod: string | null + sumBorrowedAmountByPeriod: string + sumInterestRepaidAmountByPeriod: string + sumRepaidAmountByPeriod: string + sumInvestedAmountByPeriod: string + sumRedeemedAmountByPeriod: string blockNumber: number } @@ -742,28 +744,23 @@ export type WriteOffGroup = { type InvestorTransaction = { id: string - timestamp: string + timestamp: Date accountId: string trancheId: string epochNumber: number type: InvestorTransactionType - currencyAmount: CurrencyBalance | undefined - tokenAmount: CurrencyBalance | undefined - tokenPrice: Price | undefined - transactionFee: CurrencyBalance | null + currencyAmount?: CurrencyBalance + tokenAmount?: CurrencyBalance + tokenPrice?: Price + transactionFee?: CurrencyBalance chainId: number evmAddress?: string -} - -export enum AssetType { - OnchainCash = 'OnchainCash', - OffchainCash = 'OffchainCash', - Other = 'Other', + hash: string } export type AssetTransaction = { id: string - timestamp: string + timestamp: Date poolId: string accountId: string epochId: string @@ -791,9 +788,28 @@ export type AssetTransaction = { } } +export type AssetSnapshot = { + timestamp: Date + asset: { + id: string + name: string + metadata: string + type: AssetType + } + presentValue: CurrencyBalance | undefined + outstandingPrincipal: CurrencyBalance | undefined + outstandingInterest: CurrencyBalance | undefined + outstandingDebt: CurrencyBalance | undefined + outstandingQuantity: CurrencyBalance | undefined + totalBorrowed: CurrencyBalance | undefined + totalRepaidPrincipal: CurrencyBalance | undefined + totalRepaidInterest: CurrencyBalance | undefined + totalRepaidUnscheduled: CurrencyBalance | undefined +} + export type PoolFeeTransaction = { id: string - timestamp: string + timestamp: Date epochNumber: string type: PoolFeeTransactionType amount: CurrencyBalance | undefined @@ -805,6 +821,7 @@ export type PoolFeeTransaction = { type Holder = { accountId: string chainId: number + trancheId: string evmAddress?: string balance: CurrencyBalance pendingInvestCurrency: CurrencyBalance @@ -842,12 +859,18 @@ export type ActivePoolFees = { } limit: FeeLimits destination: string + editor: + | { + root: null + } + | { account: string } id: number + position: 'Top' } export type ActivePoolFeesData = { amounts: { - disbursement: CurrencyBalance + disbursement: string feeType: { [K in FeeTypes]: { limit: { @@ -858,12 +881,14 @@ export type ActivePoolFeesData = { payable: { upTo: string } - pending: CurrencyBalance + pending: string } destination: string - editor: { - root: { account: string } | null - } + editor: + | { + root: null + } + | { account: string } id: number } @@ -873,7 +898,6 @@ export type AddFee = { feeType: FeeTypes limit: 'ShareOfPortfolioValuation' | 'AmountPerSecond' name: string - feeId: number amount: Rate account?: string feePosition: 'Top of waterfall' @@ -1822,9 +1846,7 @@ export function getPoolsModule(inst: Centrifuge) { api.events.poolSystem.EpochExecuted.is(event) || api.events.poolSystem.SolutionSubmitted.is(event) || api.events.investments.InvestOrderUpdated.is(event) || - api.events.investments.RedeemOrderUpdated.is(event) || - api.events.poolFees.Charged.is(event) || - api.events.poolFees.Added.is(event) + api.events.investments.RedeemOrderUpdated.is(event) ) return !!event }) @@ -1852,254 +1874,221 @@ export function getPoolsModule(inst: Centrifuge) { api.query.loans.portfolioValuation.entries(), api.query.poolSystem.epochExecution.entries(), getCurrencies(), - api.query.poolFees.activeFees.entries(), ]), - (api, [rawPools, rawMetadatas, rawPortfolioValuation, rawEpochExecutions, currencies, activePoolFees]) => ({ + (api, [rawPools, rawMetadatas, rawPortfolioValuation, rawEpochExecutions, currencies]) => ({ api, rawPools, rawMetadatas, rawPortfolioValuation, rawEpochExecutions, currencies, - activePoolFees, }) ), - switchMap( - ({ api, rawPools, rawMetadatas, rawPortfolioValuation, rawEpochExecutions, currencies, activePoolFees }) => { - if (!rawPools.length) return of([]) - - const poolFeesMap = activePoolFees.reduce((acc, [key, fees]) => { - const poolId = formatPoolKey(key as StorageKey<[u32]>) - acc[poolId] = fees.toJSON() as unknown as ActivePoolFeesData[] - return acc - }, {} as Record) - - const portfolioValuationMap = rawPortfolioValuation.reduce((acc, [key, navValue]) => { - const poolId = formatPoolKey(key as StorageKey<[u32]>) - const nav = navValue.toJSON() as unknown as NAVDetailsData - acc[poolId] = { - lastUpdated: nav ? nav.lastUpdated : 0, - } - return acc - }, {} as Record) - - const epochExecutionMap = rawEpochExecutions.reduce((acc, [key, navValue]) => { - const poolId = formatPoolKey(key as StorageKey<[u32]>) - const epoch = navValue.toJSON() as EpochExecutionData - acc[poolId] = { - epoch: epoch.epoch, - challengePeriodEnd: epoch.challengePeriodEnd, - } - return acc - }, {} as Record>) - - const metadataMap = rawMetadatas.reduce((acc, [key, metadataValue]) => { - const poolId = formatPoolKey(key as StorageKey<[u32]>) - const metadata = (metadataValue.toHuman() as { metadata: string }).metadata - acc[poolId] = metadata - return acc - }, {} as Record) - - // read pools, poolIds and currencies from observable - const pools = rawPools.map(([poolKeys, poolValue]) => { - const data = poolValue.toJSON() as PoolDetailsData - const { currency } = poolValue.toHuman() as any - data.currency = parseCurrencyKey(currency) - return { - id: formatPoolKey(poolKeys as any), // poolId - data, // pool data - } + switchMap(({ api, rawPools, rawMetadatas, rawPortfolioValuation, rawEpochExecutions, currencies }) => { + if (!rawPools.length) return of([]) + + const portfolioValuationMap = rawPortfolioValuation.reduce((acc, [key, navValue]) => { + const poolId = formatPoolKey(key as StorageKey<[u32]>) + const nav = navValue.toJSON() as unknown as NAVDetailsData + acc[poolId] = { + lastUpdated: nav ? nav.lastUpdated : 0, + } + return acc + }, {} as Record) + + const epochExecutionMap = rawEpochExecutions.reduce((acc, [key, navValue]) => { + const poolId = formatPoolKey(key as StorageKey<[u32]>) + const epoch = navValue.toJSON() as EpochExecutionData + acc[poolId] = { + epoch: epoch.epoch, + challengePeriodEnd: epoch.challengePeriodEnd, + } + return acc + }, {} as Record>) + + const metadataMap = rawMetadatas.reduce((acc, [key, metadataValue]) => { + const poolId = formatPoolKey(key as StorageKey<[u32]>) + const metadata = (metadataValue.toHuman() as { metadata: string }).metadata + acc[poolId] = metadata + return acc + }, {} as Record) + + // read pools, poolIds and currencies from observable + const pools = rawPools.map(([poolKeys, poolValue]) => { + const data = poolValue.toJSON() as PoolDetailsData + const { currency } = poolValue.toHuman() as any + data.currency = parseCurrencyKey(currency) + return { + id: formatPoolKey(poolKeys as any), // poolId + data, // pool data + } + }) + + const keys = pools + .map(({ id, data }) => { + return data.tranches.ids.map((tid) => [id, tid, data.epoch.lastExecuted] as const) }) + .flat() - const keys = pools - .map(({ id, data }) => { - return data.tranches.ids.map((tid) => [id, tid, data.epoch.lastExecuted] as const) - }) - .flat() + const $yield30DaysAnnualized = combineLatest( + keys.map((key) => { + const poolIdTrancheId = `${key[0]}-${key[1].toLowerCase()}` + return getLatestTrancheSnapshot(poolIdTrancheId).pipe(map((snapshot) => snapshot?.trancheSnapshots.nodes)) + }) + ) as Observable<{ yield30DaysAnnualized: string | null; trancheId: string }[][]> - const $yield30DaysAnnualized = combineLatest( - keys.map((key) => { - const poolIdTrancheId = `${key[0]}-${key[1].toLowerCase()}` - return getLatestTrancheSnapshot(poolIdTrancheId).pipe(map((snapshot) => snapshot?.trancheSnapshots.nodes)) - }) - ) as Observable<{ yield30DaysAnnualized: string | null; trancheId: string }[][]> + const trancheIdToIndex: Record = {} + keys.forEach(([, tid], i) => { + trancheIdToIndex[tid] = i + }) - const trancheIdToIndex: Record = {} - keys.forEach(([, tid], i) => { - trancheIdToIndex[tid] = i - }) + // modify keys for $issuance query [Tranche: [poolId, trancheId]] + const issuanceKeys = keys.map(([poolId, trancheId]) => ({ Tranche: [poolId, trancheId] })) + const $issuance = api.query.ormlTokens.totalIssuance.multi(issuanceKeys).pipe(take(1)) - // modify keys for $issuance query [Tranche: [poolId, trancheId]] - const issuanceKeys = keys.map(([poolId, trancheId]) => ({ Tranche: [poolId, trancheId] })) - const $issuance = api.query.ormlTokens.totalIssuance.multi(issuanceKeys).pipe(take(1)) + const $prices = combineLatest( + pools.map((p) => api.call.poolsApi.trancheTokenPrices(p.id).pipe(startWith(null))) as Observable< + Codec[] | null + >[] + ) - const $prices = combineLatest( - pools.map((p) => api.call.poolsApi.trancheTokenPrices(p.id).pipe(startWith(null))) as Observable< - Codec[] | null - >[] - ) + const $navs = combineLatest(pools.map((p) => api.call.poolsApi.nav(p.id)) as Observable[]) - const $navs = combineLatest(pools.map((p) => api.call.poolsApi.nav(p.id)) as Observable[]) + const $block = inst.getBlocks().pipe(take(1)) - const $block = inst.getBlocks().pipe(take(1)) + return combineLatest([$issuance, $block, $prices, $navs, $yield30DaysAnnualized]).pipe( + map(([rawIssuances, { block }, rawPrices, rawNavs, [rawYield30DaysAnnualized]]) => { + const blockNumber = block.header.number.toNumber() - return combineLatest([$issuance, $block, $prices, $navs, $yield30DaysAnnualized]).pipe( - map(([rawIssuances, { block }, rawPrices, rawNavs, [rawYield30DaysAnnualized]]) => { - const blockNumber = block.header.number.toNumber() + const yield30DaysAnnualizedByPoolIdTrancheId = rawYield30DaysAnnualized?.reduce( + (acc, { yield30DaysAnnualized, trancheId }) => { + acc[trancheId] = yield30DaysAnnualized + return acc + }, + {} as Record + ) - const yield30DaysAnnualizedByPoolIdTrancheId = rawYield30DaysAnnualized?.reduce( - (acc, { yield30DaysAnnualized, trancheId }) => { - acc[trancheId] = yield30DaysAnnualized - return acc - }, - {} as Record + const mappedPools = pools.map((poolObj, poolIndex) => { + const { data: pool, id: poolId } = poolObj + const metadata = metadataMap[poolId] + const portfolioValuationData = portfolioValuationMap[poolId] + const epochExecution = epochExecutionMap[poolId] + const currency = findCurrency(currencies, pool.currency)! + + const poolValue = new CurrencyBalance( + pool.tranches.tranches.reduce((prev: BN, tranche: TrancheDetailsData) => { + return new BN(prev.add(new BN(hexToBN(tranche.debt))).add(new BN(hexToBN(tranche.reserve)))) + }, new BN(0)), + currency.decimals ) - const mappedPools = pools.map((poolObj, poolIndex) => { - const { data: pool, id: poolId } = poolObj - const poolFees = poolFeesMap[poolId] - const metadata = metadataMap[poolId] - const portfolioValuationData = portfolioValuationMap[poolId] - const epochExecution = epochExecutionMap[poolId] - const currency = findCurrency(currencies, pool.currency)! - - const poolValue = new CurrencyBalance( - pool.tranches.tranches.reduce((prev: BN, tranche: TrancheDetailsData) => { - return new BN(prev.add(new BN(hexToBN(tranche.debt))).add(new BN(hexToBN(tranche.reserve)))) - }, new BN(0)), - currency.decimals - ) - - const maxReserve = new CurrencyBalance(hexToBN(pool.reserve.max), currency.decimals) - const availableReserve = new CurrencyBalance(hexToBN(pool.reserve.available), currency.decimals) - const totalReserve = new CurrencyBalance(hexToBN(pool.reserve.total), currency.decimals) - - const lastUpdatedNav = new Date((portfolioValuationData?.lastUpdated ?? 0) * 1000).toISOString() - // @ts-expect-error - const rawNav = rawNavs && rawNavs[poolIndex]?.toJSON() - - const mappedPool: Pool = { - id: poolId, - createdAt: null, - metadata, - currency, - poolFees: poolFees?.map((fee) => { - const type = Object.keys(fee.amounts.feeType)[0] as FeeTypes - const limit = Object.keys(fee.amounts.feeType[type].limit)[0] as FeeLimits - const percentOfNav = new Rate(hexToBN(fee.amounts.feeType[type].limit[limit])) - return { - ...fee, - type, - limit, - amounts: { - percentOfNav, - pending: - type === 'chargedUpTo' - ? new CurrencyBalance(fee.amounts.pending, currency.decimals) - : new CurrencyBalance( - new CurrencyBalance(fee.amounts.payable.upTo, currency.decimals) - .divn(limit === 'amountPerSecond' ? 1 : SEC_PER_YEAR) - .add(new BN(fee.amounts.pending)), - currency.decimals - ), - }, - } - }), - tranches: pool.tranches.tranches.map((tranche, index) => { - const trancheId = pool.tranches.ids[index] - const trancheKeyIndex = trancheIdToIndex[trancheId] - // const lastClosedEpoch = epochs[trancheKeyIndex] - - let minRiskBuffer: Perquintill | null = null - let interestRatePerSec: Rate | null = null - if ('nonResidual' in tranche.trancheType) { - minRiskBuffer = new Perquintill(hexToBN(tranche.trancheType.nonResidual.minRiskBuffer)) - interestRatePerSec = new Rate(hexToBN(tranche.trancheType.nonResidual.interestRatePerSec)) - } + const maxReserve = new CurrencyBalance(hexToBN(pool.reserve.max), currency.decimals) + const availableReserve = new CurrencyBalance(hexToBN(pool.reserve.available), currency.decimals) + const totalReserve = new CurrencyBalance(hexToBN(pool.reserve.total), currency.decimals) + + const lastUpdatedNav = new Date((portfolioValuationData?.lastUpdated ?? 0) * 1000).toISOString() + // @ts-expect-error + const rawNav = rawNavs && rawNavs[poolIndex]?.toJSON() + + const mappedPool: Pool = { + id: poolId, + createdAt: null, + metadata, + currency, + tranches: pool.tranches.tranches.map((tranche, index) => { + const trancheId = pool.tranches.ids[index] + const trancheKeyIndex = trancheIdToIndex[trancheId] + // const lastClosedEpoch = epochs[trancheKeyIndex] + + let minRiskBuffer: Perquintill | null = null + let interestRatePerSec: Rate | null = null + if ('nonResidual' in tranche.trancheType) { + minRiskBuffer = new Perquintill(hexToBN(tranche.trancheType.nonResidual.minRiskBuffer)) + interestRatePerSec = new Rate(hexToBN(tranche.trancheType.nonResidual.interestRatePerSec)) + } - const subordinateTranchesValue = new CurrencyBalance( - pool.tranches.tranches.slice(0, index).reduce((prev: BN, tranche: TrancheDetailsData) => { - return new BN(prev.add(new BN(hexToBN(tranche.debt))).add(new BN(hexToBN(tranche.reserve)))) - }, new BN(0)), - currency.decimals - ) - - // @ts-expect-error - const rawPrice = rawPrices?.[poolIndex]?.toPrimitive()?.[index] - const tokenPrice = rawPrice ? new Price(rawPrice) : Price.fromFloat(1) - - const currentRiskBuffer = subordinateTranchesValue.gtn(0) - ? Perquintill.fromFloat(subordinateTranchesValue.toDecimal().div(poolValue.toDecimal())) - : new Perquintill(0) - - const protection = minRiskBuffer?.toDecimal() ?? Dec(0) - const tvl = poolValue.toDecimal() - let capacityGivenMaxReserve = maxReserve.toDecimal().minus(totalReserve.toDecimal()) - capacityGivenMaxReserve = capacityGivenMaxReserve.lt(0) ? Dec(0) : capacityGivenMaxReserve - const capacityGivenProtection = protection.isZero() - ? capacityGivenMaxReserve - : currentRiskBuffer.toDecimal().div(protection).mul(tvl).minus(tvl) - const capacity = capacityGivenMaxReserve.gt(capacityGivenProtection) - ? capacityGivenProtection - : capacityGivenMaxReserve - - return { - id: trancheId, - index, - seniority: tranche.seniority, - tokenPrice, - poolCurrency: currency, - currency: findCurrency(currencies, { Tranche: [poolId, trancheId] })!, - totalIssuance: new TokenBalance(rawIssuances[trancheKeyIndex].toString(), currency.decimals), - poolId, - poolMetadata: (metadata ?? undefined) as string | undefined, - interestRatePerSec, - yield30DaysAnnualized: yield30DaysAnnualizedByPoolIdTrancheId?.[`${poolId}-${trancheId}`], - minRiskBuffer, - currentRiskBuffer, - capacity: CurrencyBalance.fromFloat(capacity, currency.decimals), - ratio: new Perquintill(hexToBN(tranche.ratio)), - lastUpdatedInterest: new Date(tranche.lastUpdatedInterest * 1000).toISOString(), - balance: new TokenBalance(hexToBN(tranche.debt).add(hexToBN(tranche.reserve)), currency.decimals), - } - }), - reserve: { - max: maxReserve, - available: availableReserve, - total: totalReserve, - }, - epoch: { - ...pool.epoch, - lastClosed: new Date(pool.epoch.lastClosed * 1000).toISOString(), - status: getEpochStatus(epochExecution, blockNumber), - challengePeriodEnd: epochExecution?.challengePeriodEnd, - }, - parameters: { - ...pool.parameters, - challengeTime: api.consts.poolSystem.challengeTime.toJSON() as number, // in blocks - }, - nav: { - lastUpdated: lastUpdatedNav, - total: rawNav?.total - ? new CurrencyBalance(hexToBN(rawNav.total).add(hexToBN(rawNav.navFees)), currency.decimals) - : new CurrencyBalance(0, currency.decimals), - aum: rawNav?.navAum - ? new CurrencyBalance(hexToBN(rawNav.navAum), currency.decimals) - : new CurrencyBalance(0, currency.decimals), - }, - value: rawNav?.total + const subordinateTranchesValue = new CurrencyBalance( + pool.tranches.tranches.slice(0, index).reduce((prev: BN, tranche: TrancheDetailsData) => { + return new BN(prev.add(new BN(hexToBN(tranche.debt))).add(new BN(hexToBN(tranche.reserve)))) + }, new BN(0)), + currency.decimals + ) + + // @ts-expect-error + const rawPrice = rawPrices?.[poolIndex]?.toPrimitive()?.[index] + const tokenPrice = rawPrice ? new Price(rawPrice) : Price.fromFloat(1) + + const currentRiskBuffer = subordinateTranchesValue.gtn(0) + ? Perquintill.fromFloat(subordinateTranchesValue.toDecimal().div(poolValue.toDecimal())) + : new Perquintill(0) + + const protection = minRiskBuffer?.toDecimal() ?? Dec(0) + const tvl = poolValue.toDecimal() + let capacityGivenMaxReserve = maxReserve.toDecimal().minus(totalReserve.toDecimal()) + capacityGivenMaxReserve = capacityGivenMaxReserve.lt(0) ? Dec(0) : capacityGivenMaxReserve + const capacityGivenProtection = protection.isZero() + ? capacityGivenMaxReserve + : currentRiskBuffer.toDecimal().div(protection).mul(tvl).minus(tvl) + const capacity = capacityGivenMaxReserve.gt(capacityGivenProtection) + ? capacityGivenProtection + : capacityGivenMaxReserve + + return { + id: trancheId, + index, + seniority: tranche.seniority, + tokenPrice, + poolCurrency: currency, + currency: findCurrency(currencies, { Tranche: [poolId, trancheId] })!, + totalIssuance: new TokenBalance(rawIssuances[trancheKeyIndex].toString(), currency.decimals), + poolId, + poolMetadata: (metadata ?? undefined) as string | undefined, + interestRatePerSec, + yield30DaysAnnualized: yield30DaysAnnualizedByPoolIdTrancheId?.[`${poolId}-${trancheId}`], + minRiskBuffer, + currentRiskBuffer, + capacity: CurrencyBalance.fromFloat(capacity, currency.decimals), + ratio: new Perquintill(hexToBN(tranche.ratio)), + lastUpdatedInterest: new Date(tranche.lastUpdatedInterest * 1000).toISOString(), + balance: new TokenBalance(hexToBN(tranche.debt).add(hexToBN(tranche.reserve)), currency.decimals), + } + }), + reserve: { + max: maxReserve, + available: availableReserve, + total: totalReserve, + }, + epoch: { + ...pool.epoch, + lastClosed: new Date(pool.epoch.lastClosed * 1000).toISOString(), + status: getEpochStatus(epochExecution, blockNumber), + challengePeriodEnd: epochExecution?.challengePeriodEnd, + }, + parameters: { + ...pool.parameters, + challengeTime: api.consts.poolSystem.challengeTime.toJSON() as number, // in blocks + }, + nav: { + lastUpdated: lastUpdatedNav, + total: rawNav?.total ? new CurrencyBalance(hexToBN(rawNav.total).add(hexToBN(rawNav.navFees)), currency.decimals) : new CurrencyBalance(0, currency.decimals), - } - - return mappedPool - }) + aum: rawNav?.navAum + ? new CurrencyBalance(hexToBN(rawNav.navAum), currency.decimals) + : new CurrencyBalance(0, currency.decimals), + }, + value: rawNav?.total + ? new CurrencyBalance(hexToBN(rawNav.total).add(hexToBN(rawNav.navFees)), currency.decimals) + : new CurrencyBalance(0, currency.decimals), + } - return mappedPools + return mappedPool }) - ) - } - ), + + return mappedPools + }) + ) + }), combineLatestWith($query), map(([pools, gqlResult]) => { return pools.map((pool) => { @@ -2141,6 +2130,55 @@ export function getPoolsModule(inst: Centrifuge) { ) } + function getPoolFees(args: [poolId: string]) { + const [poolId] = args + + const $events = inst.getEvents().pipe( + filter(({ api, events }) => { + const event = events.find( + ({ event }) => + api.events.poolFees.Charged.is(event) || + api.events.poolFees.Added.is(event) || + api.events.poolFees.Removed.is(event) || + api.events.poolFees.Proposed.is(event) + ) + return !!event + }) + ) + + return inst.getApi().pipe( + switchMap((api) => + combineLatest([api.call.poolFeesApi.listFees(poolId), getPoolCurrency([poolId])]).pipe( + map(([feesListData, currency]) => { + const feesList = feesListData.toJSON() as { bucket: 'Top'; fees: ActivePoolFeesData[] }[] + const fees: ActivePoolFees[] = feesList.flatMap((entry) => { + return entry.fees.map((fee) => { + const type = Object.keys(fee.amounts.feeType)[0] as FeeTypes + const limit = Object.keys(fee.amounts.feeType[type].limit)[0] as FeeLimits + const percentOfNav = new Rate(hexToBN(fee.amounts.feeType[type].limit[limit])) + return { + ...fee, + position: entry.bucket, + type, + limit, + amounts: { + percentOfNav, + pending: new CurrencyBalance( + new BN(fee.amounts.pending).iadd(new BN(fee.amounts.disbursement)), + currency.decimals + ), + }, + } + }) + }) + return fees + }), + repeatWhen(() => $events) + ) + ) + ) + } + function getLatestTrancheSnapshot(poolIdTrancheId: string) { return inst.getSubqueryObservable<{ trancheSnapshots: { nodes: { yield30DaysAnnualized: string | null; trancheId: string }[] } @@ -2178,11 +2216,14 @@ export function getPoolsModule(inst: Centrifuge) { totalReserve portfolioValuation blockNumber + sumPoolFeesChargedAmountByPeriod + sumPoolFeesAccruedAmountByPeriod sumBorrowedAmountByPeriod sumRepaidAmountByPeriod sumInvestedAmountByPeriod sumRedeemedAmountByPeriod sumInterestRepaidAmountByPeriod + value } pageInfo { hasNextPage @@ -2263,27 +2304,18 @@ export function getPoolsModule(inst: Centrifuge) { expand(({ trancheSnapshots, endCursor, hasNextPage }) => { if (!hasNextPage) return EMPTY return getTrancheSnapshotsWithCursor(filter, endCursor, from, to).pipe( - map( - ( - response: { - trancheSnapshots: { - nodes: SubqueryTrancheSnapshot[] - pageInfo: { hasNextPage: boolean; endCursor: string } - } - } | null - ) => { - if (response?.trancheSnapshots) { - const { endCursor, hasNextPage } = response.trancheSnapshots.pageInfo + map((response) => { + if (response?.trancheSnapshots) { + const { endCursor, hasNextPage } = response.trancheSnapshots.pageInfo - return { - endCursor, - hasNextPage, - trancheSnapshots: [...trancheSnapshots, ...response.trancheSnapshots.nodes], - } + return { + endCursor, + hasNextPage, + trancheSnapshots: [...trancheSnapshots, ...response.trancheSnapshots.nodes], } - return {} } - ) + return {} + }) ) }), takeLast(1), @@ -2315,27 +2347,18 @@ export function getPoolsModule(inst: Centrifuge) { expand(({ poolSnapshots, endCursor, hasNextPage }) => { if (!hasNextPage) return EMPTY return getPoolSnapshotsWithCursor(poolId, endCursor, from, to).pipe( - map( - ( - response: { - poolSnapshots: { - nodes: SubqueryPoolSnapshot[] - pageInfo: { hasNextPage: boolean; endCursor: string } - } - } | null - ) => { - if (response?.poolSnapshots) { - const { endCursor, hasNextPage } = response.poolSnapshots.pageInfo + map((response) => { + if (response?.poolSnapshots) { + const { endCursor, hasNextPage } = response.poolSnapshots.pageInfo - return { - endCursor, - hasNextPage, - poolSnapshots: [...poolSnapshots, ...response.poolSnapshots.nodes], - } + return { + endCursor, + hasNextPage, + poolSnapshots: [...poolSnapshots, ...response.poolSnapshots.nodes], } - return {} } - ) + return {} + }) ) }) ), @@ -2343,27 +2366,18 @@ export function getPoolsModule(inst: Centrifuge) { expand(({ trancheSnapshots, endCursor, hasNextPage }) => { if (!hasNextPage) return EMPTY return getTrancheSnapshotsWithCursor({ poolId }, endCursor, from, to).pipe( - map( - ( - response: { - trancheSnapshots: { - nodes: SubqueryTrancheSnapshot[] - pageInfo: { hasNextPage: boolean; endCursor: string } - } - } | null - ) => { - if (response?.trancheSnapshots) { - const { endCursor, hasNextPage } = response.trancheSnapshots.pageInfo + map((response) => { + if (response?.trancheSnapshots) { + const { endCursor, hasNextPage } = response.trancheSnapshots.pageInfo - return { - endCursor, - hasNextPage, - trancheSnapshots: [...trancheSnapshots, ...response.trancheSnapshots.nodes], - } + return { + endCursor, + hasNextPage, + trancheSnapshots: [...trancheSnapshots, ...response.trancheSnapshots.nodes], } - return {} } - ) + return {} + }) ) }) ), @@ -2387,6 +2401,22 @@ export function getPoolsModule(inst: Centrifuge) { id: state.id, portfolioValuation: new CurrencyBalance(state.portfolioValuation, poolCurrency.decimals), totalReserve: new CurrencyBalance(state.totalReserve, poolCurrency.decimals), + sumPoolFeesChargedAmountByPeriod: new CurrencyBalance( + state.sumPoolFeesChargedAmountByPeriod ?? 0, + poolCurrency.decimals + ), + sumPoolFeesAccruedAmountByPeriod: new CurrencyBalance( + state.sumPoolFeesAccruedAmountByPeriod ?? 0, + poolCurrency.decimals + ), + sumBorrowedAmountByPeriod: new CurrencyBalance(state.sumBorrowedAmountByPeriod, poolCurrency.decimals), + sumInterestRepaidAmountByPeriod: new CurrencyBalance( + state.sumInterestRepaidAmountByPeriod, + poolCurrency.decimals + ), + sumRepaidAmountByPeriod: new CurrencyBalance(state.sumRepaidAmountByPeriod, poolCurrency.decimals), + sumInvestedAmountByPeriod: new CurrencyBalance(state.sumInvestedAmountByPeriod, poolCurrency.decimals), + sumRedeemedAmountByPeriod: new CurrencyBalance(state.sumRedeemedAmountByPeriod, poolCurrency.decimals), } const poolValue = new CurrencyBalance(new BN(state?.portfolioValuation || '0'), poolCurrency.decimals) @@ -2399,6 +2429,7 @@ export function getPoolsModule(inst: Centrifuge) { tranches[tid] = { id: tranche.trancheId, price: tranche.tokenPrice ? new Price(tranche.tokenPrice) : null, + tokenSupply: new TokenBalance(tranche.tokenSupply, poolCurrency.decimals), fulfilledInvestOrders: new CurrencyBalance( tranche.sumFulfilledInvestOrdersByPeriod, poolCurrency.decimals @@ -2596,6 +2627,7 @@ export function getPoolsModule(inst: Centrifuge) { chainId evmAddress } + hash poolId trancheId epochNumber @@ -2620,7 +2652,6 @@ export function getPoolsModule(inst: Centrifuge) { return combineLatest([$query, getPoolCurrency([poolId])]).pipe( switchMap(([queryData, currency]) => { const currencyDecimals = currency.decimals - return [ queryData?.investorTransactions.nodes.map((tx) => { return { @@ -2629,15 +2660,16 @@ export function getPoolsModule(inst: Centrifuge) { accountId: tx.accountId, chainId: Number(tx.account.chainId), evmAddress: tx.account.evmAddress, - trancheId: tx.trancheId, + trancheId: tx.trancheId.split('-')[1], epochNumber: tx.epochNumber, type: tx.type as InvestorTransactionType, currencyAmount: tx.currencyAmount ? new CurrencyBalance(tx.currencyAmount, currencyDecimals) : undefined, tokenAmount: tx.tokenAmount ? new CurrencyBalance(tx.tokenAmount, currencyDecimals) : undefined, tokenPrice: tx.tokenPrice ? new Price(tx.tokenPrice) : undefined, transactionFee: tx.transactionFee ? new CurrencyBalance(tx.transactionFee, 18) : undefined, // native tokenks are always denominated in 18 - } - }) as unknown as InvestorTransaction[], + hash: tx.hash, + } satisfies InvestorTransaction + }), ] }) ) @@ -2669,16 +2701,19 @@ export function getPoolsModule(inst: Centrifuge) { asset { id metadata + name type } fromAsset { id metadata + name type } toAsset { id metadata + name type } } @@ -2702,7 +2737,7 @@ export function getPoolsModule(inst: Centrifuge) { principalAmount: tx.principalAmount ? new CurrencyBalance(tx.principalAmount, currency.decimals) : undefined, interestAmount: tx.interestAmount ? new CurrencyBalance(tx.interestAmount, currency.decimals) : undefined, timestamp: new Date(`${tx.timestamp}+00:00`), - })) as unknown as AssetTransaction[] + })) satisfies AssetTransaction[] }) ) } @@ -2745,19 +2780,93 @@ export function getPoolsModule(inst: Centrifuge) { return $query.pipe( switchMap(() => combineLatest([$query, getPoolCurrency([poolId])])), map(([data, currency]) => { - return data!.poolFeeTransactions.nodes.map((tx) => ({ + return data!.poolFeeTransactions.nodes.map( + (tx) => + ({ + ...tx, + amount: tx.amount ? new CurrencyBalance(tx.amount, currency.decimals) : undefined, + timestamp: new Date(`${tx.timestamp}+00:00`), + poolFee: { + feeId: Number(tx.poolFee.feeId), + }, + } satisfies PoolFeeTransaction) + ) + }) + ) + } + + function getAssetSnapshots(args: [poolId: string, loanId: string, from?: Date, to?: Date]) { + const [poolId, loanId, from, to] = args + + const $query = inst.getSubqueryObservable<{ + assetSnapshots: { nodes: SubqueryAssetSnapshot[] } + }>( + `query($assetId: String!, $from: Datetime!, $to: Datetime!) { + assetSnapshots( + first: 1000, + orderBy: TIMESTAMP_ASC, + filter: { + assetId: { equalTo: $assetId }, + timestamp: { greaterThan: $from, lessThan: $to } + } + ) { + nodes { + assetId + timestamp + presentValue + outstandingPrincipal + outstandingInterest + outstandingDebt + outstandingQuantity + totalBorrowed + totalRepaidPrincipal + totalRepaidInterest + totalRepaidUnscheduled + } + } + } + `, + { + assetId: `${poolId}-${loanId}`, + from: from ? from.toISOString() : getDateYearsFromNow(-10).toISOString(), + to: to ? to.toISOString() : new Date().toISOString(), + }, + false + ) + + return $query.pipe( + switchMap(() => combineLatest([$query, getPoolCurrency([poolId])])), + map(([data, currency]) => { + return data!.assetSnapshots.nodes.map((tx) => ({ ...tx, - amount: tx.amount ? new CurrencyBalance(tx.amount, currency.decimals) : undefined, + presentValue: tx.presentValue ? new CurrencyBalance(tx.presentValue, currency.decimals) : undefined, + outstandingPrincipal: tx.outstandingPrincipal + ? new CurrencyBalance(tx.outstandingPrincipal, currency.decimals) + : undefined, + outstandingInterest: tx.outstandingInterest + ? new CurrencyBalance(tx.outstandingInterest, currency.decimals) + : undefined, + outstandingDebt: tx.outstandingDebt ? new CurrencyBalance(tx.outstandingDebt, currency.decimals) : undefined, + outstandingQuantity: tx.outstandingQuantity + ? new CurrencyBalance(tx.outstandingQuantity, currency.decimals) + : undefined, + totalBorrowed: tx.totalBorrowed ? new CurrencyBalance(tx.totalBorrowed, currency.decimals) : undefined, + totalRepaidPrincipal: tx.totalRepaidPrincipal + ? new CurrencyBalance(tx.totalRepaidPrincipal, currency.decimals) + : undefined, + totalRepaidInterest: tx.totalRepaidInterest + ? new CurrencyBalance(tx.totalRepaidInterest, currency.decimals) + : undefined, + totalRepaidUnscheduled: tx.totalRepaidUnscheduled + ? new CurrencyBalance(tx.totalRepaidUnscheduled, currency.decimals) + : undefined, timestamp: new Date(`${tx.timestamp}+00:00`), - poolFee: { - feeId: Number(tx.poolFee.feeId), - }, - })) as unknown as PoolFeeTransaction[] + })) satisfies AssetSnapshot[] }) ) } - function getHolders(args: [poolId: string, trancheId?: string]) { + function getInvestors(args: [poolId: string, trancheId?: string]) { const [poolId, trancheId] = args const $query = inst.getApi().pipe( switchMap(() => { @@ -2772,6 +2881,7 @@ export function getPoolsModule(inst: Centrifuge) { }) { nodes { accountId + trancheId account { chainId evmAddress @@ -2795,6 +2905,9 @@ export function getPoolsModule(inst: Centrifuge) { chainId evmAddress } + currency { + trancheId + } currencyId amount } @@ -2813,32 +2926,30 @@ export function getPoolsModule(inst: Centrifuge) { switchMap(() => combineLatest([$query, getPoolCurrency([poolId])])), map(([data, currency]) => { // TODO: this should be a map by account ID + tranche ID - const currencyBalancesByAccountId = data!.currencyBalances.nodes.reduce((obj, balance) => { - if (balance.accountId in obj) obj[balance.accountId].push(balance) - else obj[balance.accountId] = [balance] - return obj - }, {} as any) - - return data!.trancheBalances.nodes.map((balance) => ({ - accountId: balance.accountId, - chainId: Number(balance.account?.chainId || 2031), - evmAddress: balance.account?.evmAddress, - balance: new CurrencyBalance( - balance.accountId in currencyBalancesByAccountId - ? currencyBalancesByAccountId[balance.accountId].reduce( - (sum: BN, balance: SubqueryCurrencyBalances) => sum.add(new BN(balance.amount)), - new BN(0) - ) - : '0', - currency.decimals - ), - pendingInvestCurrency: new CurrencyBalance(balance.pendingInvestCurrency, currency.decimals), - claimableTrancheTokens: new CurrencyBalance(balance.claimableTrancheTokens, currency.decimals), - sumClaimedTrancheTokens: new CurrencyBalance(balance.sumClaimedTrancheTokens, currency.decimals), - pendingRedeemTrancheTokens: new CurrencyBalance(balance.pendingRedeemTrancheTokens, currency.decimals), - claimableCurrency: new CurrencyBalance(balance.claimableCurrency, currency.decimals), - sumClaimedCurrency: new CurrencyBalance(balance.sumClaimedCurrency, currency.decimals), - })) as unknown as Holder[] + const currencyBalancesByAccountId: Record = {} + data!.currencyBalances.nodes.forEach((balance) => { + currencyBalancesByAccountId[`${balance.accountId}-${balance.currency.trancheId?.split('-')[1]}`] = balance + }) + + return data!.trancheBalances.nodes.map( + (balance) => + ({ + accountId: balance.accountId, + chainId: Number(balance.account?.chainId ?? 0), + trancheId: balance.trancheId.split('-')[1], + evmAddress: balance.account?.evmAddress, + balance: new CurrencyBalance( + currencyBalancesByAccountId[`${balance.accountId}-${balance.trancheId.split('-')[1]}`]?.amount ?? 0, + currency.decimals + ), + pendingInvestCurrency: new CurrencyBalance(balance.pendingInvestCurrency, currency.decimals), + claimableTrancheTokens: new CurrencyBalance(balance.claimableTrancheTokens, currency.decimals), + sumClaimedTrancheTokens: new CurrencyBalance(balance.sumClaimedTrancheTokens, currency.decimals), + pendingRedeemTrancheTokens: new CurrencyBalance(balance.pendingRedeemTrancheTokens, currency.decimals), + claimableCurrency: new CurrencyBalance(balance.claimableCurrency, currency.decimals), + sumClaimedCurrency: new CurrencyBalance(balance.sumClaimedCurrency, currency.decimals), + } satisfies Holder) + ) }) ) } @@ -3625,6 +3736,7 @@ export function getPoolsModule(inst: Centrifuge) { return $api.pipe( switchMap((api) => api.query.poolFees.lastFeeId()), + take(1), combineLatestWith($api), switchMap(([lastFeeId, api]) => { const removeSubmittables = remove.map((feeId) => api.tx.poolFees.removeFee([feeId])) @@ -3646,7 +3758,7 @@ export function getPoolsModule(inst: Centrifuge) { }) || []), ...add.map((metadata, index) => { return { - id: parseInt(lastFeeId.toHuman() as string) + index + 1, + id: parseInt(lastFeeId.toHuman() as string, 10) + index + 1, name: metadata.fee.name, feePosition: metadata.fee.feePosition, } @@ -3685,7 +3797,7 @@ export function getPoolsModule(inst: Centrifuge) { const $api = inst.getApi() return $api.pipe( switchMap((api) => combineLatest([api.query.poolFees.lastFeeId()])), - map((feeId) => parseInt(feeId[0].toHuman() as string) + 1) + map((feeId) => parseInt(feeId[0].toHuman() as string, 10) + 1) ) } @@ -3770,6 +3882,7 @@ export function getPoolsModule(inst: Centrifuge) { getPoolOrders, getPortfolio, getLoans, + getPoolFees, getPendingCollect, getWriteOffPolicy, getProposedPoolSystemChanges, @@ -3784,12 +3897,13 @@ export function getPoolsModule(inst: Centrifuge) { getInvestorTransactions, getAssetTransactions, getFeeTransactions, + getAssetSnapshots, getNativeCurrency, getCurrencies, getDailyTrancheStates, getTransactionsByAddress, getDailyTVL, - getHolders, + getInvestors, } } diff --git a/centrifuge-js/src/types/subquery.ts b/centrifuge-js/src/types/subquery.ts index a61df4e2fc..90fd7af9b3 100644 --- a/centrifuge-js/src/types/subquery.ts +++ b/centrifuge-js/src/types/subquery.ts @@ -1,24 +1,18 @@ import { CurrencyBalance, Price } from '../utils/BN' export type SubqueryPoolSnapshot = { - __typename?: 'PoolSnapshot' id: string timestamp: string + value: string portfolioValuation: number totalReserve: number - availableReserve: number - maxReserve: number - totalDebt?: number | null - totalBorrowed?: number | null - totalRepaid?: number | null - totalInvested?: number | null - totalRedeemed?: number | null - sumBorrowedAmount?: number | null - sumBorrowedAmountByPeriod?: string | null - sumInterestRepaidAmountByPeriod?: string | null - sumRepaidAmountByPeriod?: number | null - sumInvestedAmountByPeriod?: number | null - sumRedeemedAmountByPeriod?: number | null + sumPoolFeesChargedAmountByPeriod: string | null + sumPoolFeesAccruedAmountByPeriod: string | null + sumBorrowedAmountByPeriod: string + sumInterestRepaidAmountByPeriod: string + sumRepaidAmountByPeriod: string + sumInvestedAmountByPeriod: string + sumRedeemedAmountByPeriod: string blockNumber: number } @@ -33,12 +27,11 @@ export type SubqueryTrancheSnapshot = { poolId: string trancheId: string } - tokenSupply?: number | null - - sumOutstandingInvestOrdersByPeriod: number - sumOutstandingRedeemOrdersByPeriod: number - sumFulfilledInvestOrdersByPeriod: number - sumFulfilledRedeemOrdersByPeriod: number + tokenSupply: string + sumOutstandingInvestOrdersByPeriod: string + sumOutstandingRedeemOrdersByPeriod: string + sumFulfilledInvestOrdersByPeriod: string + sumFulfilledRedeemOrdersByPeriod: string } export type InvestorTransactionType = @@ -69,13 +62,19 @@ export type SubqueryInvestorTransaction = { epochNumber: number type: InvestorTransactionType hash: string - currencyAmount?: CurrencyBalance | number | null - tokenAmount?: CurrencyBalance | number | null - tokenPrice?: Price | number | null - transactionFee?: number | null + currencyAmount?: CurrencyBalance | null + tokenAmount?: CurrencyBalance | null + tokenPrice?: Price | null + transactionFee?: string | null } -export type AssetTransactionType = 'CREATED' | 'PRICED' | 'BORROWED' | 'REPAID' | 'CLOSED' +export type AssetTransactionType = 'CREATED' | 'PRICED' | 'BORROWED' | 'REPAID' | 'CLOSED' | 'CASH_TRANSFER' + +export enum AssetType { + OnchainCash = 'OnchainCash', + OffchainCash = 'OffchainCash', + Other = 'Other', +} export type SubqueryAssetTransaction = { __typename?: 'AssetTransaction' @@ -83,6 +82,7 @@ export type SubqueryAssetTransaction = { timestamp: string poolId: string accountId: string + hash: string epochId: string type: AssetTransactionType amount: CurrencyBalance | undefined @@ -93,7 +93,41 @@ export type SubqueryAssetTransaction = { asset: { id: string metadata: string + name: string + type: AssetType + } + fromAsset?: { + id: string + metadata: string + name: string + type: AssetType + } + toAsset?: { + id: string + metadata: string + name: string + type: AssetType + } +} + +export type SubqueryAssetSnapshot = { + __typename?: 'AssetSnapshot' + asset: { + id: string + metadata: string + name: string + type: AssetType } + timestamp: string + presentValue: string + outstandingPrincipal: string + outstandingInterest: string + outstandingDebt: string + outstandingQuantity: string + totalBorrowed: string + totalRepaidPrincipal: string + totalRepaidInterest: string + totalRepaidUnscheduled: string } export type PoolFeeTransactionType = 'PROPOSED' | 'ADDED' | 'REMOVED' | 'CHARGED' | 'UNCHARGED' | 'PAID' | 'ACCRUED' @@ -133,6 +167,9 @@ export type SubqueryCurrencyBalances = { __typename?: 'CurrencyBalances' id: string accountId: string + currency: { + trancheId: string | null + } account: { chainId: string evmAddress?: string