From 09fcd8d57ae47f7ea1242bfdfe74fc8f8e3c71cd Mon Sep 17 00:00:00 2001 From: JP Date: Mon, 12 Feb 2024 10:43:46 -0600 Subject: [PATCH 01/19] pool detail page layout redesign (#1949) --- .../PoolOverview/AssetsByMaturity.tsx | 9 ++ .../src/components/PoolOverview/Cashflows.tsx | 9 ++ .../components/PoolOverview/KeyMetrics.tsx | 9 ++ .../components/PoolOverview/PoolAnalysis.tsx | 9 ++ .../PoolOverview/PoolPerfomance.tsx | 9 ++ .../components/PoolOverview/PoolStructure.tsx | 9 ++ .../{ => PoolOverview}/TrancheTokenCards.tsx | 8 +- .../PoolOverview/TransactionHistory.tsx | 9 ++ .../src/pages/Pool/Overview/index.tsx | 88 ++++++++++++++++--- 9 files changed, 142 insertions(+), 17 deletions(-) create mode 100644 centrifuge-app/src/components/PoolOverview/AssetsByMaturity.tsx create mode 100644 centrifuge-app/src/components/PoolOverview/Cashflows.tsx create mode 100644 centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx create mode 100644 centrifuge-app/src/components/PoolOverview/PoolAnalysis.tsx create mode 100644 centrifuge-app/src/components/PoolOverview/PoolPerfomance.tsx create mode 100644 centrifuge-app/src/components/PoolOverview/PoolStructure.tsx rename centrifuge-app/src/components/{ => PoolOverview}/TrancheTokenCards.tsx (94%) create mode 100644 centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx diff --git a/centrifuge-app/src/components/PoolOverview/AssetsByMaturity.tsx b/centrifuge-app/src/components/PoolOverview/AssetsByMaturity.tsx new file mode 100644 index 0000000000..c939643166 --- /dev/null +++ b/centrifuge-app/src/components/PoolOverview/AssetsByMaturity.tsx @@ -0,0 +1,9 @@ +import { Card } from '@centrifuge/fabric' + +export const AssetsByMaturity = () => { + return ( + + Asset By Maturity + + ) +} diff --git a/centrifuge-app/src/components/PoolOverview/Cashflows.tsx b/centrifuge-app/src/components/PoolOverview/Cashflows.tsx new file mode 100644 index 0000000000..f663d00835 --- /dev/null +++ b/centrifuge-app/src/components/PoolOverview/Cashflows.tsx @@ -0,0 +1,9 @@ +import { Card } from '@centrifuge/fabric' + +export const Cashflows = () => { + return ( + + Cashflows + + ) +} diff --git a/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx b/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx new file mode 100644 index 0000000000..f29323cb48 --- /dev/null +++ b/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx @@ -0,0 +1,9 @@ +import { Card } from '@centrifuge/fabric' + +export const KeyMetrics = () => { + return ( + + Key Metrics + + ) +} diff --git a/centrifuge-app/src/components/PoolOverview/PoolAnalysis.tsx b/centrifuge-app/src/components/PoolOverview/PoolAnalysis.tsx new file mode 100644 index 0000000000..13d3767132 --- /dev/null +++ b/centrifuge-app/src/components/PoolOverview/PoolAnalysis.tsx @@ -0,0 +1,9 @@ +import { Card } from '@centrifuge/fabric' + +export const PoolAnalysis = () => { + return ( + + Pool Analysis + + ) +} diff --git a/centrifuge-app/src/components/PoolOverview/PoolPerfomance.tsx b/centrifuge-app/src/components/PoolOverview/PoolPerfomance.tsx new file mode 100644 index 0000000000..e3458f8d6e --- /dev/null +++ b/centrifuge-app/src/components/PoolOverview/PoolPerfomance.tsx @@ -0,0 +1,9 @@ +import { Card } from '@centrifuge/fabric' + +export const PoolPerformance = () => { + return ( + + Pool Performance + + ) +} diff --git a/centrifuge-app/src/components/PoolOverview/PoolStructure.tsx b/centrifuge-app/src/components/PoolOverview/PoolStructure.tsx new file mode 100644 index 0000000000..adfac63730 --- /dev/null +++ b/centrifuge-app/src/components/PoolOverview/PoolStructure.tsx @@ -0,0 +1,9 @@ +import { Card } from '@centrifuge/fabric' + +export const PoolStructure = () => { + return ( + + Pool Structure + + ) +} diff --git a/centrifuge-app/src/components/TrancheTokenCards.tsx b/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx similarity index 94% rename from centrifuge-app/src/components/TrancheTokenCards.tsx rename to centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx index 1d45e6c283..cba3047572 100644 --- a/centrifuge-app/src/components/TrancheTokenCards.tsx +++ b/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx @@ -1,9 +1,9 @@ import { Perquintill } from '@centrifuge/centrifuge-js' import { Box, Shelf, Stack, Text } from '@centrifuge/fabric' -import { InvestButton, Token } from '../pages/Pool/Overview' -import { daysBetween } from '../utils/date' -import { formatBalance, formatPercentage } from '../utils/formatting' -import { Tooltips } from './Tooltips' +import { InvestButton, Token } from '../../pages/Pool/Overview' +import { daysBetween } from '../../utils/date' +import { formatBalance, formatPercentage } from '../../utils/formatting' +import { Tooltips } from '../Tooltips' export const TrancheTokenCards = ({ trancheTokens, diff --git a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx new file mode 100644 index 0000000000..d5032b88f4 --- /dev/null +++ b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx @@ -0,0 +1,9 @@ +import { Card } from '@centrifuge/fabric' + +export const TransactionHistory = () => { + return ( + + Transaction History + + ) +} diff --git a/centrifuge-app/src/pages/Pool/Overview/index.tsx b/centrifuge-app/src/pages/Pool/Overview/index.tsx index e1c0b76e6e..4caf1210b7 100644 --- a/centrifuge-app/src/pages/Pool/Overview/index.tsx +++ b/centrifuge-app/src/pages/Pool/Overview/index.tsx @@ -1,23 +1,26 @@ import { CurrencyBalance, Price } from '@centrifuge/centrifuge-js' import { useWallet } from '@centrifuge/centrifuge-react' -import { Button, Shelf, Stack, Text, TextWithPlaceholder } from '@centrifuge/fabric' +import { Box, Button, Grid, TextWithPlaceholder } from '@centrifuge/fabric' import Decimal from 'decimal.js-light' import * as React from 'react' import { useLocation, useParams } from 'react-router' +import { useTheme } from 'styled-components' import { InvestRedeemProps } from '../../../components/InvestRedeem/InvestRedeem' import { InvestRedeemDrawer } from '../../../components/InvestRedeem/InvestRedeemDrawer' -import { IssuerSection } from '../../../components/IssuerSection' -import { LabelValueStack } from '../../../components/LabelValueStack' import { LayoutBase } from '../../../components/LayoutBase' import { LoadBoundary } from '../../../components/LoadBoundary' -import { PageSection } from '../../../components/PageSection' -import { PageSummary } from '../../../components/PageSummary' -import { PoolToken } from '../../../components/PoolToken' +import { AssetsByMaturity } from '../../../components/PoolOverview/AssetsByMaturity' +import { Cashflows } from '../../../components/PoolOverview/Cashflows' +import { KeyMetrics } from '../../../components/PoolOverview/KeyMetrics' +import { PoolAnalysis } from '../../../components/PoolOverview/PoolAnalysis' +import { PoolPerformance } from '../../../components/PoolOverview/PoolPerfomance' +import { PoolStructure } from '../../../components/PoolOverview/PoolStructure' +import { TrancheTokenCards } from '../../../components/PoolOverview/TrancheTokenCards' +import { TransactionHistory } from '../../../components/PoolOverview/TransactionHistory' import { Spinner } from '../../../components/Spinner' import { Tooltips } from '../../../components/Tooltips' import { Dec } from '../../../utils/Decimal' -import { formatDate } from '../../../utils/date' -import { formatBalance, formatBalanceAbbreviated, formatPercentage } from '../../../utils/formatting' +import { formatBalance } from '../../../utils/formatting' import { getPoolValueLocked } from '../../../utils/getPoolValueLocked' import { useTinlakePermissions } from '../../../utils/tinlake/useTinlakePermissions' import { useAverageMaturity } from '../../../utils/useAverageMaturity' @@ -58,6 +61,7 @@ function AverageMaturity({ poolId }: { poolId: string }) { } export function PoolDetailOverview() { + const theme = useTheme() const { pid: poolId } = useParams<{ pid: string }>() const isTinlakePool = poolId.startsWith('0x') const { state } = useLocation<{ token: string }>() @@ -123,9 +127,59 @@ export function PoolDetailOverview() { } return ( - <> - - {!isTinlakePool && ( + + + + }> + + + }> + + + + + {tokens.length > 0 && ( + + }> + + + + )} + + }> + + + + + + }> + + + }> + + + + + + }> + + + + + + + }> + + + + + + {/* {!isTinlakePool && ( }> @@ -188,8 +242,8 @@ export function PoolDetailOverview() { - - + */} + ) } @@ -206,3 +260,11 @@ export function InvestButton(props: InvestRedeemProps) { ) } + +const PoolOverviewSection = ({ children }: { children: React.ReactNode }) => { + return ( + + {children} + + ) +} From f063a4529ecb60cf1d653dc7cd6c28012a6d162b Mon Sep 17 00:00:00 2001 From: JP Date: Tue, 13 Feb 2024 09:40:20 -0600 Subject: [PATCH 02/19] feat: key metrics card (#1952) * feat: key metrics card * use status for pool type --- .../components/PoolOverview/KeyMetrics.tsx | 94 ++++++++++++++++++- .../src/pages/Pool/Overview/index.tsx | 12 ++- centrifuge-app/src/utils/date.ts | 5 +- 3 files changed, 104 insertions(+), 7 deletions(-) diff --git a/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx b/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx index f29323cb48..c94f3670de 100644 --- a/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx +++ b/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx @@ -1,9 +1,95 @@ -import { Card } from '@centrifuge/fabric' +import { ActiveLoan, Loan, TinlakeLoan } from '@centrifuge/centrifuge-js' +import { Box, Card, Shelf, Stack, Text } from '@centrifuge/fabric' +import capitalize from 'lodash/capitalize' +import startCase from 'lodash/startCase' +import { daysBetween } from '../../utils/date' + +type Props = { + assetType?: { class: string; subClass: string } + averageMaturity: string + loans: TinlakeLoan[] | Loan[] | null | undefined + poolStatus?: string +} + +export const KeyMetrics = ({ assetType, averageMaturity, loans, poolStatus }: Props) => { + const ongoingAssetCount = + loans && [...loans].filter((loan) => loan.status === 'Active' && !loan.outstandingDebt.isZero()).length + + const writtenOffAssetCount = + loans && [...loans].filter((loan) => loan.status === 'Active' && (loan as ActiveLoan).writeOffStatus).length + + const overdueAssetCount = + loans && + [...loans].filter((loan) => { + const today = new Date() + today.setUTCHours(0, 0, 0, 0) + const days = daysBetween(today, loan.pricing.maturityDate) + return loan.status === 'Active' && loan.pricing.maturityDate && days < 0 + }).length + + const metrics = [ + { + metric: 'Asset class', + value: `${capitalize(startCase(assetType?.class)).replace(/^Us /, 'US ')} - ${capitalize( + startCase(assetType?.subClass) + ).replace(/^Us /, 'US ')}`, + }, + { + metric: 'Pool type', + value: capitalize(poolStatus), + }, + { + metric: 'Average asset maturity', + value: averageMaturity, + }, + { + metric: 'Total assets', + value: loans?.length || 0, + }, + { + metric: 'Ongoing assets', + value: ongoingAssetCount, + }, + { + metric: 'Written off assets', + value: writtenOffAssetCount, + }, + { + metric: 'Overdue assets', + value: overdueAssetCount, + }, + ] -export const KeyMetrics = () => { return ( - - Key Metrics + + + + Key metrics + + + {metrics.map(({ metric, value }, index) => ( + + + + {metric} + + + + + {value} + + + + ))} + + ) } diff --git a/centrifuge-app/src/pages/Pool/Overview/index.tsx b/centrifuge-app/src/pages/Pool/Overview/index.tsx index 4caf1210b7..8bc9d7b06e 100644 --- a/centrifuge-app/src/pages/Pool/Overview/index.tsx +++ b/centrifuge-app/src/pages/Pool/Overview/index.tsx @@ -25,6 +25,7 @@ import { getPoolValueLocked } from '../../../utils/getPoolValueLocked' import { useTinlakePermissions } from '../../../utils/tinlake/useTinlakePermissions' import { useAverageMaturity } from '../../../utils/useAverageMaturity' import { useConnectBeforeAction } from '../../../utils/useConnectBeforeAction' +import { useLoans } from '../../../utils/useLoans' import { usePool, usePoolMetadata } from '../../../utils/usePools' import { PoolDetailHeader } from '../Header' @@ -69,6 +70,8 @@ export function PoolDetailOverview() { const { data: metadata, isLoading: metadataIsLoading } = usePoolMetadata(pool) const { evm } = useWallet() const { data: tinlakePermissions } = useTinlakePermissions(poolId, evm?.selectedAddress || '') + const averageMaturity = useAverageMaturity(poolId) + const loans = useLoans(poolId) const pageSummaryData = [ { @@ -129,12 +132,17 @@ export function PoolDetailOverview() { return ( - + }> }> - + diff --git a/centrifuge-app/src/utils/date.ts b/centrifuge-app/src/utils/date.ts index ff949fe89f..1ce028ddc3 100644 --- a/centrifuge-app/src/utils/date.ts +++ b/centrifuge-app/src/utils/date.ts @@ -25,7 +25,10 @@ export const formatAge = (ageInDays: number, decimals: number = 1) => { } else if (ageInDays < 0) { return '0 days' } - return `${Math.floor(ageInDays)} days` + + const days = Math.floor(ageInDays) + + return `${days} ${days === 1 ? 'day' : 'days'}` } export const getAge = (createdAt: string | undefined | null) => { From ac589885625b07a7fe0e5cd3ecdd6adb8961383f Mon Sep 17 00:00:00 2001 From: JP Date: Thu, 15 Feb 2024 13:47:59 -0600 Subject: [PATCH 03/19] feat: pool structure card (#1957) --- .../components/PoolOverview/KeyMetrics.tsx | 30 ++--- .../components/PoolOverview/PoolStructure.tsx | 117 +++++++++++++++++- .../src/pages/Pool/Overview/index.tsx | 98 +-------------- 3 files changed, 132 insertions(+), 113 deletions(-) diff --git a/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx b/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx index c94f3670de..b9a2a6f15f 100644 --- a/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx +++ b/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx @@ -1,5 +1,5 @@ import { ActiveLoan, Loan, TinlakeLoan } from '@centrifuge/centrifuge-js' -import { Box, Card, Shelf, Stack, Text } from '@centrifuge/fabric' +import { Box, Card, Grid, Stack, Text } from '@centrifuge/fabric' import capitalize from 'lodash/capitalize' import startCase from 'lodash/startCase' import { daysBetween } from '../../utils/date' @@ -61,32 +61,32 @@ export const KeyMetrics = ({ assetType, averageMaturity, loans, poolStatus }: Pr ] return ( - + Key metrics - + {metrics.map(({ metric, value }, index) => ( - - - - {metric} - - - - - {value} - - - + + {metric} + + + {value} + + ))} diff --git a/centrifuge-app/src/components/PoolOverview/PoolStructure.tsx b/centrifuge-app/src/components/PoolOverview/PoolStructure.tsx index adfac63730..311a40f933 100644 --- a/centrifuge-app/src/components/PoolOverview/PoolStructure.tsx +++ b/centrifuge-app/src/components/PoolOverview/PoolStructure.tsx @@ -1,9 +1,118 @@ -import { Card } from '@centrifuge/fabric' +import { getChainInfo, useWallet } from '@centrifuge/centrifuge-react' +import { Box, Card, Grid, Stack, Text, Tooltip } from '@centrifuge/fabric' +import capitalize from 'lodash/capitalize' +import { formatDate } from '../../utils/date' +import { useActiveDomains } from '../../utils/useLiquidityPools' +import { useInvestorTransactions } from '../../utils/usePools' + +type Props = { + numOfTranches: number + poolId: string + poolStatus?: string +} + +export const PoolStructure = ({ numOfTranches, poolId, poolStatus }: Props) => { + const investorTransactions = useInvestorTransactions(poolId) + const { data: domains } = useActiveDomains(poolId) + const { + evm: { chains }, + } = useWallet() + + const firstInvestment = investorTransactions?.find( + (investorTransaction) => investorTransaction.type === 'INVEST_EXECUTION' + )?.timestamp + const deployedLpChains = + domains?.map((domain) => { + return getChainInfo(chains, domain.chainId).name + }) ?? [] + + const metrics = [ + { + metric: 'Pool type', + value: capitalize(poolStatus), + }, + { + metric: 'Pool structure', + value: 'Revolving pool', + }, + { + metric: 'Tranche structure', + value: numOfTranches === 1 ? 'Unitranche' : `${numOfTranches} tranches`, + }, + { + metric: 'First investment', + value: firstInvestment ? formatDate(firstInvestment) : '-', + }, + { + metric: 'Available network', + value: `Centrifuge${deployedLpChains.length ? `, ${deployedLpChains.join(', ')}` : ''}`, + }, + // { + // metric: 'Protocol fee', + // value: '1% of NAV', // TODO: get fees + // }, + // { + // metric: 'Priority fee', + // value: '1% of NAV', // TODO: get fees + // }, + // { + // metric: 'Manangement fee', + // value: '1% of NAV', // TODO: get fees + // }, + ] + + const getValue = (metric: string, value: string) => { + if (metric === 'Pool structure') + return ( + + + {value} + + + ) + if (metric === 'Tranche structure') + return ( + + + {value} + + + ) + return ( + + {value} + + ) + } -export const PoolStructure = () => { return ( - - Pool Structure + + + + Structure + + + {metrics.map(({ metric, value }, index) => ( + + + {metric} + + {getValue(metric, value)} + + ))} + + ) } diff --git a/centrifuge-app/src/pages/Pool/Overview/index.tsx b/centrifuge-app/src/pages/Pool/Overview/index.tsx index 8bc9d7b06e..3ec3e40d58 100644 --- a/centrifuge-app/src/pages/Pool/Overview/index.tsx +++ b/centrifuge-app/src/pages/Pool/Overview/index.tsx @@ -1,9 +1,8 @@ import { CurrencyBalance, Price } from '@centrifuge/centrifuge-js' -import { useWallet } from '@centrifuge/centrifuge-react' import { Box, Button, Grid, TextWithPlaceholder } from '@centrifuge/fabric' import Decimal from 'decimal.js-light' import * as React from 'react' -import { useLocation, useParams } from 'react-router' +import { useParams } from 'react-router' import { useTheme } from 'styled-components' import { InvestRedeemProps } from '../../../components/InvestRedeem/InvestRedeem' import { InvestRedeemDrawer } from '../../../components/InvestRedeem/InvestRedeemDrawer' @@ -22,15 +21,12 @@ import { Tooltips } from '../../../components/Tooltips' import { Dec } from '../../../utils/Decimal' import { formatBalance } from '../../../utils/formatting' import { getPoolValueLocked } from '../../../utils/getPoolValueLocked' -import { useTinlakePermissions } from '../../../utils/tinlake/useTinlakePermissions' import { useAverageMaturity } from '../../../utils/useAverageMaturity' import { useConnectBeforeAction } from '../../../utils/useConnectBeforeAction' import { useLoans } from '../../../utils/useLoans' import { usePool, usePoolMetadata } from '../../../utils/usePools' import { PoolDetailHeader } from '../Header' -const PoolAssetReserveChart = React.lazy(() => import('../../../components/Charts/PoolAssetReserveChart')) - export type Token = { poolId: string apy: Decimal @@ -65,11 +61,8 @@ export function PoolDetailOverview() { const theme = useTheme() const { pid: poolId } = useParams<{ pid: string }>() const isTinlakePool = poolId.startsWith('0x') - const { state } = useLocation<{ token: string }>() const pool = usePool(poolId) const { data: metadata, isLoading: metadataIsLoading } = usePoolMetadata(pool) - const { evm } = useWallet() - const { data: tinlakePermissions } = useTinlakePermissions(poolId, evm?.selectedAddress || '') const averageMaturity = useAverageMaturity(poolId) const loans = useLoans(poolId) @@ -110,29 +103,10 @@ export function PoolDetailOverview() { }) .reverse() - const hasScrolledToToken = React.useRef(false) - function handleTokenMount(node: HTMLDivElement, id: string) { - if (hasScrolledToToken.current === true || id !== state?.token) return - node.scrollIntoView({ behavior: 'smooth', block: 'center' }) - hasScrolledToToken.current = true - } - - const getTrancheAvailability = (token: string) => { - if (isTinlakePool && metadata?.pool?.newInvestmentsStatus) { - const trancheName = token.split('-')[1] === '0' ? 'junior' : 'senior' - - const isMember = tinlakePermissions?.[trancheName].inMemberlist - - return isMember || metadata.pool.newInvestmentsStatus[trancheName] !== 'closed' - } - - return true - } - return ( - + }> @@ -164,9 +138,9 @@ export function PoolDetailOverview() { - + }> - + }> @@ -187,70 +161,6 @@ export function PoolDetailOverview() { - {/* {!isTinlakePool && ( - - - }> - - - - - )} - - - {tokens?.map((token, i) => ( -
node && handleTokenMount(node, token.id)}> - - - } - value={formatPercentage(token.protection)} - /> - } - value={formatBalance(token.valueLocked, pool?.currency.symbol)} - /> - {token.seniority === 0 ? ( - } - value="Variable" - /> - ) : ( - } - value={formatPercentage(token.apy)} - /> - )} - - {formatBalanceAbbreviated(token.capacity, pool?.currency.symbol)} - - } - /> - - {token.tokenPrice && formatBalance(token.tokenPrice, pool?.currency.symbol, 4, 2)} - - } - /> - {getTrancheAvailability(token.id) && } - - -
- ))} -
-
- - - */}
) } From 6878d154fc04ea25ecba25b52797a5e903b6b581 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Fri, 16 Feb 2024 08:41:56 +0100 Subject: [PATCH 04/19] show issuer section --- .../src/components/IssuerSection.tsx | 98 ++----------------- .../src/pages/Pool/Overview/index.tsx | 4 +- 2 files changed, 11 insertions(+), 91 deletions(-) diff --git a/centrifuge-app/src/components/IssuerSection.tsx b/centrifuge-app/src/components/IssuerSection.tsx index d606cf7dd3..16a88fb1a9 100644 --- a/centrifuge-app/src/components/IssuerSection.tsx +++ b/centrifuge-app/src/components/IssuerSection.tsx @@ -2,97 +2,17 @@ import { PoolMetadata } from '@centrifuge/centrifuge-js' import { useCentrifuge } from '@centrifuge/centrifuge-react' import { Accordion, AnchorButton, Box, Card, Grid, IconExternalLink, Shelf, Stack, Text } from '@centrifuge/fabric' import * as React from 'react' -import styled from 'styled-components' import { ExecutiveSummaryDialog } from './Dialogs/ExecutiveSummaryDialog' import { LabelValueStack } from './LabelValueStack' -import { AnchorPillButton, PillButton } from './PillButton' +import { PillButton } from './PillButton' import { AnchorTextLink } from './TextLink' type IssuerSectionProps = { metadata: Partial | undefined } - export function IssuerSection({ metadata }: IssuerSectionProps) { const cent = useCentrifuge() - const [isDialogOpen, setIsDialogOpen] = React.useState(false) - - return ( - <> - - - - {metadata?.pool?.issuer.logo && ( - - )} - - {metadata?.pool?.issuer.description} - - - {metadata?.pool?.links.executiveSummary && ( - - setIsDialogOpen(true)}> - Executive summary - - setIsDialogOpen(false)} - /> - - } - /> - )} - - {(metadata?.pool?.links.website || metadata?.pool?.links.forum || metadata?.pool?.issuer.email) && ( - - {metadata?.pool?.links.website && ( - - - Website - - - )} - {metadata?.pool?.links.forum && ( - - - Forum - - - )} - {metadata?.pool?.issuer.email && ( - - - Email - - - )} - - } - /> - )} - - - {!!metadata?.pool?.details?.length && } - - ) -} - -const StyledImage = styled.img` - min-height: 104px; - min-width: 100px; - max-height: 104px; -` - -export function IssuerSectionNew({ metadata }: IssuerSectionProps) { - const cent = useCentrifuge() const report = metadata?.pool?.reports?.[0] return ( @@ -139,14 +59,14 @@ export function IssuerDetails({ metadata }: IssuerSectionProps) { return ( <> - - {metadata?.pool?.issuer.logo && ( - {metadata?.pool?.issuer.name} - )} - + {metadata?.pool?.issuer.logo && ( + + )} {metadata?.pool?.issuer.name} {metadata?.pool?.issuer.description} diff --git a/centrifuge-app/src/pages/Pool/Overview/index.tsx b/centrifuge-app/src/pages/Pool/Overview/index.tsx index 3ec3e40d58..0ece6789d6 100644 --- a/centrifuge-app/src/pages/Pool/Overview/index.tsx +++ b/centrifuge-app/src/pages/Pool/Overview/index.tsx @@ -6,12 +6,12 @@ import { useParams } from 'react-router' import { useTheme } from 'styled-components' import { InvestRedeemProps } from '../../../components/InvestRedeem/InvestRedeem' import { InvestRedeemDrawer } from '../../../components/InvestRedeem/InvestRedeemDrawer' +import { IssuerSection } from '../../../components/IssuerSection' import { LayoutBase } from '../../../components/LayoutBase' import { LoadBoundary } from '../../../components/LoadBoundary' import { AssetsByMaturity } from '../../../components/PoolOverview/AssetsByMaturity' import { Cashflows } from '../../../components/PoolOverview/Cashflows' import { KeyMetrics } from '../../../components/PoolOverview/KeyMetrics' -import { PoolAnalysis } from '../../../components/PoolOverview/PoolAnalysis' import { PoolPerformance } from '../../../components/PoolOverview/PoolPerfomance' import { PoolStructure } from '../../../components/PoolOverview/PoolStructure' import { TrancheTokenCards } from '../../../components/PoolOverview/TrancheTokenCards' @@ -134,7 +134,7 @@ export function PoolDetailOverview() { )} }> - + From 71aa0a09690bffd37cb81102b1868e6a3749310e Mon Sep 17 00:00:00 2001 From: JP Date: Wed, 21 Feb 2024 11:35:43 -0600 Subject: [PATCH 05/19] feat: pool performance chart (#1969) * feat: pool performance chart * address pr feedback * hide sections for tinlake pools * add interval logic * hide nav change * address pr feedback * revert env * remove unnecessary width and height --- .../Charts/PoolAssetReserveChart.tsx | 119 ---------- .../Charts/PoolPerformanceChart.tsx | 219 ++++++++++++++++++ centrifuge-app/src/components/Charts/utils.ts | 23 ++ .../PoolOverview/PoolPerfomance.tsx | 12 +- .../components/Portfolio/PortfolioValue.tsx | 17 +- .../src/pages/Pool/Overview/index.tsx | 56 +++-- 6 files changed, 284 insertions(+), 162 deletions(-) delete mode 100644 centrifuge-app/src/components/Charts/PoolAssetReserveChart.tsx create mode 100644 centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx create mode 100644 centrifuge-app/src/components/Charts/utils.ts diff --git a/centrifuge-app/src/components/Charts/PoolAssetReserveChart.tsx b/centrifuge-app/src/components/Charts/PoolAssetReserveChart.tsx deleted file mode 100644 index 45171f6414..0000000000 --- a/centrifuge-app/src/components/Charts/PoolAssetReserveChart.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { Grid, Shelf, Stack, Text } from '@centrifuge/fabric' -import * as React from 'react' -import { useParams } from 'react-router' -import { Area, CartesianGrid, ComposedChart, Line, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts' -import { useTheme } from 'styled-components' -import { daysBetween } from '../../utils/date' -import { formatBalance, formatBalanceAbbreviated } from '../../utils/formatting' -import { useDailyPoolStates, usePool } from '../../utils/usePools' -import { Tooltips } from '../Tooltips' -import { CustomizedTooltip } from './Tooltip' - -type ChartData = { - day: Date - poolValue: number - assetValue: number - reserve: [number, number] -} - -function PoolAssetReserveChart() { - const theme = useTheme() - const { pid: poolId } = useParams<{ pid: string }>() - const { poolStates } = useDailyPoolStates(poolId) || {} - const pool = usePool(poolId) - const poolAge = pool.createdAt ? daysBetween(pool.createdAt, new Date()) : 0 - - const data: ChartData[] = React.useMemo(() => { - return ( - poolStates?.map((day) => { - const assetValue = day.poolState.portfolioValuation.toDecimal().toNumber() - const poolValue = day.poolValue.toDecimal().toNumber() - return { day: new Date(day.timestamp), poolValue, assetValue, reserve: [assetValue, poolValue] } - }) || [] - ) - }, [poolStates]) - - if (poolStates && poolStates?.length < 1 && poolAge > 0) return No data available - - // querying chain for more accurate data, since data for today from subquery is not necessarily up to date - const todayPoolValue = pool?.value.toDecimal().toNumber() || 0 - const todayAssetValue = pool?.nav.latest.toDecimal().toNumber() || 0 - const today: ChartData = { - day: new Date(), - poolValue: todayPoolValue, - assetValue: todayAssetValue, - reserve: [todayAssetValue, todayPoolValue], - } - - const chartData = [...data.slice(0, data.length - 1), today] - - return ( - - - - {chartData?.length ? ( - - - { - if (data.length > 180) { - return new Date(tick).toLocaleString('en-US', { month: 'short' }) - } - return new Date(tick).toLocaleString('en-US', { day: 'numeric', month: 'short' }) - }} - style={{ fontSize: '10px', fill: theme.colors.textSecondary, letterSpacing: '-0.5px' }} - /> - formatBalanceAbbreviated(tick, '', 0)} - /> - - } /> - - - - - - ) : ( - No data yet - )} - - - ) -} - -function CustomLegend({ data, currency }: { currency: string; data: ChartData }) { - const theme = useTheme() - - return ( - - - - - {formatBalance(data.poolValue, currency)} - - - - {formatBalance(data.assetValue, currency)} - - - - {formatBalance(data.reserve[1] - data.reserve[0], currency)} - - - - ) -} - -export default PoolAssetReserveChart diff --git a/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx b/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx new file mode 100644 index 0000000000..69bd543d8a --- /dev/null +++ b/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx @@ -0,0 +1,219 @@ +import { Box, Grid, Shelf, Stack, Text } from '@centrifuge/fabric' +import * as React from 'react' +import { useParams } from 'react-router' +import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts' +import styled, { useTheme } from 'styled-components' +import { daysBetween, formatDate } from '../../utils/date' +import { formatBalance, formatBalanceAbbreviated } from '../../utils/formatting' +import { useDailyPoolStates, usePool } from '../../utils/usePools' +import { TooltipContainer, TooltipTitle } from './Tooltip' +import { getRangeNumber } from './utils' + +type ChartData = { + day: Date + nav: number +} + +const RangeFilterButton = styled(Stack)` + &:hover { + cursor: pointer; + } +` + +const rangeFilters = [ + { value: '30d', label: '30 days' }, + { value: '90d', label: '90 days' }, + { value: 'ytd', label: 'Year to date' }, + { value: 'all', label: 'All' }, +] as const + +const chartColor = '#A4D5D8' + +function PoolPerformanceChart() { + const theme = useTheme() + const { pid: poolId } = useParams<{ pid: string }>() + const { poolStates } = useDailyPoolStates(poolId) || {} + const pool = usePool(poolId) + const poolAge = pool.createdAt ? daysBetween(pool.createdAt, new Date()) : 0 + + const [range, setRange] = React.useState<(typeof rangeFilters)[number]>({ value: 'ytd', label: 'Year to date' }) + const rangeNumber = getRangeNumber(range.value, poolAge) + + const data: ChartData[] = React.useMemo( + () => + poolStates?.map((day) => { + const nav = + day.poolState.portfolioValuation.toDecimal().toNumber() + day.poolState.totalReserve.toDecimal().toNumber() + + return { day: new Date(day.timestamp), nav } + }) || [], + [poolStates] + ) + + if (poolStates && poolStates?.length < 1 && poolAge > 0) return No data available + + // querying chain for more accurate data, since data for today from subquery is not necessarily up to date + const todayAssetValue = pool?.nav.latest.toDecimal().toNumber() || 0 + const todayReserve = pool?.reserve.total.toDecimal().toNumber() || 0 + + const chartData = data.slice(-rangeNumber) + + const today = { + day: new Date(), + nav: todayReserve + todayAssetValue, + navChange: chartData.length > 0 ? todayReserve + todayAssetValue - chartData[0]?.nav : 0, + } + + const getXAxisInterval = () => { + if (rangeNumber <= 30) return 5 + if (rangeNumber > 30 && rangeNumber <= 90) { + return 14 + } + if (rangeNumber > 90 && rangeNumber <= 180) { + return 30 + } + return 45 + } + + return ( + + + + + {chartData.length > 0 && + rangeFilters.map((rangeFilter, index) => ( + + setRange(rangeFilter)}> + + {rangeFilter.label} + + + + {index !== rangeFilters.length - 1 && ( + + )} + + ))} + + + + + {chartData?.length ? ( + + + + + + + + + { + if (data.length > 180) { + return new Date(tick).toLocaleString('en-US', { month: 'short' }) + } + return new Date(tick).toLocaleString('en-US', { day: 'numeric', month: 'short' }) + }} + style={{ fontSize: '10px', fill: theme.colors.textSecondary, letterSpacing: '-0.5px' }} + dy={4} + interval={getXAxisInterval()} + /> + formatBalanceAbbreviated(tick, '', 0)} + /> + + { + if (payload && payload?.length > 0) { + return ( + + {formatDate(payload[0].payload.day)} + {payload.map(({ value }, index) => ( + + NAV + + {typeof value === 'number' ? formatBalance(value, 'USD' || '') : '-'} + + + ))} + + ) + } + return null + }} + /> + + + + ) : ( + No data yet + )} + + + ) +} + +function CustomLegend({ + data, +}: { + data: { + day: Date + nav: number + navChange: number + } +}) { + const theme = useTheme() + + // const navChangePercentageChange = (data.navChange / data.nav) * 100 + // const navChangePercentageChangeString = + // data.navChange === data.nav || navChangePercentageChange === 0 + // ? '' + // : ` (${navChangePercentageChange > 0 ? '+' : ''}${navChangePercentageChange.toFixed(2)}%)` + + return ( + + + + + NAV + + {formatBalance(data.nav, 'USD')} + + {/* + + NAV change + + 0 && 'statusOk'}> + {data.navChange > 0 && '+'} + {formatBalance(data.navChange, 'USD')} + {navChangePercentageChangeString} + + */} + + + ) +} + +export default PoolPerformanceChart diff --git a/centrifuge-app/src/components/Charts/utils.ts b/centrifuge-app/src/components/Charts/utils.ts new file mode 100644 index 0000000000..53ef7bfe81 --- /dev/null +++ b/centrifuge-app/src/components/Charts/utils.ts @@ -0,0 +1,23 @@ +export const getRangeNumber = (rangeValue: string, poolAge?: number) => { + if (rangeValue === '30d') { + return 30 + } + if (rangeValue === '90d') { + return 90 + } + + if (rangeValue === 'ytd') { + const today = new Date() + const januaryFirst = new Date(today.getFullYear(), 0, 1) + const timeDifference = new Date(today).getTime() - new Date(januaryFirst).getTime() + const daysSinceJanuary1 = Math.floor(timeDifference / (1000 * 60 * 60 * 24)) + + return daysSinceJanuary1 + } + + if (rangeValue === 'all' && poolAge) { + return poolAge + } + + return 30 +} diff --git a/centrifuge-app/src/components/PoolOverview/PoolPerfomance.tsx b/centrifuge-app/src/components/PoolOverview/PoolPerfomance.tsx index e3458f8d6e..16c10a7cc8 100644 --- a/centrifuge-app/src/components/PoolOverview/PoolPerfomance.tsx +++ b/centrifuge-app/src/components/PoolOverview/PoolPerfomance.tsx @@ -1,9 +1,15 @@ -import { Card } from '@centrifuge/fabric' +import { Card, Stack, Text } from '@centrifuge/fabric' +import PoolPerformanceChart from '../Charts/PoolPerformanceChart' export const PoolPerformance = () => { return ( - - Pool Performance + + + + Pool performance + + + ) } diff --git a/centrifuge-app/src/components/Portfolio/PortfolioValue.tsx b/centrifuge-app/src/components/Portfolio/PortfolioValue.tsx index ee4cbf88ed..bfe17f179d 100644 --- a/centrifuge-app/src/components/Portfolio/PortfolioValue.tsx +++ b/centrifuge-app/src/components/Portfolio/PortfolioValue.tsx @@ -2,6 +2,7 @@ import { Card, Stack, Text } from '@centrifuge/fabric' import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts' import { formatDate } from '../../utils/date' import { formatBalance } from '../../utils/formatting' +import { getRangeNumber } from '../Charts/utils' import { useDailyPortfolioValue } from './usePortfolio' const chartColor = '#006ef5' @@ -103,19 +104,3 @@ export function PortfolioValue({ rangeValue, address }: { rangeValue: string; ad ) } - -const getRangeNumber = (rangeValue: string) => { - if (rangeValue === '30d') { - return 30 - } - if (rangeValue === '90d') { - return 90 - } - - const today = new Date() - const januaryFirst = new Date(today.getFullYear(), 0, 1) - const timeDifference = new Date(today).getTime() - new Date(januaryFirst).getTime() - const daysSinceJanuary1 = Math.floor(timeDifference / (1000 * 60 * 60 * 24)) - - return daysSinceJanuary1 -} diff --git a/centrifuge-app/src/pages/Pool/Overview/index.tsx b/centrifuge-app/src/pages/Pool/Overview/index.tsx index 0ece6789d6..47dbc37a12 100644 --- a/centrifuge-app/src/pages/Pool/Overview/index.tsx +++ b/centrifuge-app/src/pages/Pool/Overview/index.tsx @@ -137,30 +137,38 @@ export function PoolDetailOverview() { - - - }> - - - }> - - - - - - }> - - - - - - - }> - - - - - + {!isTinlakePool && ( + <> + + + }> + + + }> + + + + + + }> + + + + + + + }> + + + + + + + )} ) } From f0c8ba7cc7cb5a8e82bb19cf23288c5f1c64be19 Mon Sep 17 00:00:00 2001 From: JP Date: Thu, 22 Feb 2024 11:27:27 -0600 Subject: [PATCH 06/19] feat: transaction history table (#1973) * feat: transaction history table * wire up download button * use prod env temporarily * fix amount formatting * use asset name, link asset, right align --- centrifuge-app/.env-config/.env.development | 30 +-- .../PoolOverview/TransactionHistory.tsx | 188 +++++++++++++++++- .../components/Report/AssetTransactions.tsx | 6 +- centrifuge-app/src/components/Root.tsx | 6 +- .../src/pages/Loan/PricingValues.tsx | 2 +- .../src/pages/Pool/Overview/index.tsx | 6 +- centrifuge-app/src/pages/PoolTransactions.tsx | 29 +++ centrifuge-app/src/utils/usePools.ts | 2 +- centrifuge-js/src/modules/pools.ts | 56 ++++-- centrifuge-js/src/types/subquery.ts | 9 +- 10 files changed, 282 insertions(+), 52 deletions(-) create mode 100644 centrifuge-app/src/pages/PoolTransactions.tsx diff --git a/centrifuge-app/.env-config/.env.development b/centrifuge-app/.env-config/.env.development index 57881c1a10..d96792a54c 100644 --- a/centrifuge-app/.env-config/.env.development +++ b/centrifuge-app/.env-config/.env.development @@ -1,21 +1,21 @@ -REACT_APP_COLLATOR_WSS_URL=wss://fullnode.development.cntrfg.com -REACT_APP_DEFAULT_NODE_URL=https://pod-development.k-f.dev +REACT_APP_COLLATOR_WSS_URL=wss://fullnode.parachain.centrifuge.io +REACT_APP_DEFAULT_NODE_URL='' REACT_APP_DEFAULT_UNLIST_POOLS=false -REACT_APP_FAUCET_URL=https://europe-central2-peak-vista-185616.cloudfunctions.net/faucet-api-dev +REACT_APP_FAUCET_URL= REACT_APP_IPFS_GATEWAY=https://centrifuge.mypinata.cloud/ -REACT_APP_IS_DEMO=false +REACT_APP_IS_DEMO= REACT_APP_NETWORK=centrifuge -REACT_APP_ONBOARDING_API_URL=https://europe-central2-peak-vista-185616.cloudfunctions.net/onboarding-api-dev -REACT_APP_PINNING_API_URL=https://europe-central2-peak-vista-185616.cloudfunctions.net/pinning-api-dev -REACT_APP_POOL_CREATION_TYPE=immediate -REACT_APP_RELAY_WSS_URL=wss://fullnode-relay.development.cntrfg.com -REACT_APP_SUBQUERY_URL=https://api.subquery.network/sq/centrifuge/pools-development +REACT_APP_ONBOARDING_API_URL=https://europe-central2-centrifuge-production-x.cloudfunctions.net/onboarding-api-production +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_SUBSCAN_URL=https://centrifuge.subscan.io -REACT_APP_TINLAKE_NETWORK=goerli +REACT_APP_TINLAKE_NETWORK=mainnet REACT_APP_INFURA_KEY=bf808e7d3d924fbeb74672d9341d0550 -REACT_APP_ONFINALITY_KEY=0e1c049f-d876-4e77-a45f-b5afdf5739b2 -REACT_APP_WHITELISTED_ACCOUNTS= -REACT_APP_TINLAKE_SUBGRAPH_URL=https://api.goldsky.com/api/public/project_clhi43ef5g4rw49zwftsvd2ks/subgraphs/main/prod/gn -REACT_APP_REWARDS_TREE_URL=https://storage.googleapis.com/rad-rewards-trees-kovan-staging/latest.json +REACT_APP_ONFINALITY_KEY=7e8caebc-b052-402d-87a4-e990b67ed612 +REACT_APP_WHITELISTED_ACCOUNTS='' +REACT_APP_REWARDS_TREE_URL=https://storage.googleapis.com/rad-rewards-trees-mainnet-production/latest.json +REACT_APP_MEMBERLIST_ADMIN_PURE_PROXY=kALJqPUHFzDR2VkoQYWefPQyzjGzKznNny2smXGQpSf3aMw19 REACT_APP_WALLETCONNECT_ID=c32fa79350803519804a67fcab0b742a -REACT_APP_MEMBERLIST_ADMIN_PURE_PROXY=kAJ27w29x7gHM75xajP2yXVLjVBaKmmUTxHwgRuCoAcWaoEiz +REACT_APP_TINLAKE_SUBGRAPH_URL=https://api.goldsky.com/api/public/project_clhi43ef5g4rw49zwftsvd2ks/subgraphs/main/prod/gn diff --git a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx index d5032b88f4..6db48b71dc 100644 --- a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx +++ b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx @@ -1,9 +1,187 @@ -import { Card } from '@centrifuge/fabric' +import { AssetTransaction, CurrencyBalance } from '@centrifuge/centrifuge-js' +import { AnchorButton, IconDownload, IconExternalLink, Shelf, Stack, StatusChip, Text } from '@centrifuge/fabric' +import BN from 'bn.js' +import { nftMetadataSchema } from '../../schemas' +import { formatDate } from '../../utils/date' +import { formatBalance } from '../../utils/formatting' +import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl' +import { useMetadataMulti } from '../../utils/useMetadata' +import { useAssetTransactions } from '../../utils/usePools' +import { DataTable, SortableTableHeader } from '../DataTable' +import { AnchorTextLink } from '../TextLink' + +type Row = { + type: string + transactionDate: string + assetId: string + amount: CurrencyBalance | undefined + hash: string + assetName: string +} + +const getTransactionTypeStatus = (type: string) => { + if (type === 'Principal payment' || type === 'Repaid') return 'warning' + if (type === 'Interest') return 'ok' + return 'default' +} + +export const columns = [ + { + align: 'left', + header: 'Type', + cell: ({ type }: Row) => {type}, + }, + { + align: 'left', + header: , + cell: ({ transactionDate }: Row) => ( + + {formatDate(transactionDate)} + + ), + sortKey: 'transactionDate', + }, + { + align: 'left', + header: 'Asset name', + cell: ({ assetId, assetName }: Row) => { + const [poolId, id] = assetId.split('-') + return ( + + {assetName} + + ) + }, + }, + { + align: 'right', + header: , + cell: ({ amount }: Row) => ( + + {amount ? formatBalance(amount, 'USD', 2, 2) : ''} + + ), + sortKey: 'amount', + }, + { + align: 'right', + header: 'View transaction', + cell: ({ hash }: Row) => { + return ( + + + + ) + }, + }, +] + +export const TransactionHistory = ({ poolId, preview = true }: { poolId: string; preview?: boolean }) => { + const transactions = useAssetTransactions(poolId, new Date(0)) + + const assetMetadata = useMetadataMulti( + [...new Set(transactions?.map((transaction) => transaction.asset.metadata))] || [], + nftMetadataSchema + ) + + const getLabelAndAmount = (transaction: AssetTransaction) => { + if (transaction.type === 'BORROWED') { + return { + label: 'Purchase', + amount: transaction.amount, + } + } + if (transaction.type === 'REPAID' && !new BN(transaction.interestAmount || 0).isZero()) { + return { + label: 'Interest', + amount: transaction.interestAmount, + } + } + + return { + label: 'Principal payment', + amount: transaction.principalAmount, + } + } + + const csvData = transactions + ?.filter( + (transaction) => transaction.type !== 'CREATED' && transaction.type !== 'CLOSED' && transaction.type !== 'PRICED' + ) + .map((transaction) => { + const { label, amount } = getLabelAndAmount(transaction) + const [, id] = transaction.asset.id.split('-') + return { + Type: label, + 'Transaction Date': `"${formatDate(transaction.timestamp, { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + })}"`, + 'Asset Name': assetMetadata[Number(id) - 1]?.data?.name || '-', + Amount: amount ? `"${formatBalance(amount, 'USD', 2, 2)}"` : '-', + Transaction: `${import.meta.env.REACT_APP_SUBSCAN_URL}/extrinsic/${transaction.hash}`, + } + }) + + const csvUrl = csvData?.length ? getCSVDownloadUrl(csvData) : '' + + const tableData = + transactions + ?.filter( + (transaction) => + transaction.type !== 'CREATED' && transaction.type !== 'CLOSED' && transaction.type !== 'PRICED' + ) + .sort((a, b) => (a.timestamp > b.timestamp ? -1 : 1)) + .slice(0, preview ? 8 : Infinity) + .map((transaction) => { + const [, id] = transaction.asset.id.split('-') + const { label, amount } = getLabelAndAmount(transaction) + return { + type: label, + transactionDate: transaction.timestamp, + assetId: transaction.asset.id, + assetName: assetMetadata[Number(id) - 1]?.data?.name, + amount, + hash: transaction.hash, + } + }) || [] -export const TransactionHistory = () => { return ( - - Transaction History - + + + + Transaction history + + {transactions?.length && ( + + Download + + )} + + + {transactions?.length! > 8 && preview && ( + + View all + + )} + ) } diff --git a/centrifuge-app/src/components/Report/AssetTransactions.tsx b/centrifuge-app/src/components/Report/AssetTransactions.tsx index 83cc9a11e9..155133082b 100644 --- a/centrifuge-app/src/components/Report/AssetTransactions.tsx +++ b/centrifuge-app/src/components/Report/AssetTransactions.tsx @@ -7,9 +7,9 @@ import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl' import { useAssetTransactions } from '../../utils/usePools' import { DataTable } from '../DataTable' import { Spinner } from '../Spinner' -import type { TableDataRow } from './index' import { ReportContext } from './ReportContext' import { UserFeedback } from './UserFeedback' +import type { TableDataRow } from './index' import { formatAssetTransactionType } from './utils' export function AssetTransactions({ pool }: { pool: Pool }) { @@ -26,7 +26,7 @@ export function AssetTransactions({ pool }: { pool: Pool }) { return transactions?.map((tx) => ({ name: '', value: [ - tx.assetId.split('-').at(-1)!, + tx.asset.id.split('-').at(-1)!, tx.epochId.split('-').at(-1)!, formatDate(tx.timestamp.toString()), formatAssetTransactionType(tx.type), @@ -77,4 +77,4 @@ export function AssetTransactions({ pool }: { pool: Pool }) { ) : ( ) -} \ No newline at end of file +} diff --git a/centrifuge-app/src/components/Root.tsx b/centrifuge-app/src/components/Root.tsx index 30ad40e09e..84f5fc6528 100644 --- a/centrifuge-app/src/components/Root.tsx +++ b/centrifuge-app/src/components/Root.tsx @@ -5,11 +5,11 @@ import { TransactionToasts, WalletProvider, } from '@centrifuge/centrifuge-react' -import { FabricProvider, GlobalStyle as FabricGlobalStyle } from '@centrifuge/fabric' +import { GlobalStyle as FabricGlobalStyle, FabricProvider } from '@centrifuge/fabric' import * as React from 'react' import { HelmetProvider } from 'react-helmet-async' import { QueryClient, QueryClientProvider } from 'react-query' -import { BrowserRouter as Router, LinkProps, matchPath, Redirect, Route, RouteProps, Switch } from 'react-router-dom' +import { LinkProps, Redirect, Route, RouteProps, BrowserRouter as Router, Switch, matchPath } from 'react-router-dom' import { config, evmChains } from '../config' import PoolsPage from '../pages/Pools' import { pinToApi } from '../utils/pinToApi' @@ -131,6 +131,7 @@ const TransactionHistoryPage = React.lazy(() => import('../pages/Portfolio/Trans const TokenOverviewPage = React.lazy(() => import('../pages/Tokens')) const PrimePage = React.lazy(() => import('../pages/Prime')) const PrimeDetailPage = React.lazy(() => import('../pages/Prime/Detail')) +const PoolTransactionsPage = React.lazy(() => import('../pages/PoolTransactions')) const routes: RouteProps[] = [ { path: '/nfts/collection/:cid/object/mint', component: MintNFTPage }, @@ -144,6 +145,7 @@ const routes: RouteProps[] = [ { path: '/issuer/:pid', component: IssuerPoolPage }, { path: '/pools/:pid/assets/:aid', component: LoanPage }, { path: '/pools/tokens', component: TokenOverviewPage }, + { path: '/pools/:pid/transactions', component: PoolTransactionsPage }, { path: '/pools/:pid', component: PoolDetailPage }, { path: '/pools', component: PoolsPage }, { path: '/history/:address', component: TransactionHistoryPage }, diff --git a/centrifuge-app/src/pages/Loan/PricingValues.tsx b/centrifuge-app/src/pages/Loan/PricingValues.tsx index 345156f792..1937941b41 100644 --- a/centrifuge-app/src/pages/Loan/PricingValues.tsx +++ b/centrifuge-app/src/pages/Loan/PricingValues.tsx @@ -27,7 +27,7 @@ export function PricingValues({ loan, pool }: Props) { const days = getAge(new Date(pricing.oracle.timestamp).toISOString()) const borrowerAssetTransactions = assetTransactions?.filter( - (assetTransaction) => assetTransaction.loanId === `${loan.poolId}-${loan.id}` + (assetTransaction) => assetTransaction.asset.id === `${loan.poolId}-${loan.id}` ) const latestPrice = getLatestPrice(pricing.oracle.value, borrowerAssetTransactions, pool.currency.decimals) diff --git a/centrifuge-app/src/pages/Pool/Overview/index.tsx b/centrifuge-app/src/pages/Pool/Overview/index.tsx index 47dbc37a12..f69e21ed91 100644 --- a/centrifuge-app/src/pages/Pool/Overview/index.tsx +++ b/centrifuge-app/src/pages/Pool/Overview/index.tsx @@ -1,5 +1,5 @@ import { CurrencyBalance, Price } from '@centrifuge/centrifuge-js' -import { Box, Button, Grid, TextWithPlaceholder } from '@centrifuge/fabric' +import { Box, Button, Card, Grid, TextWithPlaceholder } from '@centrifuge/fabric' import Decimal from 'decimal.js-light' import * as React from 'react' import { useParams } from 'react-router' @@ -163,7 +163,9 @@ export function PoolDetailOverview() { }> - + + + diff --git a/centrifuge-app/src/pages/PoolTransactions.tsx b/centrifuge-app/src/pages/PoolTransactions.tsx new file mode 100644 index 0000000000..bb860855ef --- /dev/null +++ b/centrifuge-app/src/pages/PoolTransactions.tsx @@ -0,0 +1,29 @@ +import { Box, Stack, Text } from '@centrifuge/fabric' +import { useParams } from 'react-router' +import { LayoutBase } from '../components/LayoutBase' +import { LayoutSection } from '../components/LayoutBase/LayoutSection' +import { TransactionHistory } from '../components/PoolOverview/TransactionHistory' +import { usePool, usePoolMetadata } from '../utils/usePools' + +const PoolTransactions = () => { + const { pid: poolId } = useParams<{ pid: string }>() + const pool = usePool(poolId) + const { data: metadata } = usePoolMetadata(pool) + + return ( + + + + + {metadata?.pool?.name} + + + + + + + + ) +} + +export default PoolTransactions diff --git a/centrifuge-app/src/utils/usePools.ts b/centrifuge-app/src/utils/usePools.ts index f54997748b..7a98a0ba7c 100644 --- a/centrifuge-app/src/utils/usePools.ts +++ b/centrifuge-app/src/utils/usePools.ts @@ -117,7 +117,7 @@ export function useBorrowerAssetTransactions(poolId: string, assetId: string, fr return assetTransactions.pipe( map((transactions: AssetTransaction[]) => - transactions.filter((transaction) => transaction.assetId.split('-')[1] === assetId) + transactions.filter((transaction) => transaction.asset.id.split('-')[1] === assetId) ) ) }, diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index e9bf8c6fe9..8383c4ccb7 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -10,22 +10,22 @@ import { SolverResult, calculateOptimalSolution } from '..' import { Centrifuge } from '../Centrifuge' import { Account, TransactionOptions } from '../types' import { - AssetTransactionType, - InvestorTransactionType, - SubqueryAssetTransaction, - SubqueryCurrencyBalances, - SubqueryInvestorTransaction, - SubqueryPoolSnapshot, - SubqueryTrancheBalances, - SubqueryTrancheSnapshot, + AssetTransactionType, + InvestorTransactionType, + SubqueryAssetTransaction, + SubqueryCurrencyBalances, + SubqueryInvestorTransaction, + SubqueryPoolSnapshot, + SubqueryTrancheBalances, + SubqueryTrancheSnapshot, } from '../types/subquery' import { - addressToHex, - computeTrancheId, - getDateMonthsFromNow, - getDateYearsFromNow, - getRandomUint, - isSameAddress, + addressToHex, + computeTrancheId, + getDateMonthsFromNow, + getDateYearsFromNow, + getRandomUint, + isSameAddress, } from '../utils' import { CurrencyBalance, Perquintill, Price, Rate, TokenBalance } from '../utils/BN' import { Dec } from '../utils/Decimal' @@ -731,11 +731,17 @@ export type AssetTransaction = { poolId: string accountId: string epochId: string - loanId: string type: AssetTransactionType amount: CurrencyBalance | undefined settlementPrice: string | null quantity: string | null + principalAmount: CurrencyBalance | undefined + interestAmount: CurrencyBalance | undefined + hash: string + asset: { + id: string + metadata: string + } } type Holder = { @@ -2521,13 +2527,19 @@ export function getPoolsModule(inst: Centrifuge) { timestamp: { greaterThan: $from, lessThan: $to }, }) { nodes { - assetId + principalAmount + interestAmount epochId type timestamp amount settlementPrice quantity + hash + asset { + id + metadata + } } } } @@ -2546,6 +2558,8 @@ export function getPoolsModule(inst: Centrifuge) { return data!.assetTransactions.nodes.map((tx) => ({ ...tx, amount: tx.amount ? new CurrencyBalance(tx.amount, currency.decimals) : undefined, + 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[] }) @@ -2933,11 +2947,11 @@ export function getPoolsModule(inst: Centrifuge) { } > = {} // oracles.forEach(() => { - // const { timestamp, value } = oracle[1].toPrimitive() as any - // oraclePrices[(oracle[0].toHuman() as any)[0].Isin] = { - // timestamp, - // value: new CurrencyBalance(value, currency.decimals), - // } + // const { timestamp, value } = oracle[1].toPrimitive() as any + // oraclePrices[(oracle[0].toHuman() as any)[0].Isin] = { + // timestamp, + // value: new CurrencyBalance(value, currency.decimals), + // } // }) const activeLoansPortfolio: Record< diff --git a/centrifuge-js/src/types/subquery.ts b/centrifuge-js/src/types/subquery.ts index 4cc7707737..75be6a2871 100644 --- a/centrifuge-js/src/types/subquery.ts +++ b/centrifuge-js/src/types/subquery.ts @@ -83,11 +83,16 @@ export type SubqueryAssetTransaction = { poolId: string accountId: string epochId: string - assetId: string type: AssetTransactionType - amount?: number | null + amount: CurrencyBalance | undefined + principalAmount: CurrencyBalance | undefined + interestAmount: CurrencyBalance | undefined settlementPrice: string | null quantity: string | null + asset: { + id: string + metadata: string + } } export type SubqueryTrancheBalances = { From d947ee79f242b428b63964082153ee99dd25b830 Mon Sep 17 00:00:00 2001 From: JP Angelle Date: Thu, 22 Feb 2024 11:29:26 -0600 Subject: [PATCH 07/19] revert env --- centrifuge-app/.env-config/.env.development | 30 ++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/centrifuge-app/.env-config/.env.development b/centrifuge-app/.env-config/.env.development index d96792a54c..57881c1a10 100644 --- a/centrifuge-app/.env-config/.env.development +++ b/centrifuge-app/.env-config/.env.development @@ -1,21 +1,21 @@ -REACT_APP_COLLATOR_WSS_URL=wss://fullnode.parachain.centrifuge.io -REACT_APP_DEFAULT_NODE_URL='' +REACT_APP_COLLATOR_WSS_URL=wss://fullnode.development.cntrfg.com +REACT_APP_DEFAULT_NODE_URL=https://pod-development.k-f.dev REACT_APP_DEFAULT_UNLIST_POOLS=false -REACT_APP_FAUCET_URL= +REACT_APP_FAUCET_URL=https://europe-central2-peak-vista-185616.cloudfunctions.net/faucet-api-dev REACT_APP_IPFS_GATEWAY=https://centrifuge.mypinata.cloud/ -REACT_APP_IS_DEMO= +REACT_APP_IS_DEMO=false REACT_APP_NETWORK=centrifuge -REACT_APP_ONBOARDING_API_URL=https://europe-central2-centrifuge-production-x.cloudfunctions.net/onboarding-api-production -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_ONBOARDING_API_URL=https://europe-central2-peak-vista-185616.cloudfunctions.net/onboarding-api-dev +REACT_APP_PINNING_API_URL=https://europe-central2-peak-vista-185616.cloudfunctions.net/pinning-api-dev +REACT_APP_POOL_CREATION_TYPE=immediate +REACT_APP_RELAY_WSS_URL=wss://fullnode-relay.development.cntrfg.com +REACT_APP_SUBQUERY_URL=https://api.subquery.network/sq/centrifuge/pools-development REACT_APP_SUBSCAN_URL=https://centrifuge.subscan.io -REACT_APP_TINLAKE_NETWORK=mainnet +REACT_APP_TINLAKE_NETWORK=goerli REACT_APP_INFURA_KEY=bf808e7d3d924fbeb74672d9341d0550 -REACT_APP_ONFINALITY_KEY=7e8caebc-b052-402d-87a4-e990b67ed612 -REACT_APP_WHITELISTED_ACCOUNTS='' -REACT_APP_REWARDS_TREE_URL=https://storage.googleapis.com/rad-rewards-trees-mainnet-production/latest.json -REACT_APP_MEMBERLIST_ADMIN_PURE_PROXY=kALJqPUHFzDR2VkoQYWefPQyzjGzKznNny2smXGQpSf3aMw19 -REACT_APP_WALLETCONNECT_ID=c32fa79350803519804a67fcab0b742a +REACT_APP_ONFINALITY_KEY=0e1c049f-d876-4e77-a45f-b5afdf5739b2 +REACT_APP_WHITELISTED_ACCOUNTS= REACT_APP_TINLAKE_SUBGRAPH_URL=https://api.goldsky.com/api/public/project_clhi43ef5g4rw49zwftsvd2ks/subgraphs/main/prod/gn +REACT_APP_REWARDS_TREE_URL=https://storage.googleapis.com/rad-rewards-trees-kovan-staging/latest.json +REACT_APP_WALLETCONNECT_ID=c32fa79350803519804a67fcab0b742a +REACT_APP_MEMBERLIST_ADMIN_PURE_PROXY=kAJ27w29x7gHM75xajP2yXVLjVBaKmmUTxHwgRuCoAcWaoEiz From b006c980b0fef65ab22a4d2bc58a86b28babb6a0 Mon Sep 17 00:00:00 2001 From: JP Date: Mon, 26 Feb 2024 11:03:19 -0600 Subject: [PATCH 08/19] feat: cashflow chart (#1978) * feat: cashflow chart * use prod env temporarily * truncate pool states * revert env --- .../src/components/Charts/CashflowsChart.tsx | 217 ++++++++++++++++++ .../Charts/PoolPerformanceChart.tsx | 2 - .../src/components/PoolOverview/Cashflows.tsx | 89 ++++++- .../src/pages/Pool/Overview/index.tsx | 2 +- centrifuge-js/src/modules/pools.ts | 4 +- centrifuge-js/src/types/subquery.ts | 3 +- 6 files changed, 309 insertions(+), 8 deletions(-) create mode 100644 centrifuge-app/src/components/Charts/CashflowsChart.tsx diff --git a/centrifuge-app/src/components/Charts/CashflowsChart.tsx b/centrifuge-app/src/components/Charts/CashflowsChart.tsx new file mode 100644 index 0000000000..6af6183751 --- /dev/null +++ b/centrifuge-app/src/components/Charts/CashflowsChart.tsx @@ -0,0 +1,217 @@ +import { CurrencyBalance, DailyPoolState, Pool } from '@centrifuge/centrifuge-js' +import { Box, Grid, Shelf, Stack, Text } from '@centrifuge/fabric' +import capitalize from 'lodash/capitalize' +import startCase from 'lodash/startCase' +import * as React from 'react' +import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts' +import styled, { useTheme } from 'styled-components' +import { daysBetween, formatDate } from '../../utils/date' +import { formatBalance, formatBalanceAbbreviated } from '../../utils/formatting' +import { TinlakePool } from '../../utils/tinlake/useTinlakePools' +import { TooltipContainer, TooltipTitle } from '../Charts/Tooltip' +import { getRangeNumber } from './utils' + +type Props = { + poolStates?: DailyPoolState[] + pool: Pool | TinlakePool +} + +const RangeFilterButton = styled(Stack)` + &:hover { + cursor: pointer; + } +` + +const rangeFilters = [ + { value: '30d', label: '30 days' }, + { value: '90d', label: '90 days' }, + { value: 'ytd', label: 'Year to date' }, + { value: 'all', label: 'All' }, +] as const + +export const CashflowsChart = ({ poolStates, pool }: Props) => { + const theme = useTheme() + const [range, setRange] = React.useState<(typeof rangeFilters)[number]>({ value: 'ytd', label: 'Year to date' }) + + const poolAge = pool.createdAt ? daysBetween(pool.createdAt, new Date()) : 0 + const rangeNumber = getRangeNumber(range.value, poolAge) + + 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 } + }) || [], + [poolStates, pool.currency.decimals] + ) + + const chartData = data.slice(-rangeNumber) + + const today = { + 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), + } + + const getXAxisInterval = () => { + if (rangeNumber <= 30) return 5 + if (rangeNumber > 30 && rangeNumber <= 90) { + return 14 + } + if (rangeNumber > 90 && rangeNumber <= 180) { + return 30 + } + return 45 + } + + return ( + + + + + {chartData.length > 0 && + rangeFilters.map((rangeFilter, index) => ( + + setRange(rangeFilter)}> + + {rangeFilter.label} + + + + {index !== rangeFilters.length - 1 && ( + + )} + + ))} + + + + + + + { + if (data.length > 180) { + return new Date(tick).toLocaleString('en-US', { month: 'short' }) + } + return new Date(tick).toLocaleString('en-US', { day: 'numeric', month: 'short' }) + }} + interval={getXAxisInterval()} + /> + formatBalanceAbbreviated(tick, '', 0)} + /> + { + if (payload) { + return ( + + {formatDate(label)} + {payload.map(({ color, name, value }, index) => { + return ( + + + + + + {typeof name === 'string' ? capitalize(startCase(name)) : '-'} + + + + {typeof value === 'number' ? formatBalance(value, 'USD', 2, 2) : '-'} + + + + ) + })} + + ) + } + return null + }} + /> + + + + {/* */} + + + + + ) +} + +function CustomLegend({ + data, +}: { + data: { + totalPurchases: number + principalRepayments: number + interest: number + } +}) { + const theme = useTheme() + + return ( + + + + + Total purchases + + {formatBalance(data.totalPurchases, 'USD', 2)} + + + + Principal repayments + + {formatBalance(data.principalRepayments, 'USD', 2)} + + + + Interest + + {formatBalance(data.interest, 'USD', 2)} + + {/* + + Fees + + {formatBalance(0, 'USD', 2)} + */} + + + ) +} diff --git a/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx b/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx index 69bd543d8a..a3a31c413a 100644 --- a/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx +++ b/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx @@ -59,7 +59,6 @@ function PoolPerformanceChart() { const chartData = data.slice(-rangeNumber) const today = { - day: new Date(), nav: todayReserve + todayAssetValue, navChange: chartData.length > 0 ? todayReserve + todayAssetValue - chartData[0]?.nav : 0, } @@ -167,7 +166,6 @@ function CustomLegend({ data, }: { data: { - day: Date nav: number navChange: number } diff --git a/centrifuge-app/src/components/PoolOverview/Cashflows.tsx b/centrifuge-app/src/components/PoolOverview/Cashflows.tsx index f663d00835..9b83d0bc1e 100644 --- a/centrifuge-app/src/components/PoolOverview/Cashflows.tsx +++ b/centrifuge-app/src/components/PoolOverview/Cashflows.tsx @@ -1,9 +1,92 @@ -import { Card } from '@centrifuge/fabric' +import { CurrencyBalance, Loan } from '@centrifuge/centrifuge-js' +import { AnchorButton, Card, IconDownload, Shelf, Stack, Text } from '@centrifuge/fabric' +import { useParams } from 'react-router' +import { formatDate } from '../../utils/date' +import { formatBalance } from '../../utils/formatting' +import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl' +import { useLoans } from '../../utils/useLoans' +import { useDailyPoolStates, usePool } from '../../utils/usePools' +import { CashflowsChart } from '../Charts/CashflowsChart' export const Cashflows = () => { + const { pid: poolId } = useParams<{ pid: string }>() + const { poolStates } = useDailyPoolStates(poolId) || {} + const pool = usePool(poolId) + const loans = useLoans(poolId) as Loan[] | undefined | null + + const firstOriginationDate = loans?.reduce((acc, cur) => { + if ('originationDate' in cur) { + if (!acc) return cur.originationDate + return acc < cur.originationDate ? acc : cur.originationDate + } + return acc + }, '') + + const truncatedPoolStates = poolStates?.filter((poolState) => { + if (firstOriginationDate) { + return new Date(poolState.timestamp) >= new Date(firstOriginationDate) + } + return true + }) + + const csvData = truncatedPoolStates?.map((poolState) => { + return { + Date: `"${formatDate(poolState.timestamp, { + year: 'numeric', + month: 'long', + day: 'numeric', + })}"`, + Purchases: poolState.sumBorrowedAmountByPeriod + ? `"${formatBalance( + new CurrencyBalance(poolState.sumBorrowedAmountByPeriod, pool.currency.decimals).toDecimal().toNumber(), + 'USD', + 2, + 2 + )}"` + : '-', + 'Principal repayments': poolState.sumRepaidAmountByPeriod + ? `"${formatBalance( + new CurrencyBalance(poolState.sumRepaidAmountByPeriod, pool.currency.decimals).toDecimal().toNumber(), + 'USD', + 2, + 2 + )}"` + : '-', + Interest: poolState.sumInterestRepaidAmountByPeriod + ? `"${formatBalance( + new CurrencyBalance(poolState.sumInterestRepaidAmountByPeriod, pool.currency.decimals) + .toDecimal() + .toNumber(), + 'USD', + 2, + 2 + )}"` + : '-', + } + }) + + const csvUrl = csvData?.length ? getCSVDownloadUrl(csvData) : '' + return ( - - Cashflows + + + + + Cashflows + + + Download + + + + ) } diff --git a/centrifuge-app/src/pages/Pool/Overview/index.tsx b/centrifuge-app/src/pages/Pool/Overview/index.tsx index f69e21ed91..fd5a7e34d5 100644 --- a/centrifuge-app/src/pages/Pool/Overview/index.tsx +++ b/centrifuge-app/src/pages/Pool/Overview/index.tsx @@ -156,7 +156,7 @@ export function PoolDetailOverview() { }> - + diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index 8383c4ccb7..1d58b182a0 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -559,7 +559,8 @@ export type DailyPoolState = { timestamp: string tranches: { [trancheId: string]: DailyTrancheState } - sumBorrowedAmountByPeriod?: number | null + sumBorrowedAmountByPeriod?: string | null + sumInterestRepaidAmountByPeriod?: string | null sumRepaidAmountByPeriod?: number | null sumInvestedAmountByPeriod?: number | null sumRedeemedAmountByPeriod?: number | null @@ -2076,6 +2077,7 @@ export function getPoolsModule(inst: Centrifuge) { sumRepaidAmountByPeriod sumInvestedAmountByPeriod sumRedeemedAmountByPeriod + sumInterestRepaidAmountByPeriod } pageInfo { hasNextPage diff --git a/centrifuge-js/src/types/subquery.ts b/centrifuge-js/src/types/subquery.ts index 75be6a2871..12e8563371 100644 --- a/centrifuge-js/src/types/subquery.ts +++ b/centrifuge-js/src/types/subquery.ts @@ -14,7 +14,8 @@ export type SubqueryPoolSnapshot = { totalInvested?: number | null totalRedeemed?: number | null sumBorrowedAmount?: number | null - sumBorrowedAmountByPeriod?: number | null + sumBorrowedAmountByPeriod?: string | null + sumInterestRepaidAmountByPeriod?: string | null sumRepaidAmountByPeriod?: number | null sumInvestedAmountByPeriod?: number | null sumRedeemedAmountByPeriod?: number | null From 9a00d12ae6b5aa252995bf6bec7d847aac733143 Mon Sep 17 00:00:00 2001 From: JP Angelle Date: Mon, 26 Feb 2024 11:24:19 -0600 Subject: [PATCH 09/19] fix transaction history sort --- .../src/components/PoolOverview/TransactionHistory.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx index 6db48b71dc..b71d93d718 100644 --- a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx +++ b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx @@ -152,7 +152,7 @@ export const TransactionHistory = ({ poolId, preview = true }: { poolId: string; transactionDate: transaction.timestamp, assetId: transaction.asset.id, assetName: assetMetadata[Number(id) - 1]?.data?.name, - amount, + amount: amount || 0, hash: transaction.hash, } }) || [] From f9e7f613ca6be547ef0ab40833631afea21ab81d Mon Sep 17 00:00:00 2001 From: JP Angelle Date: Mon, 26 Feb 2024 14:40:49 -0600 Subject: [PATCH 10/19] hide metrics, tooltip for pool type --- .../components/PoolOverview/KeyMetrics.tsx | 28 +++++++++++-------- .../components/PoolOverview/PoolStructure.tsx | 11 +++++++- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx b/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx index b9a2a6f15f..00d329e6d1 100644 --- a/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx +++ b/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx @@ -34,10 +34,6 @@ export const KeyMetrics = ({ assetType, averageMaturity, loans, poolStatus }: Pr startCase(assetType?.subClass) ).replace(/^Us /, 'US ')}`, }, - { - metric: 'Pool type', - value: capitalize(poolStatus), - }, { metric: 'Average asset maturity', value: averageMaturity, @@ -50,14 +46,22 @@ export const KeyMetrics = ({ assetType, averageMaturity, loans, poolStatus }: Pr metric: 'Ongoing assets', value: ongoingAssetCount, }, - { - metric: 'Written off assets', - value: writtenOffAssetCount, - }, - { - metric: 'Overdue assets', - value: overdueAssetCount, - }, + ...(writtenOffAssetCount + ? [ + { + metric: 'Written off assets', + value: writtenOffAssetCount, + }, + ] + : []), + ...(overdueAssetCount + ? [ + { + metric: 'Overdue assets', + value: overdueAssetCount, + }, + ] + : []), ] return ( diff --git a/centrifuge-app/src/components/PoolOverview/PoolStructure.tsx b/centrifuge-app/src/components/PoolOverview/PoolStructure.tsx index 311a40f933..6eabc1f26c 100644 --- a/centrifuge-app/src/components/PoolOverview/PoolStructure.tsx +++ b/centrifuge-app/src/components/PoolOverview/PoolStructure.tsx @@ -44,7 +44,7 @@ export const PoolStructure = ({ numOfTranches, poolId, poolStatus }: Props) => { value: firstInvestment ? formatDate(firstInvestment) : '-', }, { - metric: 'Available network', + metric: 'Available networks', value: `Centrifuge${deployedLpChains.length ? `, ${deployedLpChains.join(', ')}` : ''}`, }, // { @@ -78,6 +78,15 @@ export const PoolStructure = ({ numOfTranches, poolId, poolStatus }: Props) => { ) + + if (metric === 'Pool type' && value === 'Open') + return ( + + + Open + + + ) return ( {value} From 2d2f7c3e4275680bf6aa970dddf1120d6b24f313 Mon Sep 17 00:00:00 2001 From: JP Angelle Date: Mon, 26 Feb 2024 16:46:34 -0600 Subject: [PATCH 11/19] use prod env temporarily --- centrifuge-app/.env-config/.env.development | 30 ++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/centrifuge-app/.env-config/.env.development b/centrifuge-app/.env-config/.env.development index 57881c1a10..d96792a54c 100644 --- a/centrifuge-app/.env-config/.env.development +++ b/centrifuge-app/.env-config/.env.development @@ -1,21 +1,21 @@ -REACT_APP_COLLATOR_WSS_URL=wss://fullnode.development.cntrfg.com -REACT_APP_DEFAULT_NODE_URL=https://pod-development.k-f.dev +REACT_APP_COLLATOR_WSS_URL=wss://fullnode.parachain.centrifuge.io +REACT_APP_DEFAULT_NODE_URL='' REACT_APP_DEFAULT_UNLIST_POOLS=false -REACT_APP_FAUCET_URL=https://europe-central2-peak-vista-185616.cloudfunctions.net/faucet-api-dev +REACT_APP_FAUCET_URL= REACT_APP_IPFS_GATEWAY=https://centrifuge.mypinata.cloud/ -REACT_APP_IS_DEMO=false +REACT_APP_IS_DEMO= REACT_APP_NETWORK=centrifuge -REACT_APP_ONBOARDING_API_URL=https://europe-central2-peak-vista-185616.cloudfunctions.net/onboarding-api-dev -REACT_APP_PINNING_API_URL=https://europe-central2-peak-vista-185616.cloudfunctions.net/pinning-api-dev -REACT_APP_POOL_CREATION_TYPE=immediate -REACT_APP_RELAY_WSS_URL=wss://fullnode-relay.development.cntrfg.com -REACT_APP_SUBQUERY_URL=https://api.subquery.network/sq/centrifuge/pools-development +REACT_APP_ONBOARDING_API_URL=https://europe-central2-centrifuge-production-x.cloudfunctions.net/onboarding-api-production +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_SUBSCAN_URL=https://centrifuge.subscan.io -REACT_APP_TINLAKE_NETWORK=goerli +REACT_APP_TINLAKE_NETWORK=mainnet REACT_APP_INFURA_KEY=bf808e7d3d924fbeb74672d9341d0550 -REACT_APP_ONFINALITY_KEY=0e1c049f-d876-4e77-a45f-b5afdf5739b2 -REACT_APP_WHITELISTED_ACCOUNTS= -REACT_APP_TINLAKE_SUBGRAPH_URL=https://api.goldsky.com/api/public/project_clhi43ef5g4rw49zwftsvd2ks/subgraphs/main/prod/gn -REACT_APP_REWARDS_TREE_URL=https://storage.googleapis.com/rad-rewards-trees-kovan-staging/latest.json +REACT_APP_ONFINALITY_KEY=7e8caebc-b052-402d-87a4-e990b67ed612 +REACT_APP_WHITELISTED_ACCOUNTS='' +REACT_APP_REWARDS_TREE_URL=https://storage.googleapis.com/rad-rewards-trees-mainnet-production/latest.json +REACT_APP_MEMBERLIST_ADMIN_PURE_PROXY=kALJqPUHFzDR2VkoQYWefPQyzjGzKznNny2smXGQpSf3aMw19 REACT_APP_WALLETCONNECT_ID=c32fa79350803519804a67fcab0b742a -REACT_APP_MEMBERLIST_ADMIN_PURE_PROXY=kAJ27w29x7gHM75xajP2yXVLjVBaKmmUTxHwgRuCoAcWaoEiz +REACT_APP_TINLAKE_SUBGRAPH_URL=https://api.goldsky.com/api/public/project_clhi43ef5g4rw49zwftsvd2ks/subgraphs/main/prod/gn From 164fcb2c0d34ce3556f12574dc36dab39f1d827c Mon Sep 17 00:00:00 2001 From: JP Angelle Date: Mon, 26 Feb 2024 17:01:24 -0600 Subject: [PATCH 12/19] hide assets by maturity card --- centrifuge-app/src/pages/Pool/Overview/index.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/centrifuge-app/src/pages/Pool/Overview/index.tsx b/centrifuge-app/src/pages/Pool/Overview/index.tsx index fd5a7e34d5..adfb2faeee 100644 --- a/centrifuge-app/src/pages/Pool/Overview/index.tsx +++ b/centrifuge-app/src/pages/Pool/Overview/index.tsx @@ -9,7 +9,6 @@ import { InvestRedeemDrawer } from '../../../components/InvestRedeem/InvestRedee import { IssuerSection } from '../../../components/IssuerSection' import { LayoutBase } from '../../../components/LayoutBase' import { LoadBoundary } from '../../../components/LoadBoundary' -import { AssetsByMaturity } from '../../../components/PoolOverview/AssetsByMaturity' import { Cashflows } from '../../../components/PoolOverview/Cashflows' import { KeyMetrics } from '../../../components/PoolOverview/KeyMetrics' import { PoolPerformance } from '../../../components/PoolOverview/PoolPerfomance' @@ -148,15 +147,15 @@ export function PoolDetailOverview() { poolStatus={metadata?.pool?.status} /> - }> + {/* }> - + */} }> - + From 6ca1026832189c038d93394cc2a7f364eeaf5fe8 Mon Sep 17 00:00:00 2001 From: JP Angelle Date: Mon, 26 Feb 2024 17:04:23 -0600 Subject: [PATCH 13/19] include jan 1 --- centrifuge-app/src/components/Charts/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/centrifuge-app/src/components/Charts/utils.ts b/centrifuge-app/src/components/Charts/utils.ts index 53ef7bfe81..b279bcc8c3 100644 --- a/centrifuge-app/src/components/Charts/utils.ts +++ b/centrifuge-app/src/components/Charts/utils.ts @@ -12,7 +12,7 @@ export const getRangeNumber = (rangeValue: string, poolAge?: number) => { const timeDifference = new Date(today).getTime() - new Date(januaryFirst).getTime() const daysSinceJanuary1 = Math.floor(timeDifference / (1000 * 60 * 60 * 24)) - return daysSinceJanuary1 + return daysSinceJanuary1 + 1 } if (rangeValue === 'all' && poolAge) { From 29b4dc46ba757a1eaa00b5f18f720912dc3686f4 Mon Sep 17 00:00:00 2001 From: JP Angelle Date: Mon, 26 Feb 2024 23:20:54 -0600 Subject: [PATCH 14/19] delete pool analysis --- .../src/components/PoolOverview/PoolAnalysis.tsx | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 centrifuge-app/src/components/PoolOverview/PoolAnalysis.tsx diff --git a/centrifuge-app/src/components/PoolOverview/PoolAnalysis.tsx b/centrifuge-app/src/components/PoolOverview/PoolAnalysis.tsx deleted file mode 100644 index 13d3767132..0000000000 --- a/centrifuge-app/src/components/PoolOverview/PoolAnalysis.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Card } from '@centrifuge/fabric' - -export const PoolAnalysis = () => { - return ( - - Pool Analysis - - ) -} From 4e24391a95b16988ce57ed97ac6b1c0855440b13 Mon Sep 17 00:00:00 2001 From: JP Angelle Date: Mon, 26 Feb 2024 23:21:45 -0600 Subject: [PATCH 15/19] remove unnecessary width and height --- centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx | 2 +- centrifuge-app/src/components/PoolOverview/PoolStructure.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx b/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx index 00d329e6d1..30dda3b3ee 100644 --- a/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx +++ b/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx @@ -65,7 +65,7 @@ export const KeyMetrics = ({ assetType, averageMaturity, loans, poolStatus }: Pr ] return ( - + Key metrics diff --git a/centrifuge-app/src/components/PoolOverview/PoolStructure.tsx b/centrifuge-app/src/components/PoolOverview/PoolStructure.tsx index 6eabc1f26c..f3fc0a6c13 100644 --- a/centrifuge-app/src/components/PoolOverview/PoolStructure.tsx +++ b/centrifuge-app/src/components/PoolOverview/PoolStructure.tsx @@ -95,7 +95,7 @@ export const PoolStructure = ({ numOfTranches, poolId, poolStatus }: Props) => { } return ( - + Structure From 28214081db641bd90db2e40bd959d6b12ddc1ec8 Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Wed, 7 Feb 2024 22:31:16 +0100 Subject: [PATCH 16/19] Add pool analysis section to Create Pool form (#1939) --- .../src/components/IssuerSection.tsx | 35 +++++++++- .../IssuerCreatePool/PoolReportsInput.tsx | 47 +++++++++++++ .../src/pages/IssuerCreatePool/index.tsx | 38 ++++++++++- .../pages/IssuerPool/Configuration/Issuer.tsx | 67 +++++++++++++++++-- centrifuge-js/src/modules/pools.ts | 19 ++++++ 5 files changed, 199 insertions(+), 7 deletions(-) create mode 100644 centrifuge-app/src/pages/IssuerCreatePool/PoolReportsInput.tsx diff --git a/centrifuge-app/src/components/IssuerSection.tsx b/centrifuge-app/src/components/IssuerSection.tsx index 16a88fb1a9..942191d73a 100644 --- a/centrifuge-app/src/components/IssuerSection.tsx +++ b/centrifuge-app/src/components/IssuerSection.tsx @@ -53,6 +53,38 @@ export function IssuerSection({ metadata }: IssuerSectionProps) { ) } +export function ReportDetails({ metadata }: IssuerSectionProps) { + const cent = useCentrifuge() + const report = metadata?.pool?.reports?.[0] + return ( + report && ( + <> + + {report.author.avatar?.uri && ( + + )} + + Reviewer: {report.author.name} +
+ {report.author.title} +
+
+
+ + View full report + +
+ + ) + ) +} + export function IssuerDetails({ metadata }: IssuerSectionProps) { const cent = useCentrifuge() const [isDialogOpen, setIsDialogOpen] = React.useState(false) @@ -62,7 +94,8 @@ export function IssuerDetails({ metadata }: IssuerSectionProps) { {metadata?.pool?.issuer.logo && ( diff --git a/centrifuge-app/src/pages/IssuerCreatePool/PoolReportsInput.tsx b/centrifuge-app/src/pages/IssuerCreatePool/PoolReportsInput.tsx new file mode 100644 index 0000000000..1444ec83ff --- /dev/null +++ b/centrifuge-app/src/pages/IssuerCreatePool/PoolReportsInput.tsx @@ -0,0 +1,47 @@ +import { FileUpload, Grid, TextInput } from '@centrifuge/fabric' +import { Field, FieldProps } from 'formik' +import { FieldWithErrorMessage } from '../../components/FieldWithErrorMessage' +import { combineAsync, imageFile, maxFileSize, maxImageSize } from '../../utils/validation' +import { validate } from './validate' + +export function PoolReportsInput() { + return ( + + + + + + {({ field, meta, form }: FieldProps) => ( + { + form.setFieldTouched('reportAuthorAvatar', true, false) + form.setFieldValue('reportAuthorAvatar', file) + }} + label="Reviewer avatar (JPG/PNG/SVG, max 40x40px)" + errorMessage={meta.touched && meta.error ? meta.error : undefined} + accept="image/*" + /> + )} + + + ) +} diff --git a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx index 44a28bcfa6..628093a041 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx @@ -46,6 +46,7 @@ import { usePools } from '../../utils/usePools' import { truncate } from '../../utils/web3' import { AdminMultisigSection } from './AdminMultisig' import { IssuerInput } from './IssuerInput' +import { PoolReportsInput } from './PoolReportsInput' import { TrancheSection } from './TrancheInput' import { useStoredIssuer } from './useStoredIssuer' import { validate } from './validate' @@ -86,11 +87,15 @@ export const createEmptyTranche = (junior?: boolean): Tranche => ({ export type CreatePoolValues = Omit< PoolMetadataInput, - 'poolIcon' | 'issuerLogo' | 'executiveSummary' | 'adminMultisig' + 'poolIcon' | 'issuerLogo' | 'executiveSummary' | 'adminMultisig' | 'poolReport' > & { poolIcon: File | null issuerLogo: File | null executiveSummary: File | null + reportAuthorName: string + reportAuthorTitle: string + reportAuthorAvatar: File | null + reportUrl: string adminMultisigEnabled: boolean adminMultisig: Exclude } @@ -117,6 +122,10 @@ const initialValues: CreatePoolValues = { forum: '', email: '', details: [], + reportAuthorName: '', + reportAuthorTitle: '', + reportAuthorAvatar: null, + reportUrl: '', tranches: [createEmptyTranche(true)], adminMultisig: { @@ -332,6 +341,14 @@ function CreatePoolForm() { prevRiskBuffer = t.minRiskBuffer } }) + if (values.reportUrl) { + if (!values.reportAuthorName) { + errors = setIn(errors, 'reportAuthorName', 'Required') + } + if (!values.reportAuthorTitle) { + errors = setIn(errors, 'reportAuthorTitle', 'Required') + } + } return errors }, @@ -367,6 +384,22 @@ function CreatePoolForm() { metadataValues.executiveSummary = { uri: pinnedExecSummary.uri, mime: values.executiveSummary.type } metadataValues.poolIcon = { uri: pinnedPoolIcon.uri, mime: values.poolIcon.type } + if (values.reportUrl) { + let avatar = null + if (values.reportAuthorAvatar) { + const pinned = await lastValueFrom( + centrifuge.metadata.pinFile(await getFileDataURI(values.reportAuthorAvatar)) + ) + avatar = { uri: pinned.uri, mime: values.reportAuthorAvatar.type } + } + metadataValues.poolReport = { + authorAvatar: avatar, + authorName: values.reportAuthorName, + authorTitle: values.reportAuthorTitle, + url: values.reportUrl, + } + } + // tranches must be reversed (most junior is the first in the UI but the last in the API) const nonJuniorTranches = metadataValues.tranches.slice(1) const tranches = [ @@ -590,6 +623,9 @@ function CreatePoolForm() { + + + diff --git a/centrifuge-app/src/pages/IssuerPool/Configuration/Issuer.tsx b/centrifuge-app/src/pages/IssuerPool/Configuration/Issuer.tsx index f23f0689dc..c4af695e02 100644 --- a/centrifuge-app/src/pages/IssuerPool/Configuration/Issuer.tsx +++ b/centrifuge-app/src/pages/IssuerPool/Configuration/Issuer.tsx @@ -1,12 +1,12 @@ import { PoolMetadata } from '@centrifuge/centrifuge-js' import { useCentrifuge, useCentrifugeTransaction } from '@centrifuge/centrifuge-react' -import { Button, Stack } from '@centrifuge/fabric' -import { Form, FormikProvider, useFormik } from 'formik' +import { Button, Stack, Text } from '@centrifuge/fabric' +import { Form, FormikErrors, FormikProvider, setIn, useFormik } from 'formik' import * as React from 'react' import { useParams } from 'react-router' import { lastValueFrom } from 'rxjs' import { ButtonGroup } from '../../../components/ButtonGroup' -import { IssuerDetails } from '../../../components/IssuerSection' +import { IssuerDetails, ReportDetails } from '../../../components/IssuerSection' import { PageSection } from '../../../components/PageSection' import { getFileDataURI } from '../../../utils/getFileDataURI' import { useFile } from '../../../utils/useFile' @@ -15,6 +15,7 @@ import { useSuitableAccounts } from '../../../utils/usePermissions' import { usePool, usePoolMetadata } from '../../../utils/usePools' import { CreatePoolValues } from '../../IssuerCreatePool' import { IssuerInput } from '../../IssuerCreatePool/IssuerInput' +import { PoolReportsInput } from '../../IssuerCreatePool/PoolReportsInput' type Values = Pick< CreatePoolValues, @@ -27,7 +28,12 @@ type Values = Pick< | 'forum' | 'email' | 'details' -> + | 'reportUrl' + | 'reportAuthorName' + | 'reportAuthorTitle' +> & { + reportAuthorAvatar: string | null | File +} export function Issuer() { const { pid: poolId } = useParams<{ pid: string }>() @@ -50,6 +56,12 @@ export function Issuer() { forum: metadata?.pool?.links?.forum ?? '', email: metadata?.pool?.issuer?.email ?? '', details: metadata?.pool?.details, + reportUrl: metadata?.pool?.reports?.[0]?.uri ?? '', + reportAuthorName: metadata?.pool?.reports?.[0]?.author?.name ?? '', + reportAuthorTitle: metadata?.pool?.reports?.[0]?.author?.title ?? '', + reportAuthorAvatar: metadata?.pool?.reports?.[0]?.author?.avatar + ? `avatar.${metadata.pool.reports[0].author.avatar.mime?.split('/')[1]}` + : null, }), [metadata, logoFile] ) @@ -62,6 +74,20 @@ export function Issuer() { const form = useFormik({ initialValues, + validate: (values) => { + let errors: FormikErrors = {} + + if (values.reportUrl) { + if (!values.reportAuthorName) { + errors = setIn(errors, 'reportAuthorName', 'Required') + } + if (!values.reportAuthorTitle) { + errors = setIn(errors, 'reportAuthorTitle', 'Required') + } + } + + return errors + }, onSubmit: async (values, actions) => { const oldMetadata = metadata as PoolMetadata const execSummaryChanged = values.executiveSummary !== initialValues.executiveSummary @@ -107,6 +133,27 @@ export function Issuer() { }, } + if (values.reportUrl) { + let avatar = null + const avatarChanged = values.reportAuthorAvatar !== initialValues.reportAuthorAvatar + if (avatarChanged && values.reportAuthorAvatar) { + const pinned = await lastValueFrom( + cent.metadata.pinFile(await getFileDataURI(values.reportAuthorAvatar as File)) + ) + avatar = { uri: pinned.uri, mime: (values.reportAuthorAvatar as File).type } + } + newPoolMetadata.pool.reports = [ + { + author: { + avatar: avatar, + name: values.reportAuthorName, + title: values.reportAuthorTitle, + }, + uri: values.reportUrl, + }, + ] + } + execute([poolId, newPoolMetadata], { account }) actions.setSubmitting(false) }, @@ -157,10 +204,20 @@ export function Issuer() { } > {isEditing ? ( - + + + Pool analysis + + ) : ( + {metadata?.pool?.reports?.[0] && ( + + Pool analysis + + + )} )} diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index 1d58b182a0..36444bfbc8 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -610,6 +610,13 @@ export interface PoolMetadataInput { issuerLogo?: FileType | null issuerDescription: string + poolReport?: { + authorName: string + authorTitle: string + authorAvatar: FileType | null + url: string + } + executiveSummary: FileType | null website: string forum: string @@ -883,6 +890,18 @@ export function getPoolsModule(inst: Centrifuge) { details: metadata.details, status: 'open', listed: metadata.listed ?? true, + reports: metadata.poolReport + ? [ + { + author: { + name: metadata.poolReport.authorName, + title: metadata.poolReport.authorTitle, + avatar: metadata.poolReport.authorAvatar, + }, + uri: metadata.poolReport.url, + }, + ] + : undefined, }, pod: { node: metadata.podEndpoint ?? null, From b520fdb7ec994ade8b463059ad7bd5af7c98032e Mon Sep 17 00:00:00 2001 From: JP Angelle Date: Wed, 28 Feb 2024 09:47:25 -0600 Subject: [PATCH 17/19] increase image limit to 200x200 --- .../pages/IssuerCreatePool/PoolReportsInput.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/centrifuge-app/src/pages/IssuerCreatePool/PoolReportsInput.tsx b/centrifuge-app/src/pages/IssuerCreatePool/PoolReportsInput.tsx index 1444ec83ff..60b31f71c4 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/PoolReportsInput.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/PoolReportsInput.tsx @@ -8,11 +8,11 @@ export function PoolReportsInput() { return ( - + {({ field, meta, form }: FieldProps) => ( Date: Thu, 29 Feb 2024 10:01:14 -0600 Subject: [PATCH 18/19] fix: tweaks (#1988) * fix: tweaks * FIx page loading, set up cash transfers * Update tx history table * Fix cash assets * Fix status checks * Add missing dashes * Fix first investment * hide avg asset maturity for bt3/4 --------- Co-authored-by: Jeroen Offerijns --- .../Charts/PoolPerformanceChart.tsx | 24 +++- centrifuge-app/src/components/LoanList.tsx | 7 +- .../src/components/PoolOverview/Cashflows.tsx | 4 +- .../components/PoolOverview/KeyMetrics.tsx | 20 ++- .../components/PoolOverview/PoolStructure.tsx | 9 +- .../PoolOverview/TrancheTokenCards.tsx | 13 +- .../PoolOverview/TransactionHistory.tsx | 119 +++++++++++------- .../src/pages/Pool/Assets/index.tsx | 43 ++++--- .../src/pages/Pool/Overview/index.tsx | 2 +- .../src/utils/tinlake/useTinlakePools.ts | 33 +++-- centrifuge-js/src/modules/pools.ts | 28 +++++ 11 files changed, 191 insertions(+), 111 deletions(-) diff --git a/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx b/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx index a3a31c413a..6f2e82547a 100644 --- a/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx +++ b/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx @@ -5,6 +5,7 @@ import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YA import styled, { useTheme } from 'styled-components' import { daysBetween, formatDate } from '../../utils/date' import { formatBalance, formatBalanceAbbreviated } from '../../utils/formatting' +import { useLoans } from '../../utils/useLoans' import { useDailyPoolStates, usePool } from '../../utils/usePools' import { TooltipContainer, TooltipTitle } from './Tooltip' import { getRangeNumber } from './utils' @@ -35,22 +36,39 @@ function PoolPerformanceChart() { const { poolStates } = useDailyPoolStates(poolId) || {} const pool = usePool(poolId) const poolAge = pool.createdAt ? daysBetween(pool.createdAt, new Date()) : 0 + const loans = useLoans(poolId) + + const firstOriginationDate = loans?.reduce((acc, cur) => { + if ('originationDate' in cur) { + if (!acc) return cur.originationDate + return acc < cur.originationDate ? acc : cur.originationDate + } + return acc + }, '') + + const truncatedPoolStates = poolStates?.filter((poolState) => { + if (firstOriginationDate) { + return new Date(poolState.timestamp) >= new Date(firstOriginationDate) + } + return true + }) const [range, setRange] = React.useState<(typeof rangeFilters)[number]>({ value: 'ytd', label: 'Year to date' }) const rangeNumber = getRangeNumber(range.value, poolAge) const data: ChartData[] = React.useMemo( () => - poolStates?.map((day) => { + truncatedPoolStates?.map((day) => { const nav = day.poolState.portfolioValuation.toDecimal().toNumber() + day.poolState.totalReserve.toDecimal().toNumber() return { day: new Date(day.timestamp), nav } }) || [], - [poolStates] + [truncatedPoolStates] ) - if (poolStates && poolStates?.length < 1 && poolAge > 0) return No data available + if (truncatedPoolStates && truncatedPoolStates?.length < 1 && poolAge > 0) + return No data available // querying chain for more accurate data, since data for today from subquery is not necessarily up to date const todayAssetValue = pool?.nav.latest.toDecimal().toNumber() || 0 diff --git a/centrifuge-app/src/components/LoanList.tsx b/centrifuge-app/src/components/LoanList.tsx index 92394e5609..6d0f318664 100644 --- a/centrifuge-app/src/components/LoanList.tsx +++ b/centrifuge-app/src/components/LoanList.tsx @@ -15,6 +15,7 @@ import get from 'lodash/get' import * as React from 'react' import { useParams, useRouteMatch } from 'react-router' import currencyDollar from '../assets/images/currency-dollar.svg' +import daiLogo from '../assets/images/dai-logo.svg' import usdcLogo from '../assets/images/usdc-logo.svg' import { formatNftAttribute } from '../pages/Loan/utils' import { nftMetadataSchema } from '../schemas' @@ -72,7 +73,7 @@ export function LoanList({ loans }: Props) { const { data: templateMetadata } = useMetadata(templateId) const loansWithLabelStatus = React.useMemo(() => { return loans - .filter((loan) => 'valuationMethod' in loan.pricing && loan.pricing.valuationMethod !== 'cash') + .filter((loan) => isTinlakePool || ('valuationMethod' in loan.pricing && loan.pricing.valuationMethod !== 'cash')) .map((loan) => ({ ...loan, labelStatus: getLoanStatus(loan), @@ -265,7 +266,7 @@ function AssetMetadataField({ loan, name, attribute }: { loan: Row; name: string variant="body2" style={{ overflow: 'hidden', maxWidth: '300px', textOverflow: 'ellipsis' }} > - {formatNftAttribute(metadata?.properties?.[name], attribute)} + {metadata?.properties?.[name] ? formatNftAttribute(metadata?.properties?.[name], attribute) : '-'} ) @@ -279,7 +280,7 @@ function AssetName({ loan }: { loan: Row }) { return ( - + { const { pid: poolId } = useParams<{ pid: string }>() const { poolStates } = useDailyPoolStates(poolId) || {} const pool = usePool(poolId) - const loans = useLoans(poolId) as Loan[] | undefined | null + const loans = useLoans(poolId) const firstOriginationDate = loans?.reduce((acc, cur) => { if ('originationDate' in cur) { diff --git a/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx b/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx index 30dda3b3ee..b96bdd4f83 100644 --- a/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx +++ b/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx @@ -8,10 +8,10 @@ type Props = { assetType?: { class: string; subClass: string } averageMaturity: string loans: TinlakeLoan[] | Loan[] | null | undefined - poolStatus?: string + poolId: string } -export const KeyMetrics = ({ assetType, averageMaturity, loans, poolStatus }: Props) => { +export const KeyMetrics = ({ assetType, averageMaturity, loans, poolId }: Props) => { const ongoingAssetCount = loans && [...loans].filter((loan) => loan.status === 'Active' && !loan.outstandingDebt.isZero()).length @@ -27,6 +27,10 @@ export const KeyMetrics = ({ assetType, averageMaturity, loans, poolStatus }: Pr return loan.status === 'Active' && loan.pricing.maturityDate && days < 0 }).length + const isBT3BT4 = + poolId.toLowerCase() === '0x90040f96ab8f291b6d43a8972806e977631affde' || + poolId.toLowerCase() === '0x55d86d51ac3bcab7ab7d2124931fba106c8b60c7' + const metrics = [ { metric: 'Asset class', @@ -34,10 +38,14 @@ export const KeyMetrics = ({ assetType, averageMaturity, loans, poolStatus }: Pr startCase(assetType?.subClass) ).replace(/^Us /, 'US ')}`, }, - { - metric: 'Average asset maturity', - value: averageMaturity, - }, + ...(isBT3BT4 + ? [] + : [ + { + metric: 'Average asset maturity', + value: averageMaturity, + }, + ]), { metric: 'Total assets', value: loans?.length || 0, diff --git a/centrifuge-app/src/components/PoolOverview/PoolStructure.tsx b/centrifuge-app/src/components/PoolOverview/PoolStructure.tsx index f3fc0a6c13..0ec50d64b6 100644 --- a/centrifuge-app/src/components/PoolOverview/PoolStructure.tsx +++ b/centrifuge-app/src/components/PoolOverview/PoolStructure.tsx @@ -1,7 +1,6 @@ import { getChainInfo, useWallet } from '@centrifuge/centrifuge-react' import { Box, Card, Grid, Stack, Text, Tooltip } from '@centrifuge/fabric' import capitalize from 'lodash/capitalize' -import { formatDate } from '../../utils/date' import { useActiveDomains } from '../../utils/useLiquidityPools' import { useInvestorTransactions } from '../../utils/usePools' @@ -39,10 +38,10 @@ export const PoolStructure = ({ numOfTranches, poolId, poolStatus }: Props) => { metric: 'Tranche structure', value: numOfTranches === 1 ? 'Unitranche' : `${numOfTranches} tranches`, }, - { - metric: 'First investment', - value: firstInvestment ? formatDate(firstInvestment) : '-', - }, + // { + // metric: 'First investment', + // value: firstInvestment ? formatDate(firstInvestment) : '-', + // }, { metric: 'Available networks', value: `Centrifuge${deployedLpChains.length ? `, ${deployedLpChains.join(', ')}` : ''}`, diff --git a/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx b/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx index cba3047572..6a4ae52dbf 100644 --- a/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx +++ b/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx @@ -62,14 +62,9 @@ const TrancheTokenCard = ({ }` const calculateApy = () => { - if (isTinlakePool && trancheText === 'senior') { - return formatPercentage(trancheToken.apy) - } - - if (daysSinceCreation < 30 || !trancheToken.yield30DaysAnnualized) { - return 'N/A' - } - + if (poolId === '4139607887') return formatPercentage(5) + if (isTinlakePool && trancheText === 'senior') return formatPercentage(trancheToken.apy) + if (daysSinceCreation < 30 || !trancheToken.yield30DaysAnnualized) return 'N/A' return formatPercentage(new Perquintill(trancheToken.yield30DaysAnnualized)) } @@ -82,7 +77,7 @@ const TrancheTokenCard = ({ - + {calculateApy()} diff --git a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx index b71d93d718..4c3e10d801 100644 --- a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx +++ b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx @@ -1,4 +1,4 @@ -import { AssetTransaction, CurrencyBalance } from '@centrifuge/centrifuge-js' +import { AssetTransaction, AssetTransactionType, AssetType, CurrencyBalance } from '@centrifuge/centrifuge-js' import { AnchorButton, IconDownload, IconExternalLink, Shelf, Stack, StatusChip, Text } from '@centrifuge/fabric' import BN from 'bn.js' import { nftMetadataSchema } from '../../schemas' @@ -19,9 +19,7 @@ type Row = { assetName: string } -const getTransactionTypeStatus = (type: string) => { - if (type === 'Principal payment' || type === 'Repaid') return 'warning' - if (type === 'Interest') return 'ok' +const getTransactionTypeStatus = (type: string): 'default' | 'info' | 'ok' | 'warning' | 'critical' => { return 'default' } @@ -90,8 +88,17 @@ export const TransactionHistory = ({ poolId, preview = true }: { poolId: string; nftMetadataSchema ) - const getLabelAndAmount = (transaction: AssetTransaction) => { - if (transaction.type === 'BORROWED') { + const getLabelAndAmount = ( + transaction: Omit & { type: AssetTransactionType | 'SETTLED' } + ) => { + if (transaction.asset.type == AssetType.OffchainCash) { + return { + label: 'Cash transfer', + amount: transaction.amount, + } + } + + if (transaction.type === 'BORROWED' || transaction.type === 'SETTLED') { return { label: 'Purchase', amount: transaction.amount, @@ -110,52 +117,72 @@ export const TransactionHistory = ({ poolId, preview = true }: { poolId: string; } } - const csvData = transactions - ?.filter( + const settlements = transactions?.reduce((acc, transaction, index) => { + if (transaction.hash === transactions[index + 1]?.hash) { + acc[transaction.hash] = { ...transaction, type: 'SETTLED' } + } + + return acc + }, {} as Record & { type: AssetTransactionType | 'SETTLED' }>) + + const transformedTransactions = [ + ...(transactions?.filter((transaction) => !settlements?.[transaction.hash]) || []), + ...Object.values(settlements || []), + ] + .filter( (transaction) => transaction.type !== 'CREATED' && transaction.type !== 'CLOSED' && transaction.type !== 'PRICED' ) - .map((transaction) => { - const { label, amount } = getLabelAndAmount(transaction) - const [, id] = transaction.asset.id.split('-') - return { - Type: label, - 'Transaction Date': `"${formatDate(transaction.timestamp, { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - })}"`, - 'Asset Name': assetMetadata[Number(id) - 1]?.data?.name || '-', - Amount: amount ? `"${formatBalance(amount, 'USD', 2, 2)}"` : '-', - Transaction: `${import.meta.env.REACT_APP_SUBSCAN_URL}/extrinsic/${transaction.hash}`, - } - }) + .sort((a, b) => (a.timestamp > b.timestamp ? -1 : 1)) + + const csvData = transformedTransactions.map((transaction) => { + const { label, amount } = getLabelAndAmount(transaction) + const [, id] = transaction.asset.id.split('-') + return { + Type: label, + 'Transaction Date': `"${formatDate(transaction.timestamp, { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + })}"`, + 'Asset Name': + transaction.asset.type == AssetType.OffchainCash + ? transaction.type === 'BORROWED' + ? `Onchain reserve > Settlement Account` + : `Settlement Account > onchain reserve` + : transaction.type === 'SETTLED' + ? `Settlement Account > ${assetMetadata[Number(id) - 1].data?.name || '-'}` + : assetMetadata[Number(id) - 1].data?.name || '-', + Amount: amount ? `"${formatBalance(amount, 'USD', 2, 2)}"` : '-', + Transaction: `${import.meta.env.REACT_APP_SUBSCAN_URL}/extrinsic/${transaction.hash}`, + } + }) const csvUrl = csvData?.length ? getCSVDownloadUrl(csvData) : '' const tableData = - transactions - ?.filter( - (transaction) => - transaction.type !== 'CREATED' && transaction.type !== 'CLOSED' && transaction.type !== 'PRICED' - ) - .sort((a, b) => (a.timestamp > b.timestamp ? -1 : 1)) - .slice(0, preview ? 8 : Infinity) - .map((transaction) => { - const [, id] = transaction.asset.id.split('-') - const { label, amount } = getLabelAndAmount(transaction) - return { - type: label, - transactionDate: transaction.timestamp, - assetId: transaction.asset.id, - assetName: assetMetadata[Number(id) - 1]?.data?.name, - amount: amount || 0, - hash: transaction.hash, - } - }) || [] + transformedTransactions.slice(0, preview ? 8 : Infinity).map((transaction) => { + const [, id] = transaction.asset.id.split('-') + const { label, amount } = getLabelAndAmount(transaction) + return { + type: label, + transactionDate: transaction.timestamp, + assetId: transaction.asset.id, + assetName: + transaction.asset.type == AssetType.OffchainCash + ? transaction.type === 'BORROWED' + ? `Onchain reserve > Settlement account` + : `Settlement account > onchain reserve` + : transaction.type === 'SETTLED' + ? `${assetMetadata[Number(id) - 1].data?.name || '-'}` + : assetMetadata[Number(id) - 1].data?.name || '-', + amount: amount || 0, + hash: transaction.hash, + } + }) || [] return ( diff --git a/centrifuge-app/src/pages/Pool/Assets/index.tsx b/centrifuge-app/src/pages/Pool/Assets/index.tsx index 5133cee25c..044e73f2c5 100644 --- a/centrifuge-app/src/pages/Pool/Assets/index.tsx +++ b/centrifuge-app/src/pages/Pool/Assets/index.tsx @@ -3,6 +3,7 @@ import { Box, Shelf, Text } from '@centrifuge/fabric' import * as React from 'react' import { useParams } from 'react-router' import currencyDollar from '../../../assets/images/currency-dollar.svg' +import daiLogo from '../../../assets/images/dai-logo.svg' import usdcLogo from '../../../assets/images/usdc-logo.svg' import { LayoutBase } from '../../../components/LayoutBase' import { LoadBoundary } from '../../../components/LoadBoundary' @@ -72,30 +73,34 @@ export function PoolDetailAssets() { { label: ( - + ), value: formatBalance(pool.reserve.total || 0, pool.currency.symbol), }, - { - label: ( - - - - - ), - value: formatBalance(offchainReserve, 'USD'), - }, - { - label: 'Total assets', - value: loans.length, - }, - { label: , value: ongoingAssets.length || 0 }, - { - label: 'Overdue assets', - value: 0 ? 'statusCritical' : 'inherit'}>{overdueAssets.length}, - }, + ...(!isTinlakePool + ? [ + { + label: ( + + + + + ), + value: formatBalance(offchainReserve, 'USD'), + }, + { + label: 'Total assets', + value: loans.length, + }, + { label: , value: ongoingAssets.length || 0 }, + { + label: 'Overdue assets', + value: 0 ? 'statusCritical' : 'inherit'}>{overdueAssets.length}, + }, + ] + : []), ] return ( diff --git a/centrifuge-app/src/pages/Pool/Overview/index.tsx b/centrifuge-app/src/pages/Pool/Overview/index.tsx index adfb2faeee..135c621a27 100644 --- a/centrifuge-app/src/pages/Pool/Overview/index.tsx +++ b/centrifuge-app/src/pages/Pool/Overview/index.tsx @@ -114,7 +114,7 @@ export function PoolDetailOverview() { assetType={metadata?.pool?.asset} averageMaturity={averageMaturity} loans={loans} - poolStatus={metadata?.pool?.status} + poolId={poolId} /> diff --git a/centrifuge-app/src/utils/tinlake/useTinlakePools.ts b/centrifuge-app/src/utils/tinlake/useTinlakePools.ts index a69a842493..a169da0476 100644 --- a/centrifuge-app/src/utils/tinlake/useTinlakePools.ts +++ b/centrifuge-app/src/utils/tinlake/useTinlakePools.ts @@ -1,12 +1,12 @@ import { - CurrencyBalance, - Perquintill, - Pool, - PoolMetadata, - Price, - Rate, - TinlakeLoan, - TokenBalance, + CurrencyBalance, + Perquintill, + Pool, + PoolMetadata, + Price, + Rate, + TinlakeLoan, + TokenBalance, } from '@centrifuge/centrifuge-js' import { useCentrifuge } from '@centrifuge/centrifuge-react' import { BigNumber } from '@ethersproject/bignumber' @@ -20,14 +20,14 @@ import { currencies } from './currencies' import { Call, multicall } from './multicall' import { Fixed27Base } from './ratios' import { - ActivePool, - ArchivedPool, - IpfsPools, - LaunchingPool, - PoolMetadataDetails, - PoolStatus, - TinlakeMetadataPool, - UpcomingPool, + ActivePool, + ArchivedPool, + IpfsPools, + LaunchingPool, + PoolMetadataDetails, + PoolStatus, + TinlakeMetadataPool, + UpcomingPool, } from './types' export interface PoolData { @@ -789,7 +789,6 @@ async function getPools(pools: IpfsPools): Promise<{ pools: TinlakePool[] }> { } }) - return { pools: combined } } diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index 36444bfbc8..76b9855073 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -733,6 +733,12 @@ type InvestorTransaction = { evmAddress?: string } +export enum AssetType { + OnchainCash = 'OnchainCash', + OffchainCash = 'OffchainCash', + Other = 'Other', +} + export type AssetTransaction = { id: string timestamp: string @@ -749,6 +755,17 @@ export type AssetTransaction = { asset: { id: string metadata: string + type: AssetType + } + fromAsset?: { + id: string + metadata: string + type: AssetType + } + toAsset?: { + id: string + metadata: string + type: AssetType } } @@ -2560,6 +2577,17 @@ export function getPoolsModule(inst: Centrifuge) { asset { id metadata + type + } + fromAsset { + id + metadata + type + } + toAsset { + id + metadata + type } } } From 763126f4b1afb57d6c4f7e71e9b50b011fcfc53c Mon Sep 17 00:00:00 2001 From: JP Angelle Date: Thu, 29 Feb 2024 10:10:39 -0600 Subject: [PATCH 19/19] revert env --- centrifuge-app/.env-config/.env.development | 30 ++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/centrifuge-app/.env-config/.env.development b/centrifuge-app/.env-config/.env.development index d96792a54c..57881c1a10 100644 --- a/centrifuge-app/.env-config/.env.development +++ b/centrifuge-app/.env-config/.env.development @@ -1,21 +1,21 @@ -REACT_APP_COLLATOR_WSS_URL=wss://fullnode.parachain.centrifuge.io -REACT_APP_DEFAULT_NODE_URL='' +REACT_APP_COLLATOR_WSS_URL=wss://fullnode.development.cntrfg.com +REACT_APP_DEFAULT_NODE_URL=https://pod-development.k-f.dev REACT_APP_DEFAULT_UNLIST_POOLS=false -REACT_APP_FAUCET_URL= +REACT_APP_FAUCET_URL=https://europe-central2-peak-vista-185616.cloudfunctions.net/faucet-api-dev REACT_APP_IPFS_GATEWAY=https://centrifuge.mypinata.cloud/ -REACT_APP_IS_DEMO= +REACT_APP_IS_DEMO=false REACT_APP_NETWORK=centrifuge -REACT_APP_ONBOARDING_API_URL=https://europe-central2-centrifuge-production-x.cloudfunctions.net/onboarding-api-production -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_ONBOARDING_API_URL=https://europe-central2-peak-vista-185616.cloudfunctions.net/onboarding-api-dev +REACT_APP_PINNING_API_URL=https://europe-central2-peak-vista-185616.cloudfunctions.net/pinning-api-dev +REACT_APP_POOL_CREATION_TYPE=immediate +REACT_APP_RELAY_WSS_URL=wss://fullnode-relay.development.cntrfg.com +REACT_APP_SUBQUERY_URL=https://api.subquery.network/sq/centrifuge/pools-development REACT_APP_SUBSCAN_URL=https://centrifuge.subscan.io -REACT_APP_TINLAKE_NETWORK=mainnet +REACT_APP_TINLAKE_NETWORK=goerli REACT_APP_INFURA_KEY=bf808e7d3d924fbeb74672d9341d0550 -REACT_APP_ONFINALITY_KEY=7e8caebc-b052-402d-87a4-e990b67ed612 -REACT_APP_WHITELISTED_ACCOUNTS='' -REACT_APP_REWARDS_TREE_URL=https://storage.googleapis.com/rad-rewards-trees-mainnet-production/latest.json -REACT_APP_MEMBERLIST_ADMIN_PURE_PROXY=kALJqPUHFzDR2VkoQYWefPQyzjGzKznNny2smXGQpSf3aMw19 -REACT_APP_WALLETCONNECT_ID=c32fa79350803519804a67fcab0b742a +REACT_APP_ONFINALITY_KEY=0e1c049f-d876-4e77-a45f-b5afdf5739b2 +REACT_APP_WHITELISTED_ACCOUNTS= REACT_APP_TINLAKE_SUBGRAPH_URL=https://api.goldsky.com/api/public/project_clhi43ef5g4rw49zwftsvd2ks/subgraphs/main/prod/gn +REACT_APP_REWARDS_TREE_URL=https://storage.googleapis.com/rad-rewards-trees-kovan-staging/latest.json +REACT_APP_WALLETCONNECT_ID=c32fa79350803519804a67fcab0b742a +REACT_APP_MEMBERLIST_ADMIN_PURE_PROXY=kAJ27w29x7gHM75xajP2yXVLjVBaKmmUTxHwgRuCoAcWaoEiz