From 17710dc5a295fb742c225cd0911572f7811f886b Mon Sep 17 00:00:00 2001 From: katty barroso Date: Fri, 25 Oct 2024 11:17:40 +0200 Subject: [PATCH 01/20] Add updates to asset loan list --- centrifuge-app/src/components/DataTable.tsx | 1 - centrifuge-app/src/components/LoanList.tsx | 352 +++++++----------- centrifuge-app/src/components/PageSummary.tsx | 26 +- .../src/pages/Pool/Assets/OffchainMenu.tsx | 83 +++++ .../src/pages/Pool/Assets/index.tsx | 116 +++--- fabric/src/theme/tokens/typography.ts | 6 + fabric/src/theme/types.ts | 1 + 7 files changed, 316 insertions(+), 269 deletions(-) create mode 100644 centrifuge-app/src/pages/Pool/Assets/OffchainMenu.tsx diff --git a/centrifuge-app/src/components/DataTable.tsx b/centrifuge-app/src/components/DataTable.tsx index 37c927dbe0..4a49bd0efe 100644 --- a/centrifuge-app/src/components/DataTable.tsx +++ b/centrifuge-app/src/components/DataTable.tsx @@ -486,6 +486,5 @@ const StyledHeader = styled(Text)` &:hover, &:focus-visible { cursor: pointer; - color: ${({ theme }) => theme.colors.textInteractiveHover}; } ` diff --git a/centrifuge-app/src/components/LoanList.tsx b/centrifuge-app/src/components/LoanList.tsx index 9b5876f27f..9e384768d0 100644 --- a/centrifuge-app/src/components/LoanList.tsx +++ b/centrifuge-app/src/components/LoanList.tsx @@ -1,8 +1,7 @@ import { useBasePath } from '@centrifuge/centrifuge-app/src/utils/useBasePath' -import { CurrencyBalance, Loan, Rate, TinlakeLoan } from '@centrifuge/centrifuge-js' +import { CurrencyBalance, Loan, TinlakeLoan } from '@centrifuge/centrifuge-js' import { Box, - IconChevronRight, Pagination, PaginationContainer, Shelf, @@ -14,45 +13,33 @@ import { import get from 'lodash/get' 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 { formatNftAttribute } from '../pages/Loan/utils' import { nftMetadataSchema } from '../schemas' -import { LoanTemplate, LoanTemplateAttribute } from '../types' import { formatDate } from '../utils/date' -import { formatBalance } from '../utils/formatting' +import { formatBalance, formatPercentage } from '../utils/formatting' import { useFilters } from '../utils/useFilters' import { useMetadata } from '../utils/useMetadata' import { useCentNFT } from '../utils/useNFTs' -import { usePool, usePoolMetadata } from '../utils/usePools' -import { Column, DataTable, FilterableTableHeader, SortableTableHeader } from './DataTable' +import { useAllPoolAssetSnapshots, usePool } from '../utils/usePools' +import { Column, DataTable, SortableTableHeader } from './DataTable' import { LoadBoundary } from './LoadBoundary' -import { LoanLabel, getLoanLabelStatus } from './LoanLabel' import { prefetchRoute } from './Root' -import { Tooltips } from './Tooltips' type Row = (Loan | TinlakeLoan) & { idSortKey: number originationDateSortKey: string status: 'Created' | 'Active' | 'Closed' | '' maturityDate: string | null + marketPrice: CurrencyBalance + marketValue: CurrencyBalance + unrealizedPL: CurrencyBalance + realizedPL: CurrencyBalance + portfolioPercentage: string } type Props = { loans: Loan[] | TinlakeLoan[] } -const getLoanStatus = (loan: Loan | TinlakeLoan) => { - const [labelType, label] = getLoanLabelStatus(loan) - - if (label.includes('Due')) { - return labelType === 'critical' ? 'Overdue' : 'Ongoing' - } - - return label -} - export function LoanList({ loans }: Props) { const { pid: poolId } = useParams<{ pid: string }>() if (!poolId) throw new Error('Pool not found') @@ -60,58 +47,69 @@ export function LoanList({ loans }: Props) { const pool = usePool(poolId) const isTinlakePool = poolId?.startsWith('0x') const basePath = useBasePath() + const snapshots = useAllPoolAssetSnapshots(pool.id, new Date().toString()) + const loansData = isTinlakePool + ? loans + : (loans ?? []).filter((loan) => 'valuationMethod' in loan.pricing && loan.pricing.valuationMethod !== 'cash') + + const snapshotsValues = + snapshots?.reduce((acc: { [key: string]: any }, snapshot) => { + const id = snapshot.assetId.split('-')[1] + acc[id] = { + marketPrice: snapshot.currentPrice, + marketValue: snapshot.presentValue, + unrealizedPL: snapshot.unrealizedProfitAtMarketPrice, + realizedPL: snapshot.sumRealizedProfitFifo, + } + return acc + }, {}) ?? {} + + const totalMarketValue = Object.values(snapshotsValues).reduce((sum, snapshot) => { + return sum + (snapshot.marketValue?.toDecimal().toNumber() ?? 0) + }, 0) - const { data: poolMetadata } = usePoolMetadata(pool) - const templateIds = poolMetadata?.loanTemplates?.map((s) => s.id) ?? [] - const templateId = templateIds.at(-1) - const { data: templateMetadata } = useMetadata(templateId) const loansWithLabelStatus = React.useMemo(() => { - return loans - .filter((loan) => isTinlakePool || ('valuationMethod' in loan.pricing && loan.pricing.valuationMethod !== 'cash')) - .map((loan) => ({ - ...loan, - labelStatus: getLoanStatus(loan), - })) - .sort((a, b) => { - const aId = get(a, 'id') as string - const bId = get(b, 'id') as string + return loansData.sort((a, b) => { + const aId = get(a, 'id') as string + const bId = get(b, 'id') as string + + return aId.localeCompare(bId) + }) + }, [isTinlakePool, loansData]) - return aId.localeCompare(bId) - }) - }, [isTinlakePool, loans]) const filters = useFilters({ - data: loansWithLabelStatus, + data: loansWithLabelStatus as Loan[], }) React.useEffect(() => { prefetchRoute('/pools/1/assets/1') }, []) - const additionalColumns: Column[] = - templateMetadata?.keyAttributes?.map((key) => { - const attr = templateMetadata.attributes![key] - return { - align: 'left', - header: attr.label, - cell: (l: Row) => , - } - }) || [] + const rows: Row[] = filters.data.map((loan) => { + const snapshot = snapshotsValues?.[loan.id] + const marketValue = snapshot?.marketValue?.toDecimal().toNumber() ?? 0 - const rows: Row[] = filters.data.map((loan) => ({ - nftIdSortKey: loan.asset.nftId, - idSortKey: parseInt(loan.id, 10), - outstandingDebtSortKey: loan.status !== 'Closed' && loan?.outstandingDebt?.toDecimal().toNumber(), - originationDateSortKey: - loan.status === 'Active' && - loan?.originationDate && - 'interestRate' in loan.pricing && - !loan?.pricing.interestRate?.isZero() && - !loan?.totalBorrowed?.isZero() - ? loan.originationDate - : '', - maturityDate: loan.pricing.maturityDate, - ...loan, - })) + const portfolioPercentage = + loan.status === 'Closed' || totalMarketValue === 0 ? 0 : (marketValue / totalMarketValue) * 100 + + return { + ...snapshot, + nftIdSortKey: loan.asset.nftId, + idSortKey: parseInt(loan.id, 10), + outstandingDebtSortKey: loan.status !== 'Closed' && loan?.outstandingDebt?.toDecimal().toNumber(), + originationDateSortKey: + loan.status === 'Active' && + loan?.originationDate && + 'interestRate' in loan.pricing && + !loan?.pricing.interestRate?.isZero() && + !loan?.totalBorrowed?.isZero() + ? loan.originationDate + : '', + maturityDate: loan.pricing.maturityDate, + portfolioPercentage, + ...loan, + } + }) const hasMaturityDate = rows.some((loan) => loan.maturityDate) @@ -121,25 +119,21 @@ export function LoanList({ loans }: Props) { header: , cell: (l: Row) => , sortKey: 'idSortKey', - width: 'minmax(300px, 1fr)', }, - ...(additionalColumns?.length - ? additionalColumns - : [ - { - align: 'left', - header: , - cell: (l: Row) => { - if (l.poolId.startsWith('0x') && l.id !== '0') { - return formatDate((l as TinlakeLoan).originationDate) - } - return l.status === 'Active' && 'valuationMethod' in l.pricing && l.pricing.valuationMethod !== 'cash' - ? formatDate(l.originationDate) - : '-' - }, - sortKey: 'originationDateSortKey', - }, - ]), + + { + align: 'left', + header: , + cell: (l: Row) => { + if (l.poolId.startsWith('0x') && l.id !== '0') { + return formatDate((l as TinlakeLoan).originationDate) + } + return l.status === 'Active' && 'valuationMethod' in l.pricing && l.pricing.valuationMethod !== 'cash' + ? formatDate(l.originationDate) + : '-' + }, + sortKey: 'originationDateSortKey', + }, ...(hasMaturityDate ? [ { @@ -157,87 +151,82 @@ export function LoanList({ loans }: Props) { }, ] : []), - { - align: 'right', - header: , - cell: (l: Row) => , - sortKey: 'outstandingDebtSortKey', - }, - { - align: 'left', - header: ( - labelStatus))]} - /> - ), - cell: (l: Row) => , - width: '100px', - }, - { - header: '', - cell: (l: Row) => (l.status ? : ''), - width: '52px', - }, + ...(isTinlakePool + ? [] + : [ + { + align: 'left', + header: , + cell: (l: Row) => , + sortKey: 'outstandingDebtSortKey', + }, + ]), + ...(isTinlakePool + ? [] + : [ + { + align: 'left', + header: , + cell: (l: Row) => formatBalance(l.marketPrice ?? '', pool.currency, 2, 0), + sortKey: 'marketPriceSortKey', + }, + ]), + ...(isTinlakePool + ? [] + : [ + { + align: 'left', + header: , + cell: (l: Row) => formatBalance(l.marketValue ?? 0, pool.currency, 2, 0), + sortKey: 'marketValueSortKey', + }, + ]), + ...(isTinlakePool + ? [] + : [ + { + align: 'left', + header: , + cell: (l: Row) => formatBalance(l.unrealizedPL ?? '', pool.currency, 2, 0), + sortKey: 'unrealizedPLSortKey', + }, + ]), + ...(isTinlakePool + ? [] + : [ + { + align: 'left', + header: , + cell: (l: Row) => formatBalance(l.realizedPL ?? '', pool.currency, 2, 0), + sortKey: 'realizedPLSortKey', + }, + ]), + ...(isTinlakePool + ? [] + : [ + { + align: 'left', + header: , + cell: (l: Row) => formatPercentage(l.portfolioPercentage ?? 0, true, undefined, 1), + sortKey: 'portfolioSortKey', + width: '80px', + }, + ]), ].filter(Boolean) as Column[] - const pinnedData: Row[] = [ - { - id: '0', - // @ts-expect-error - status: '', - poolId: pool.id, - pricing: { - valuationMethod: 'discountedCashFlow', - maxBorrowAmount: 'upToTotalBorrowed', - value: CurrencyBalance.fromFloat(0, 18), - maturityDate: '', - maturityExtensionDays: 0, - advanceRate: Rate.fromFloat(0), - interestRate: Rate.fromFloat(0), - }, - asset: { collectionId: '', nftId: '' }, - totalBorrowed: CurrencyBalance.fromFloat(0, 18), - totalRepaid: CurrencyBalance.fromFloat(0, 18), - outstandingDebt: CurrencyBalance.fromFloat(0, 18), - }, - ...loans - .filter((loan) => 'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'cash') - .map((loan) => { - return { - nftIdSortKey: loan.asset.nftId, - idSortKey: parseInt(loan.id, 10), - outstandingDebtSortKey: loan.status !== 'Closed' && loan?.outstandingDebt?.toDecimal().toNumber(), - originationDateSortKey: - loan.status === 'Active' && - loan?.originationDate && - 'interestRate' in loan.pricing && - !loan?.pricing.interestRate?.isZero() && - !loan?.totalBorrowed?.isZero() - ? loan.originationDate - : '', - ...loan, - maturityDate: loan.pricing.maturityDate, - } - }), - ] - const pagination = usePagination({ data: rows, pageSize: 20 }) return ( - + `${basePath}/${poolId}/assets/${row.id}`} pageSize={20} page={pagination.page} - pinnedData={pinnedData} defaultSortKey="maturityDate" /> @@ -252,46 +241,11 @@ export function LoanList({ loans }: Props) { ) } -function AssetMetadataField({ loan, name, attribute }: { loan: Row; name: string; attribute: LoanTemplateAttribute }) { - const isTinlakePool = loan.poolId.startsWith('0x') - const nft = useCentNFT(loan.asset.collectionId, loan.asset.nftId, false, isTinlakePool) - const { data: metadata, isLoading } = useMetadata(nft?.metadataUri, nftMetadataSchema) - - return ( - - - {metadata?.properties?.[name] ? formatNftAttribute(metadata?.properties?.[name], attribute) : '-'} - - - ) -} - export function AssetName({ loan }: { loan: Pick }) { const isTinlakePool = loan.poolId.startsWith('0x') const nft = useCentNFT(loan.asset.collectionId, loan.asset.nftId, false, isTinlakePool) const { data: metadata, isLoading } = useMetadata(nft?.metadataUri, nftMetadataSchema) - if (loan.id === '0') { - return ( - - - - - - Onchain reserve} /> - - - ) - } + if (loan.id === '0') return if (isTinlakePool) { return ( @@ -310,24 +264,6 @@ export function AssetName({ loan }: { loan: Pick - - - - - {metadata?.name}} /> - - - ) - } - return ( - {data?.map(({ label, value }, index) => ( + {data?.map(({ label, value, heading }, index) => ( {label} - {value} + + {value} + ))} - {children} + {children} ) } diff --git a/centrifuge-app/src/pages/Pool/Assets/OffchainMenu.tsx b/centrifuge-app/src/pages/Pool/Assets/OffchainMenu.tsx new file mode 100644 index 0000000000..b6eb85a8fa --- /dev/null +++ b/centrifuge-app/src/pages/Pool/Assets/OffchainMenu.tsx @@ -0,0 +1,83 @@ +import { Loan } from '@centrifuge/centrifuge-js' +import { + Box, + IconChevronDown, + IconChevronRight, + IconChevronUp, + Menu, + MenuItem, + MenuItemGroup, + Popover, + Text, +} from '@centrifuge/fabric' +import { useLocation, useNavigate } from 'react-router' +import styled from 'styled-components' +import { nftMetadataSchema } from '../../../../src/schemas' +import { useMetadata } from '../../../../src/utils/useMetadata' +import { useCentNFT } from '../../../../src/utils/useNFTs' + +type Props = { + value: string + loans: Loan[] +} + +type LoanOptionProps = { + loan: Loan +} + +const StyledButton = styled(Box)` + background: transparent; + border: none; + margin: 0; + padding: 0; + display: flex; + align-items: center; + font-family: Inter, sans-serif; +` + +const LoanOption: React.FC = ({ loan }) => { + const navigate = useNavigate() + const location = useLocation() + const nft = useCentNFT(loan.asset.collectionId, loan.asset.nftId, false, loan.poolId.startsWith('0x')) + const { data } = useMetadata(nft?.metadataUri, nftMetadataSchema) + + const handleNavigate = (id: string) => { + navigate(`${location.pathname}/${id}`) + } + + return ( + + } + onClick={() => handleNavigate(loan.id)} + /> + + ) +} + +export const OffchainMenu = ({ value, loans }: Props) => { + if (!loans.length) return null + return ( + ( + + + {value} + {state.isOpen ? : } + + + )} + renderContent={(props, ref) => ( + + + {loans.map((loan) => ( + + ))} + + + )} + /> + ) +} diff --git a/centrifuge-app/src/pages/Pool/Assets/index.tsx b/centrifuge-app/src/pages/Pool/Assets/index.tsx index e7345950b7..deacc444f1 100644 --- a/centrifuge-app/src/pages/Pool/Assets/index.tsx +++ b/centrifuge-app/src/pages/Pool/Assets/index.tsx @@ -1,10 +1,10 @@ -import { ActiveLoan, Loan } from '@centrifuge/centrifuge-js' -import { Box, Shelf, Text } from '@centrifuge/fabric' +import { CurrencyBalance, Loan } from '@centrifuge/centrifuge-js' +import { Box, Button, IconChevronRight, IconDownload, IconPlus, 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 { useNavigate, useParams } from 'react-router' +import styled from 'styled-components' +import { RouterTextLink } from '../../../../src/components/TextLink' +import { useBasePath } from '../../../../src/utils/useBasePath' import { LoadBoundary } from '../../../components/LoadBoundary' import { LoanList } from '../../../components/LoanList' import { PageSummary } from '../../../components/PageSummary' @@ -16,6 +16,13 @@ import { useLoans } from '../../../utils/useLoans' import { useSuitableAccounts } from '../../../utils/usePermissions' import { usePool } from '../../../utils/usePools' import { PoolDetailHeader } from '../Header' +import { OffchainMenu } from './OffchainMenu' + +const StyledRouterTextLink = styled(RouterTextLink)` + text-decoration: unset; + display: flex; + align-items: center; +` export function PoolDetailAssetsTab() { return ( @@ -30,12 +37,17 @@ export function PoolDetailAssetsTab() { export function PoolDetailAssets() { const { pid: poolId } = useParams<{ pid: string }>() + const navigate = useNavigate() if (!poolId) throw new Error('Pool not found') const pool = usePool(poolId) const loans = useLoans(poolId) const isTinlakePool = poolId.startsWith('0x') + const basePath = useBasePath() + const cashLoans = (loans ?? []).filter( + (loan) => 'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'cash' + ) if (!pool) return null @@ -48,19 +60,10 @@ export function PoolDetailAssets() { ) } - function hasValuationMethod(pricing: any): pricing is { valuationMethod: string } { + function hasValuationMethod(pricing: any): pricing is { valuationMethod: string; presentValue: CurrencyBalance } { return pricing && typeof pricing.valuationMethod === 'string' } - const ongoingAssets = (loans && - [...loans].filter( - (loan) => - loan.status === 'Active' && - hasValuationMethod(loan.pricing) && - loan.pricing.valuationMethod !== 'cash' && - !loan.outstandingDebt.isZero() - )) as ActiveLoan[] - const offchainAssets = !isTinlakePool ? loans.filter( (loan) => hasValuationMethod((loan as Loan).pricing) && (loan as Loan).pricing.valuationMethod === 'cash' @@ -71,48 +74,40 @@ export function PoolDetailAssets() { Dec(0) ) - const overdueAssets = loans.filter( - (loan) => - loan.status === 'Active' && - loan.outstandingDebt.gtn(0) && - loan.pricing.maturityDate && - new Date(loan.pricing.maturityDate).getTime() < Date.now() - ) + const totalPresentValue = loans.reduce((sum, loan) => { + if (hasValuationMethod(loan.pricing) && loan.pricing.valuationMethod !== 'cash') { + return sum.add(loan.pricing.presentValue?.toDecimal() || Dec(0)) + } + return sum + }, Dec(0)) - const pageSummaryData: { label: React.ReactNode; value: React.ReactNode }[] = [ + const pageSummaryData: { label: React.ReactNode; value: React.ReactNode; heading?: boolean }[] = [ { - label: , + label: 'Total NAV', value: formatBalance(pool.nav.total.toDecimal(), pool.currency.symbol), + heading: true, }, { - label: ( - - - - + label: , + value: ( + + {formatBalance(pool.reserve.total || 0, pool.currency.symbol)} + + ), - value: formatBalance(pool.reserve.total || 0, pool.currency.symbol), + heading: false, }, - ...(!isTinlakePool + ...(!isTinlakePool && cashLoans.length ? [ { - label: ( - - - - - ), - value: formatBalance(offchainReserve, 'USD'), + label: , + value: , + heading: false, }, { label: 'Total assets', - value: loans.filter((loan) => hasValuationMethod(loan.pricing) && loan.pricing.valuationMethod !== 'cash') - .length, - }, - { label: , value: ongoingAssets.length || 0 }, - { - label: 'Overdue assets', - value: 0 ? 'statusCritical' : 'inherit'}>{overdueAssets.length}, + value: formatBalance(totalPresentValue, pool.currency.symbol), + heading: false, }, ] : []), @@ -123,7 +118,30 @@ export function PoolDetailAssets() { - + + Assets + + + + + + @@ -134,8 +152,8 @@ function CreateAssetButton({ poolId }: { poolId: string }) { const canCreateAssets = useSuitableAccounts({ poolId, poolRole: ['Borrower'], proxyType: ['Borrow'] }).length > 0 return canCreateAssets ? ( - - Create asset + }> + Create assets ) : null } diff --git a/fabric/src/theme/tokens/typography.ts b/fabric/src/theme/tokens/typography.ts index 73ab39c964..24389f3994 100644 --- a/fabric/src/theme/tokens/typography.ts +++ b/fabric/src/theme/tokens/typography.ts @@ -1,6 +1,12 @@ import { ThemeTypography } from '../types' const typography: ThemeTypography = { + heading: { + fontSize: 36, + lineHeight: 1.4, + fontWeight: 500, + color: 'textPrimary', + }, heading1: { fontSize: [20, 24], lineHeight: 1.4, diff --git a/fabric/src/theme/types.ts b/fabric/src/theme/types.ts index e8c36c0074..52d0023606 100644 --- a/fabric/src/theme/types.ts +++ b/fabric/src/theme/types.ts @@ -90,6 +90,7 @@ type ThemeSpace = SpaceValue[] & { // Typography export type TextVariantName = + | 'heading' | 'heading1' | 'heading2' | 'heading3' From 1b0a6af12fb9b19ccd7bee8854884b1395360257 Mon Sep 17 00:00:00 2001 From: katty barroso Date: Fri, 25 Oct 2024 14:12:48 +0200 Subject: [PATCH 02/20] Add updates to asset detail page --- .../src/components/AssetSummary.tsx | 32 ++- .../Charts/AssetPerformanceChart.tsx | 183 ++++++++--------- centrifuge-app/src/components/PageSection.tsx | 12 +- .../PoolOverview/TransactionHistory.tsx | 12 +- .../src/components/Report/BalanceSheet.tsx | 2 +- .../src/pages/Loan/HoldingsValues.tsx | 6 +- centrifuge-app/src/pages/Loan/KeyMetrics.tsx | 10 +- .../src/pages/Loan/MetricsTable.tsx | 12 +- .../src/pages/Loan/PricingValues.tsx | 12 +- .../src/pages/Loan/TransactionTable.tsx | 26 ++- centrifuge-app/src/pages/Loan/index.tsx | 185 +++++++++--------- 11 files changed, 231 insertions(+), 261 deletions(-) diff --git a/centrifuge-app/src/components/AssetSummary.tsx b/centrifuge-app/src/components/AssetSummary.tsx index 3b078fd42f..4a36e5fac8 100644 --- a/centrifuge-app/src/components/AssetSummary.tsx +++ b/centrifuge-app/src/components/AssetSummary.tsx @@ -1,8 +1,7 @@ import { Loan, TinlakeLoan } from '@centrifuge/centrifuge-js' -import { Box, Shelf, Stack, Text } from '@centrifuge/fabric' +import { Shelf, Stack, Text } from '@centrifuge/fabric' import * as React from 'react' import { useTheme } from 'styled-components' -import { LoanLabel } from './LoanLabel' type Props = { data?: { @@ -16,26 +15,21 @@ type Props = { export function AssetSummary({ data, children, loan }: Props) { const theme = useTheme() return ( - - - - Details - {loan && } - - - + + {data?.map(({ label, value }, index) => ( - - + + {label} - {value} + {value} ))} {children} diff --git a/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx b/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx index 85cf10f542..6bc29f5e73 100644 --- a/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx +++ b/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx @@ -1,11 +1,11 @@ import { CurrencyBalance, Pool } from '@centrifuge/centrifuge-js' -import { AnchorButton, Box, Card, IconDownload, Shelf, Spinner, Stack, Text } from '@centrifuge/fabric' +import { AnchorButton, Box, Card, IconDownload, Shelf, Spinner, Stack, Tabs, TabsItem, Text } from '@centrifuge/fabric' import * as React from 'react' import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts' -import styled, { useTheme } from 'styled-components' +import { useTheme } from 'styled-components' +import { getCSVDownloadUrl } from '../../../src/utils/getCSVDownloadUrl' import { formatDate } from '../../utils/date' import { formatBalance, formatBalanceAbbreviated } from '../../utils/formatting' -import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl' import { TinlakePool } from '../../utils/tinlake/useTinlakePools' import { useLoan } from '../../utils/useLoans' import { useAssetSnapshots } from '../../utils/usePools' @@ -25,50 +25,13 @@ interface Props { loanId: string } -const FilterButton = styled(Stack)` - &:hover { - cursor: pointer; - } -` - -const filterOptions = [ - { value: 'price', label: 'Price' }, - { value: 'value', label: 'Asset value' }, -] as const - function AssetPerformanceChart({ pool, poolId, loanId }: Props) { const theme = useTheme() const chartColor = theme.colors.accentPrimary const asset = useLoan(poolId, loanId) const assetSnapshots = useAssetSnapshots(poolId, loanId) - const [activeFilter, setActiveFilter] = React.useState<(typeof filterOptions)[number]>(filterOptions[0]) - - React.useEffect(() => { - if (assetSnapshots && assetSnapshots[0]?.currentPrice?.toString() === '0') { - setActiveFilter(filterOptions[1]) - } - }, [assetSnapshots]) - - const dataUrl: any = React.useMemo(() => { - if (!assetSnapshots || !assetSnapshots?.length) { - return undefined - } - - const formatted = assetSnapshots.map((assetObject: Record) => { - const keys = Object.keys(assetObject) - const newObj: Record = {} - - keys.forEach((assetKey) => { - newObj[assetKey] = - assetObject[assetKey] instanceof CurrencyBalance ? assetObject[assetKey].toFloat() : assetObject[assetKey] - }) - - return newObj - }) - - return getCSVDownloadUrl(formatted as any) - }, [assetSnapshots]) + const [selectedTabIndex, setSelectedTabIndex] = React.useState(0) const data: ChartData[] = React.useMemo(() => { if (!asset || !assetSnapshots) return [] @@ -157,58 +120,76 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) { [data, assetSnapshots] ) + const dataUrl: any = React.useMemo(() => { + if (!assetSnapshots || !assetSnapshots?.length) { + return undefined + } + + const formatted = assetSnapshots.map((assetObject: Record) => { + const keys = Object.keys(assetObject) + const newObj: Record = {} + + keys.forEach((assetKey) => { + newObj[assetKey] = + assetObject[assetKey] instanceof CurrencyBalance ? assetObject[assetKey].toFloat() : assetObject[assetKey] + }) + + return newObj + }) + + return getCSVDownloadUrl(formatted as any) + }, [assetSnapshots]) + if (!assetSnapshots) return return ( - + - - - {asset && 'valuationMethod' in asset.pricing && asset?.pricing.valuationMethod !== 'cash' - ? 'Asset performance' - : 'Cash balance'} - - {!isChartEmpty && ( - - Download - + + + + {asset && 'valuationMethod' in asset.pricing && asset?.pricing.valuationMethod !== 'cash' + ? 'Asset performance' + : 'Cash balance'} + + + ({pool.currency.name ?? 'USD'}) + + + {!(assetSnapshots && assetSnapshots[0]?.currentPrice?.toString() === '0') && ( + + + {data.length > 0 && ( + setSelectedTabIndex(index)}> + + Price + + + Asset value + + + )} + + )} - - - {isChartEmpty && No data yet} - - {!(assetSnapshots && assetSnapshots[0]?.currentPrice?.toString() === '0') && ( - - - {data.length > 0 && - filterOptions.map((filter, index) => ( - - setActiveFilter(filter)}> - - {filter.label} - - - - {index !== filterOptions.length - 1 && ( - - )} - - ))} - - + + Download + + + + {isChartEmpty && ( + + No data available + )} - + {data?.length ? ( @@ -225,7 +206,7 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) { tickFormatter={(tick: number) => { return new Date(tick).toLocaleString('en-US', { day: 'numeric', month: 'short' }) }} - style={{ fontSize: 8, fill: theme.colors.textSecondary, letterSpacing: '-0.7px' }} + style={{ fontSize: 8, fill: theme.colors.textPrimary, letterSpacing: '-0.7px' }} dy={4} interval={10} angle={-40} @@ -234,9 +215,9 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) { formatBalanceAbbreviated(tick, '', 2)} - domain={activeFilter.value === 'price' ? priceRange : [0, 'auto']} + domain={selectedTabIndex === 0 ? priceRange : [0, 'auto']} width={90} /> @@ -249,22 +230,22 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) { {payload.map(({ value }, index) => ( <> - {'Value'} - + {'Value'} + {payload[0].payload.historicPV - ? formatBalance(payload[0].payload.historicPV, 'USD', 2) + ? formatBalance(payload[0].payload.historicPV, pool.currency.name, 2) : payload[0].payload.futurePV - ? `~${formatBalance(payload[0].payload.futurePV, 'USD', 2)}` + ? `~${formatBalance(payload[0].payload.futurePV, pool.currency.name, 2)}` : '-'} - {'Price'} - + Price + {payload[0].payload.historicPrice - ? formatBalance(payload[0].payload.historicPrice, 'USD', 6) + ? formatBalance(payload[0].payload.historicPrice, pool.currency.name, 6) : payload[0].payload.futurePrice - ? `~${formatBalance(payload[0].payload.futurePrice, 'USD', 6)}` + ? `~${formatBalance(payload[0].payload.futurePrice, pool.currency.name, 6)}` : '-'} @@ -277,7 +258,7 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) { }} /> - {activeFilter.value === 'price' && ( + {selectedTabIndex === 0 && ( )} - {activeFilter.value === 'price' && ( + {selectedTabIndex === 0 && ( )} - {activeFilter.value === 'value' && ( + {selectedTabIndex === 1 && ( )} - {activeFilter.value === 'value' && ( + {selectedTabIndex === 1 && ( + {(title || titleAddition) && ( diff --git a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx index 38718364ef..610632ca31 100644 --- a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx +++ b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx @@ -225,6 +225,7 @@ export const TransactionHistoryTable = ({ ), sortKey: 'transactionDate', + width: '200px', }, { align: 'left', @@ -249,9 +250,10 @@ export const TransactionHistoryTable = ({ ) }, sortKey: 'transaction', + width: '60%', }, { - align: 'right', + align: 'left', header: , cell: ({ amount, netFlow }: Row) => ( @@ -259,9 +261,10 @@ export const TransactionHistoryTable = ({ ), sortKey: 'amount', + width: '250px', }, { - align: 'right', + align: 'center', header: 'View transaction', cell: ({ hash }: Row) => { return ( @@ -276,15 +279,14 @@ export const TransactionHistoryTable = ({ ) }, + width: '110px', }, ] return ( - - Transaction history - + Transaction history {transactions?.length! > 8 && preview && ( diff --git a/centrifuge-app/src/components/Report/BalanceSheet.tsx b/centrifuge-app/src/components/Report/BalanceSheet.tsx index 7f1169276f..ff93e44356 100644 --- a/centrifuge-app/src/components/Report/BalanceSheet.tsx +++ b/centrifuge-app/src/components/Report/BalanceSheet.tsx @@ -76,7 +76,7 @@ export function BalanceSheet({ pool }: { pool: Pool }) { ] .concat( poolStates.map((state, index) => ({ - align: 'right', + align: 'left', timestamp: state.timestamp, header: new Date(state.timestamp).toLocaleDateString('en-US', { day: 'numeric', diff --git a/centrifuge-app/src/pages/Loan/HoldingsValues.tsx b/centrifuge-app/src/pages/Loan/HoldingsValues.tsx index 8f9146f16a..d3ba229f84 100644 --- a/centrifuge-app/src/pages/Loan/HoldingsValues.tsx +++ b/centrifuge-app/src/pages/Loan/HoldingsValues.tsx @@ -74,11 +74,9 @@ export function HoldingsValues({ pool, transactions, currentFace, pricing }: Pro ] return ( - + - - Holdings - + Holdings diff --git a/centrifuge-app/src/pages/Loan/KeyMetrics.tsx b/centrifuge-app/src/pages/Loan/KeyMetrics.tsx index 3d5bb53459..cb748ce051 100644 --- a/centrifuge-app/src/pages/Loan/KeyMetrics.tsx +++ b/centrifuge-app/src/pages/Loan/KeyMetrics.tsx @@ -143,23 +143,21 @@ export function KeyMetrics({ pool, loan }: Props) { loan.pricing.valuationMethod === 'oracle' && loan.pricing.notional.gtn(0) && currentYTM - ? [{ label: , value: formatPercentage(currentYTM) }] + ? [{ label: , value: formatPercentage(currentYTM) }] : []), ...(loan.pricing.maturityDate && 'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'oracle' && loan.pricing.notional.gtn(0) && averageWeightedYTM - ? [{ label: , value: formatPercentage(averageWeightedYTM) }] + ? [{ label: , value: formatPercentage(averageWeightedYTM) }] : []), ] return ( - + - - Key metrics - + Key metrics diff --git a/centrifuge-app/src/pages/Loan/MetricsTable.tsx b/centrifuge-app/src/pages/Loan/MetricsTable.tsx index 431ebc3425..dedc335393 100644 --- a/centrifuge-app/src/pages/Loan/MetricsTable.tsx +++ b/centrifuge-app/src/pages/Loan/MetricsTable.tsx @@ -12,7 +12,7 @@ type Props = { export function MetricsTable({ metrics }: Props) { return ( - + {metrics.map(({ label, value }, index) => { const multirow = value && value.length > 20 const asLink = value && /^(https?:\/\/[^\s]+)$/.test(value) @@ -33,25 +33,21 @@ export function MetricsTable({ metrics }: Props) { } : {} - const combinedStyle: React.CSSProperties = { ...defaultStyle, ...multiRowStyle } + const combinedStyle: React.CSSProperties = { ...defaultStyle, ...multiRowStyle, textAlign: 'right' } return ( - + {label} - + {asLink ? {value} : value} diff --git a/centrifuge-app/src/pages/Loan/PricingValues.tsx b/centrifuge-app/src/pages/Loan/PricingValues.tsx index 4d095fa314..9ac7e84218 100644 --- a/centrifuge-app/src/pages/Loan/PricingValues.tsx +++ b/centrifuge-app/src/pages/Loan/PricingValues.tsx @@ -59,11 +59,9 @@ export function PricingValues({ loan, pool }: Props) { const accruedPrice = 'currentPrice' in loan && loan.currentPrice return ( - + - - Pricing - + Pricing , + label: , value: pricing.withLinearPricing ? 'Enabled' : 'Disabled', }, ...(loan.status === 'Active' && loan.outstandingDebt.toDecimal().lte(0) @@ -102,9 +100,7 @@ export function PricingValues({ loan, pool }: Props) { return ( - - Pricing - + Pricing { const assetTransactions = useMemo(() => { const sortedTransactions = transactions?.sort((a, b) => { @@ -120,6 +123,7 @@ export const TransactionTable = ({ return sum }, Dec(0)), realizedProfitFifo: transaction.realizedProfitFifo, + unrealizedProfitAtMarketPrice: transaction.unrealizedProfitAtMarketPrice, } }) }, [transactions, maturityDate, pricing, decimals]) @@ -189,10 +193,15 @@ export const TransactionTable = ({ }, { align: 'left', - header: `Realized P&L`, + header: loanStatus === 'Closed' || loanStatus === 'Repaid' ? 'Realized P&L' : 'Unrealized P&L', cell: (row: Row) => - row.realizedProfitFifo - ? `${row.type !== 'REPAID' ? '-' : ''}${formatBalance(row.realizedProfitFifo, undefined, 2, 2)}` + row.realizedProfitFifo || row.unrealizedProfitAtMarketPrice + ? formatBalance( + loanStatus === 'Closed' ? row.unrealizedProfitAtMarketPrice ?? 0 : row.realizedProfitFifo ?? 0, + undefined, + 2, + 2 + ) : '-', }, { @@ -214,10 +223,15 @@ export const TransactionTable = ({ }, { align: 'left', - header: `Realized P&L`, + header: loanStatus === 'Closed' || loanStatus === 'Repaid' ? 'Realized P&L' : 'Unrealized P&L', cell: (row: Row) => - row.realizedProfitFifo - ? `${row.type !== 'REPAID' ? '-' : ''}${formatBalance(row.realizedProfitFifo, undefined, 2, 2)}` + row.realizedProfitFifo || row.unrealizedProfitAtMarketPrice + ? formatBalance( + loanStatus === 'Closed' ? row.unrealizedProfitAtMarketPrice ?? 0 : row.realizedProfitFifo ?? 0, + undefined, + 2, + 2 + ) : '-', }, ]), diff --git a/centrifuge-app/src/pages/Loan/index.tsx b/centrifuge-app/src/pages/Loan/index.tsx index 78bb4547fa..373003aafa 100644 --- a/centrifuge-app/src/pages/Loan/index.tsx +++ b/centrifuge-app/src/pages/Loan/index.tsx @@ -5,24 +5,22 @@ import { Card, Drawer, Grid, - IconChevronLeft, + IconArrowLeft, Shelf, Spinner, Stack, Text, - TextWithPlaceholder, truncate, } from '@centrifuge/fabric' import * as React from 'react' import { useParams } from 'react-router' -import styled, { useTheme } from 'styled-components' -import { AssetSummary } from '../../components/AssetSummary' +import styled from 'styled-components' +import { AssetSummary } from '../../../src/components/AssetSummary' +import { LoanLabel, getLoanLabelStatus } from '../../../src/components/LoanLabel' import AssetPerformanceChart from '../../components/Charts/AssetPerformanceChart' import { LabelValueStack } from '../../components/LabelValueStack' import { LayoutSection } from '../../components/LayoutBase/LayoutSection' import { LoadBoundary } from '../../components/LoadBoundary' -import { LoanLabel } from '../../components/LoanLabel' -import { PageHeader } from '../../components/PageHeader' import { PageSection } from '../../components/PageSection' import { TransactionHistoryTable } from '../../components/PoolOverview/TransactionHistory' import { RouterLinkButton } from '../../components/RouterLinkButton' @@ -47,13 +45,6 @@ import { RepayForm } from './RepayForm' import { TransactionTable } from './TransactionTable' import { formatNftAttribute, isCashLoan, isExternalLoan } from './utils' -const FullHeightStack = styled(Stack)` - flex: 1; - display: flex; - flex-direction: column; - height: 100%; -` - export default function LoanPage() { return } @@ -61,6 +52,25 @@ function isTinlakeLoan(loan: LoanType | TinlakeLoan): loan is TinlakeLoan { return loan.poolId.startsWith('0x') } +const StyledRouterLinkButton = styled(RouterLinkButton)` + margin-left: 14px; + border-radius: 50%; + margin: 0px; + padding: 0px; + width: fit-content; + margin-left: 30px; + + > span { + width: 35px; + } + &:hover { + background-color: ${({ theme }) => theme.colors.backgroundTertiary}; + span { + color: ${({ theme }) => theme.colors.textPrimary}; + } + } +` + function ActionButtons({ loan }: { loan: LoanType }) { const canBorrow = useCanBorrowAsset(loan.poolId, loan.id) const [financeShown, setFinanceShown] = React.useState(false) @@ -68,7 +78,7 @@ function ActionButtons({ loan }: { loan: LoanType }) { const [correctionShown, setCorrectionShown] = React.useState(false) if (!loan || !canBorrow || isTinlakeLoan(loan) || !canBorrow || loan.status === 'Closed') return null return ( - <> + setFinanceShown(false)} innerPaddingTop={2}> @@ -92,27 +102,26 @@ function ActionButtons({ loan }: { loan: LoanType }) { {!(loan.pricing.maturityDate && new Date() > new Date(loan.pricing.maturityDate)) || !loan.pricing.maturityDate ? ( - ) : null} {loan.outstandingDebt.gtn(0) && ( - )} {loan.outstandingDebt.gtn(0) && ( - )} - + ) } function Loan() { - const theme = useTheme() const { pid: poolId, aid: loanId } = useParams<{ pid: string; aid: string }>() if (!poolId || !loanId) throw new Error('Loan no found') const basePath = useBasePath() @@ -124,6 +133,8 @@ function Loan() { const { data: nftMetadata, isLoading: nftMetadataIsLoading } = useMetadata(nft?.metadataUri, nftMetadataSchema) const metadataIsLoading = poolMetadataIsLoading || nftMetadataIsLoading const borrowerAssetTransactions = useBorrowerAssetTransactions(`${poolId}`, `${loanId}`) + const isOracle = loan && 'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'oracle' + const loanStatus = loan && getLoanLabelStatus(loan)[1] const currentFace = loan?.pricing && 'outstandingQuantity' in loan.pricing @@ -149,51 +160,53 @@ function Loan() { const originationDate = loan && 'originationDate' in loan ? new Date(loan?.originationDate).toISOString() : undefined + const getCurrentValue = () => { + if (loanId === '0') return pool.reserve.total + else return loan?.presentValue || 0 + } + + if (metadataIsLoading) return + return ( - - - - {poolMetadata?.pool?.name ?? 'Pool assets'} - + + + + + + {name} + + {loan && } + - - - {name} - - {loan && } - - } - subtitle={loan && !isTinlakeLoan(loan) && } - /> + + + {loan && !isTinlakeLoan(loan) && } + + {loanId === '0' && ( - <> - + - - - - + )} {loan && pool && ( - - + + }> - {'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'oracle' && ( + {isOracle && ( }> @@ -206,7 +219,7 @@ function Loan() { gridAutoRows="minContent" gap={[2, 2, 2]} > - {'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'oracle' && ( + {isOracle && ( }> )} - {'valuationMethod' in loan.pricing && loan.pricing.valuationMethod !== 'cash' && ( + {'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'cash' && ( }> @@ -228,11 +241,9 @@ function Loan() { if (!isPublic) return null return ( }> - + - - {section.name} - + {section.name} - - + ) : ( - - - - Transaction history - - - - - + + Transaction history + + ) ) : null} @@ -319,6 +320,6 @@ function Loan() { ) : null} - + ) } From f46769caa261049f1577d7c760d04b1ecc035440 Mon Sep 17 00:00:00 2001 From: katty barroso Date: Fri, 25 Oct 2024 14:13:08 +0200 Subject: [PATCH 03/20] Fabric and types updates --- centrifuge-js/src/modules/pools.ts | 1 + fabric/src/components/Card/index.ts | 6 +++--- fabric/src/components/Tooltip/index.tsx | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index 0ed530b3a9..064e0fe4cb 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -849,6 +849,7 @@ export type AssetTransaction = { interestAmount: CurrencyBalance | undefined hash: string realizedProfitFifo: CurrencyBalance | undefined + unrealizedProfitAtMarketPrice: CurrencyBalance | undefined asset: { id: string metadata: string diff --git a/fabric/src/components/Card/index.ts b/fabric/src/components/Card/index.ts index 52f34e9528..79e5894cdf 100644 --- a/fabric/src/components/Card/index.ts +++ b/fabric/src/components/Card/index.ts @@ -3,7 +3,7 @@ import styled from 'styled-components' import { Box, BoxProps } from '../Box' type Props = { - variant?: 'default' | 'interactive' | 'overlay' + variant?: 'default' | 'interactive' | 'overlay' | 'secondary' backgroundColor?: string } @@ -14,9 +14,9 @@ export const Card = styled(Box)(({ variant = 'default', backgroundColor } css({ bg: backgroundColor ?? 'white', borderRadius: 'card', - borderWidth: variant === 'default' && !backgroundColor ? 1 : 0, + borderWidth: variant === 'default' || (variant === 'secondary' && !backgroundColor) ? 1 : 0, borderStyle: 'solid', - borderColor: 'borderPrimary', + borderColor: variant === 'secondary' ? 'borderSecondary' : 'borderPrimary', boxShadow: variant === 'interactive' ? 'cardInteractive' : variant === 'overlay' ? 'cardOverlay' : undefined, transition: 'box-shadow 100ms ease', diff --git a/fabric/src/components/Tooltip/index.tsx b/fabric/src/components/Tooltip/index.tsx index 725f0e8455..f926323331 100644 --- a/fabric/src/components/Tooltip/index.tsx +++ b/fabric/src/components/Tooltip/index.tsx @@ -116,7 +116,7 @@ export function Tooltip({ pointer={pointer} > {!!title && ( - + {title} )} From 9971bef603146f8084b5eba2ad6e506b608f819e Mon Sep 17 00:00:00 2001 From: katty barroso Date: Mon, 28 Oct 2024 09:41:03 +0100 Subject: [PATCH 04/20] Redesign side drawer variations --- centrifuge-app/src/components/PageSection.tsx | 2 +- .../src/pages/Loan/ChargeFeesFields.tsx | 14 +- .../src/pages/Loan/CorrectionForm.tsx | 186 ++++++----- .../src/pages/Loan/ExternalFinanceForm.tsx | 263 +++++++++------ .../src/pages/Loan/ExternalRepayForm.tsx | 235 +++++++------ centrifuge-app/src/pages/Loan/FinanceForm.tsx | 262 ++++++++------- centrifuge-app/src/pages/Loan/RepayForm.tsx | 316 ++++++++++-------- centrifuge-app/src/pages/Loan/index.tsx | 4 +- .../src/components/InlineFeedback/index.tsx | 2 +- fabric/src/components/InputUnit/index.tsx | 4 +- fabric/src/theme/tokens/theme.ts | 2 +- 11 files changed, 722 insertions(+), 568 deletions(-) diff --git a/centrifuge-app/src/components/PageSection.tsx b/centrifuge-app/src/components/PageSection.tsx index 5384b9aa13..fcbbdaa726 100644 --- a/centrifuge-app/src/components/PageSection.tsx +++ b/centrifuge-app/src/components/PageSection.tsx @@ -43,7 +43,7 @@ export function PageSection({ }: Props) { const [open, setOpen] = React.useState(defaultOpen) return ( - + {(title || titleAddition) && ( diff --git a/centrifuge-app/src/pages/Loan/ChargeFeesFields.tsx b/centrifuge-app/src/pages/Loan/ChargeFeesFields.tsx index 2781b6733b..60cfc6c9ac 100644 --- a/centrifuge-app/src/pages/Loan/ChargeFeesFields.tsx +++ b/centrifuge-app/src/pages/Loan/ChargeFeesFields.tsx @@ -6,7 +6,7 @@ import { useCentrifugeApi, wrapProxyCallsForAccount, } from '@centrifuge/centrifuge-react' -import { Box, CurrencyInput, IconMinusCircle, IconPlusCircle, Select, Shelf, Stack, Text } from '@centrifuge/fabric' +import { Box, CurrencyInput, IconPlus, IconX, Select, Shelf, Stack, Text } from '@centrifuge/fabric' import { Field, FieldArray, FieldProps, useFormikContext } from 'formik' import React from 'react' import { combineLatest, map, of } from 'rxjs' @@ -101,11 +101,11 @@ export const ChargeFeesFields = ({ background="none" border="none" as="button" - mt={4} + mt="34px" style={{ cursor: 'pointer' }} onClick={() => remove(index)} > - + ) @@ -125,7 +125,7 @@ export const ChargeFeesFields = ({ return push({ id: '', amount: '' }) }} > - + Add fee @@ -148,8 +148,10 @@ function ChargePoolFeeSummary({ poolId }: { poolId: string }) { return form.values.fees.length > 0 ? ( - Fees - {formatBalance(Dec(totalFees), pool.currency.symbol, 2)} + + Fees + + {formatBalance(Dec(totalFees), pool.currency.symbol, 2)} ) : null diff --git a/centrifuge-app/src/pages/Loan/CorrectionForm.tsx b/centrifuge-app/src/pages/Loan/CorrectionForm.tsx index 659e191734..d2718c1f7e 100644 --- a/centrifuge-app/src/pages/Loan/CorrectionForm.tsx +++ b/centrifuge-app/src/pages/Loan/CorrectionForm.tsx @@ -1,10 +1,11 @@ import { ActiveLoan, CurrencyBalance, Pool, Price } from '@centrifuge/centrifuge-js' import { useCentrifugeApi, useCentrifugeTransaction, wrapProxyCallsForAccount } from '@centrifuge/centrifuge-react' -import { Button, CurrencyInput, Shelf, Stack, Text, TextInput } from '@centrifuge/fabric' +import { Box, Button, CurrencyInput, Shelf, Stack, Text, TextInput } from '@centrifuge/fabric' import Decimal from 'decimal.js-light' import { Field, FieldProps, Form, FormikProvider, useFormik } from 'formik' import * as React from 'react' import { combineLatest, switchMap } from 'rxjs' +import { useTheme } from 'styled-components' import { FieldWithErrorMessage } from '../../components/FieldWithErrorMessage' import { Dec } from '../../utils/Decimal' import { formatBalance } from '../../utils/formatting' @@ -25,6 +26,7 @@ export type CorrectionValues = { } export function CorrectionForm({ loan }: { loan: ActiveLoan }) { + const theme = useTheme() const pool = usePool(loan.poolId) as Pool const account = useBorrower(loan.poolId, loan.id) const poolFees = useChargePoolFees(loan.poolId, loan.id) @@ -116,105 +118,111 @@ export function CorrectionForm({ loan }: { loan: ActiveLoan }) { Correction - - {isExternalLoan(loan) ? ( - <> - - - {({ field, form, meta }: FieldProps) => { - return ( - form.setFieldValue('quantity', value)} - errorMessage={meta.touched ? meta.error : undefined} - /> - ) - }} - - - {({ field, form, meta }: FieldProps) => { - return ( - form.setFieldValue('price', value)} - decimals={8} - errorMessage={meta.touched ? meta.error : undefined} - /> - ) - }} - - - - - ={' '} - {formatBalance( - Dec(correctionForm.values.price || 0).mul(correctionForm.values.quantity || 0), - pool.currency.symbol, - 2 - )}{' '} - principal - - - - ) : isInternalLoan(loan) ? ( + + + {isExternalLoan(loan) ? ( + <> + + + {({ field, form, meta }: FieldProps) => { + return ( + form.setFieldValue('quantity', value)} + errorMessage={meta.touched ? meta.error : undefined} + /> + ) + }} + + + {({ field, form, meta }: FieldProps) => { + return ( + form.setFieldValue('price', value)} + decimals={8} + errorMessage={meta.touched ? meta.error : undefined} + /> + ) + }} + + + + + ={' '} + {formatBalance( + Dec(correctionForm.values.price || 0).mul(correctionForm.values.quantity || 0), + pool.currency.symbol, + 2 + )}{' '} + principal + + + + ) : isInternalLoan(loan) ? ( + + {({ field, form, meta }: FieldProps) => { + return ( + form.setFieldValue('principal', value)} + errorMessage={meta.touched ? meta.error : undefined} + /> + ) + }} + + ) : null} - {({ field, form, meta }: FieldProps) => { - return ( - form.setFieldValue('principal', value)} - errorMessage={meta.touched ? meta.error : undefined} - /> - ) - }} - - ) : null} - - - - {poolFees.render()} + validate={required()} + name="reason" + as={TextInput} + label="Reason" + placeholder="" + maxLength={40} + /> + {poolFees.render()} + + - + Summary - + Old holdings - {formatBalance(oldPrincipal, pool.currency.symbol, 2)} + {formatBalance(oldPrincipal, pool.currency.symbol, 2)} - + New holdings - + {formatBalance(newPrincipal, pool.currency.symbol, 2)} ( {isIncrease ? '+' : ''} diff --git a/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx b/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx index 041c7faeb6..4b3c599f00 100644 --- a/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx +++ b/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx @@ -8,12 +8,24 @@ import { WithdrawAddress, } from '@centrifuge/centrifuge-js' import { useCentrifugeApi, useCentrifugeTransaction, wrapProxyCallsForAccount } from '@centrifuge/centrifuge-react' -import { Button, CurrencyInput, InlineFeedback, Shelf, Stack, Text, Tooltip } from '@centrifuge/fabric' +import { + Box, + Button, + CurrencyInput, + IconCheckCircle, + IconClock, + InlineFeedback, + Shelf, + Stack, + Text, + Tooltip, +} from '@centrifuge/fabric' import { BN } from 'bn.js' import Decimal from 'decimal.js-light' import { Field, FieldProps, Form, FormikProvider, useFormik } from 'formik' import * as React from 'react' import { combineLatest, switchMap } from 'rxjs' +import styled, { useTheme } from 'styled-components' import { AnchorTextLink } from '../../components/TextLink' import { Dec } from '../../utils/Decimal' import { formatBalance } from '../../utils/formatting' @@ -25,6 +37,7 @@ import { combine, maxPriceVariance, positiveNumber, required } from '../../utils import { useChargePoolFees } from './ChargeFeesFields' import { ErrorMessage } from './ErrorMessage' import { useWithdraw } from './FinanceForm' +import { SourceSelect } from './SourceSelect' export type FinanceValues = { price: number | '' | Decimal @@ -33,10 +46,34 @@ export type FinanceValues = { fees: { id: string; amount: '' | number | Decimal }[] } +const StyledSuccessButton = styled(Button)` + span { + color: ${({ theme }) => theme.colors.textPrimary}; + background-color: ${({ theme }) => theme.colors.statusOkBg}; + border-color: ${({ theme }) => theme.colors.statusOk}; + border-width: 1px; + &:hover { + background-color: ${({ theme }) => theme.colors.statusOkBg}; + border-color: ${({ theme }) => theme.colors.statusOk}; + border-width: 1px; + box-shadow: none; + } + } +` + /** * Finance form for loans with `valuationMethod === oracle` */ -export function ExternalFinanceForm({ loan, source }: { loan: ExternalLoan; source: string }) { +export function ExternalFinanceForm({ + loan, + source, + setSource, +}: { + loan: ExternalLoan + source: string + setSource: (source: string) => void +}) { + const theme = useTheme() const pool = usePool(loan.poolId) as Pool const account = useBorrower(loan.poolId, loan.id) const poolFees = useChargePoolFees(loan.poolId, loan.id) @@ -44,6 +81,7 @@ export function ExternalFinanceForm({ loan, source }: { loan: ExternalLoan; sour const loans = useLoans(loan.poolId) const sourceLoan = loans?.find((l) => l.id === source) as CreatedLoan | ActiveLoan const displayCurrency = source === 'reserve' ? pool.currency.symbol : 'USD' + const [transactionSuccess, setTransactionSuccess] = React.useState(false) const { execute: doFinanceTransaction, isLoading: isFinanceLoading } = useCentrifugeTransaction( 'Purchase asset', (cent) => (args: [poolId: string, loanId: string, quantity: Price, price: CurrencyBalance], options) => { @@ -79,7 +117,7 @@ export function ExternalFinanceForm({ loan, source }: { loan: ExternalLoan; sour }, { onSuccess: () => { - financeForm.resetForm() + setTransactionSuccess(true) }, } ) @@ -125,62 +163,69 @@ export function ExternalFinanceForm({ loan, source }: { loan: ExternalLoan; sour { - - - - {({ field, form }: FieldProps) => { - return ( - form.setFieldValue('quantity', value)} - /> - ) - }} - - { - const financeAmount = Dec(val).mul(financeForm.values.quantity || 1) - return financeAmount.gt(maxAvailable) - ? `Amount exceeds available (${formatBalance(maxAvailable, displayCurrency, 2)})` - : '' - }, - maxPriceVariance(loan.pricing) - )} - > - {({ field, form }: FieldProps) => { - return ( - form.setFieldValue('price', value)} - decimals={8} - /> - ) - }} - - - - - ={' '} - {formatBalance( - Dec(financeForm.values.price || 0).mul(financeForm.values.quantity || 0), - displayCurrency, - 2 - )}{' '} - principal - - - - {source === 'reserve' && withdraw.render()} - - {poolFees.render()} + + + + + + {({ field, form }: FieldProps) => { + return ( + form.setFieldValue('quantity', value)} + /> + ) + }} + + { + const financeAmount = Dec(val).mul(financeForm.values.quantity || 1) + return financeAmount.gt(maxAvailable) + ? `Amount exceeds available (${formatBalance(maxAvailable, displayCurrency, 2)})` + : '' + }, + maxPriceVariance(loan.pricing) + )} + > + {({ field, form }: FieldProps) => { + return ( + form.setFieldValue('price', value)} + decimals={8} + /> + ) + }} + + + + + ={' '} + {formatBalance( + Dec(financeForm.values.price || 0).mul(financeForm.values.quantity || 0), + displayCurrency, + 2 + )}{' '} + principal + + + {source === 'reserve' && withdraw.render()} + {poolFees.render()} + + Principal amount ({formatBalance(totalFinance, displayCurrency, 2)}) is greater than the available balance @@ -202,61 +247,71 @@ export function ExternalFinanceForm({ loan, source }: { loan: ExternalLoan; sour Liquidity tab. - + Transaction summary - - - - Available balance - - + + + - {formatBalance(maxAvailable, displayCurrency, 2)} + + Available balance + - - - - - - Principal amount - - {formatBalance(totalFinance, displayCurrency, 2)} + {formatBalance(maxAvailable, displayCurrency, 2)} - - {poolFees.renderSummary()} - + + + + Principal amount + + {formatBalance(totalFinance, displayCurrency, 2)} + + + {poolFees.renderSummary()} + - {source === 'reserve' ? ( - - - Stablecoins will be transferred to the specified withdrawal addresses, on the specified networks. A - delay until the transfer is completed is to be expected. - - - ) : ( - - - Virtual accounting process. No onchain stablecoin transfers are expected. - - - )} + {source === 'reserve' ? ( + + + Stablecoins will be transferred to the designated withdrawal addresses on the specified networks. + A delay may occur before the transfer is completed. + + + ) : ( + + + Virtual accounting process. No onchain stablecoin transfers are expected. + + + )} + - + {transactionSuccess ? ( + }>Transaction successful + ) : ( + + )} diff --git a/centrifuge-app/src/pages/Loan/ExternalRepayForm.tsx b/centrifuge-app/src/pages/Loan/ExternalRepayForm.tsx index 18e8b025e8..b445e56df0 100644 --- a/centrifuge-app/src/pages/Loan/ExternalRepayForm.tsx +++ b/centrifuge-app/src/pages/Loan/ExternalRepayForm.tsx @@ -5,12 +5,13 @@ import { useCentrifugeUtils, wrapProxyCallsForAccount, } from '@centrifuge/centrifuge-react' -import { Button, CurrencyInput, InlineFeedback, Shelf, Stack, Text } from '@centrifuge/fabric' +import { Box, Button, CurrencyInput, InlineFeedback, Shelf, Stack, Text } from '@centrifuge/fabric' import { BN } from 'bn.js' import Decimal from 'decimal.js-light' import { Field, FieldProps, Form, FormikProvider, useFormik } from 'formik' import * as React from 'react' import { combineLatest, switchMap } from 'rxjs' +import { useTheme } from 'styled-components' import { copyable } from '../../components/Report/utils' import { Tooltips } from '../../components/Tooltips' import { Dec } from '../../utils/Decimal' @@ -22,6 +23,7 @@ import { usePool } from '../../utils/usePools' import { combine, maxNotRequired, nonNegativeNumberNotRequired } from '../../utils/validation' import { useChargePoolFees } from './ChargeFeesFields' import { ErrorMessage } from './ErrorMessage' +import { SourceSelect } from './SourceSelect' type RepayValues = { price: number | '' | Decimal @@ -36,7 +38,16 @@ const UNLIMITED = Dec(1000000000000000) /** * Repay form for loans with `valuationMethod === oracle */ -export function ExternalRepayForm({ loan, destination }: { loan: ExternalLoan; destination: string }) { +export function ExternalRepayForm({ + loan, + destination, + setDestination, +}: { + loan: ExternalLoan + destination: string + setDestination: (destination: string) => void +}) { + const theme = useTheme() const pool = usePool(loan.poolId) const account = useBorrower(loan.poolId, loan.id) const balances = useBalances(account?.actingAddress) @@ -187,94 +198,101 @@ export function ExternalRepayForm({ loan, destination }: { loan: ExternalLoan; d return ( - - + + + + + { + if (Dec(val || 0).gt(maxQuantity.toDecimal())) { + return `Quantity exeeds max (${maxQuantity.toString()})` + } + return '' + })} + name="quantity" + > + {({ field, form }: FieldProps) => { + return ( + form.setFieldValue('quantity', value)} + placeholder="0" + onSetMax={() => + form.setFieldValue('quantity', loan.pricing.outstandingQuantity.toDecimal().toNumber()) + } + /> + ) + }} + + + {({ field, form }: FieldProps) => { + return ( + form.setFieldValue('price', value)} + decimals={8} + currency={displayCurrency} + /> + ) + }} + + + + + = {formatBalance(principal, displayCurrency, 2)} principal + + + {'outstandingInterest' in loan && loan.outstandingInterest.toDecimal().gt(0) && ( + + {({ field, form }: FieldProps) => { + return ( + form.setFieldValue('interest', value)} + onSetMax={() => form.setFieldValue('interest', maxInterest.toNumber())} + /> + ) + }} + + )} { - if (Dec(val || 0).gt(maxQuantity.toDecimal())) { - return `Quantity exeeds max (${maxQuantity.toString()})` - } - return '' - })} - name="quantity" + name="amountAdditional" + validate={combine(nonNegativeNumberNotRequired(), maxNotRequired(maxAvailable.toNumber()))} > {({ field, form }: FieldProps) => { return ( form.setFieldValue('quantity', value)} - placeholder="0" - onSetMax={() => - form.setFieldValue('quantity', loan.pricing.outstandingQuantity.toDecimal().toNumber()) - } - /> - ) - }} - - - {({ field, form }: FieldProps) => { - return ( - form.setFieldValue('price', value)} - decimals={8} currency={displayCurrency} + onChange={(value) => form.setFieldValue('amountAdditional', value)} /> ) }} - - - - = {formatBalance(principal, displayCurrency, 2)} principal - - - - {'outstandingInterest' in loan && loan.outstandingInterest.toDecimal().gt(0) && ( - - {({ field, form }: FieldProps) => { - return ( - form.setFieldValue('interest', value)} - onSetMax={() => form.setFieldValue('interest', maxInterest.toNumber())} - /> - ) - }} - - )} - - {({ field, form }: FieldProps) => { - return ( - } - disabled={isRepayLoading} - currency={displayCurrency} - onChange={(value) => form.setFieldValue('amountAdditional', value)} - /> - ) - }} - - - {poolFees.render()} + {poolFees.render()} + + The balance of the asset originator account ({formatBalance(balance, displayCurrency, 2)}) is insufficient. @@ -292,41 +310,48 @@ export function ExternalRepayForm({ loan, destination }: { loan: ExternalLoan; d outstanding interest ({formatBalance(maxInterest, displayCurrency, 2)}). - - + + Transaction summary - - - - {maxAvailable === UNLIMITED ? 'No limit' : formatBalance(maxAvailable, displayCurrency, 2)} - - - - + - - Sale amount + + + {maxAvailable === UNLIMITED ? 'No limit' : formatBalance(maxAvailable, displayCurrency, 2)} - {formatBalance(totalRepay, displayCurrency, 2)} - - {poolFees.renderSummary()} + + + + Sale amount + + {formatBalance(totalRepay, displayCurrency, 2)} + + + + {poolFees.renderSummary()} + - {destination === 'reserve' ? ( - - Stablecoins will be transferred to the onchain reserve. - - ) : ( - - - Virtual accounting process. No onchain stablecoin transfers are expected. - - - )} + + {destination === 'reserve' ? ( + + + Stablecoins will be transferred to the onchain reserve. + + + ) : ( + + + Virtual accounting process. No onchain stablecoin transfers are expected. + + + )} + diff --git a/centrifuge-app/src/pages/Loan/FinanceForm.tsx b/centrifuge-app/src/pages/Loan/FinanceForm.tsx index 016793aec3..294c595180 100644 --- a/centrifuge-app/src/pages/Loan/FinanceForm.tsx +++ b/centrifuge-app/src/pages/Loan/FinanceForm.tsx @@ -41,6 +41,7 @@ import Decimal from 'decimal.js-light' import { Field, FieldProps, Form, FormikProvider, useField, useFormik, useFormikContext } from 'formik' import * as React from 'react' import { combineLatest, map, of, switchMap } from 'rxjs' +import { useTheme } from 'styled-components' import { AnchorTextLink } from '../../components/TextLink' import { parachainIcons, parachainNames } from '../../config' import { Dec, min } from '../../utils/Decimal' @@ -75,8 +76,7 @@ export function FinanceForm({ loan }: { loan: LoanType }) { return ( Purchase - - + ) } @@ -84,8 +84,7 @@ export function FinanceForm({ loan }: { loan: LoanType }) { return ( {isCashLoan(loan) ? 'Deposit' : 'Finance'} - - + ) } @@ -93,7 +92,16 @@ export function FinanceForm({ loan }: { loan: LoanType }) { /** * Finance form for loans with `valuationMethod: outstandingDebt, discountedCashflow, cash` */ -function InternalFinanceForm({ loan, source }: { loan: LoanType; source: string }) { +function InternalFinanceForm({ + loan, + source, + onChange, +}: { + loan: LoanType + source: string + onChange: (source: string) => void +}) { + const theme = useTheme() const pool = usePool(loan.poolId) as Pool const account = useBorrower(loan.poolId, loan.id) const api = useCentrifugeApi() @@ -189,104 +197,120 @@ function InternalFinanceForm({ loan, source }: { loan: LoanType; source: string <> {!maturityDatePassed && ( - - { - const principalValue = typeof val === 'number' ? Dec(val) : (val as Decimal) - if (maxAvailable !== UNLIMITED && principalValue.gt(maxAvailable)) { - return `Principal exceeds available financing` - } - return '' - })} - > - {({ field, form }: FieldProps) => { - return ( - form.setFieldValue('principal', value)} - onSetMax={ - maxAvailable !== UNLIMITED ? () => form.setFieldValue('principal', maxAvailable) : undefined - } - /> - ) - }} - - {source === 'other' && ( - - {({ field }: FieldProps) => { + + + + { + const principalValue = typeof val === 'number' ? Dec(val) : (val as Decimal) + if (maxAvailable !== UNLIMITED && principalValue.gt(maxAvailable)) { + return `Principal exceeds available financing` + } + return '' + })} + > + {({ field, form }: FieldProps) => { return ( - + ) + }} + + )} + {source === 'reserve' && withdraw.render()} - {poolFees.render()} + {poolFees.render()} - - {isCashLoan(loan) ? 'Deposit amount' : 'Financing amount'} ( - {formatBalance(totalFinance, displayCurrency, 2)}) is greater than the available balance ( - {formatBalance(maxAvailable, displayCurrency, 2)}). - - - - There is an additional{' '} - {formatBalance( - new CurrencyBalance(pool.reserve.total.sub(pool.reserve.available), pool.currency.decimals), - displayCurrency - )}{' '} - available from repayments or deposits. This requires first executing the orders on the{' '} - Liquidity tab. - - - - Transaction summary - + + {isCashLoan(loan) ? 'Deposit amount' : 'Financing amount'} ( + {formatBalance(totalFinance, displayCurrency, 2)}) is greater than the available balance ( + {formatBalance(maxAvailable, displayCurrency, 2)}). + + + + There is an additional{' '} + {formatBalance( + new CurrencyBalance(pool.reserve.total.sub(pool.reserve.available), pool.currency.decimals), + displayCurrency + )}{' '} + available from repayments or deposits. This requires first executing the orders on the{' '} + Liquidity tab. + + + + + + Transaction summary + + - - Available balance - - - - {maxAvailable === UNLIMITED ? 'No limit' : formatBalance(maxAvailable, displayCurrency, 2)} - + + + Available balance + + + + {maxAvailable === UNLIMITED ? 'No limit' : formatBalance(maxAvailable, displayCurrency, 2)} - + {isCashLoan(loan) ? 'Deposit amount' : 'Financing amount'} - {formatBalance(totalFinance, displayCurrency, 2)} + {formatBalance(totalFinance, displayCurrency, 2)} @@ -295,42 +319,42 @@ function InternalFinanceForm({ loan, source }: { loan: LoanType; source: string {source === 'reserve' ? ( - - Stablecoins will be transferred to the specified withdrawal addresses, on the specified networks. A - delay until the transfer is completed is to be expected. + + Stablecoins will be transferred to the designated withdrawal addresses on the specified networks. A + delay may occur before the transfer is completed. ) : source === 'other' ? ( - + Virtual accounting process. No onchain stablecoin transfers are expected. This action will lead to an increase in the NAV of the pool. ) : ( - + Virtual accounting process. No onchain stablecoin transfers are expected. )} - + + - - - + + )} @@ -372,16 +396,18 @@ function WithdrawSelect({ withdrawAddresses, poolId }: { withdrawAddresses: With ) return ( - helpers.setValue(JSON.parse(event.target.value))} + onBlur={field.onBlur} + errorMessage={(meta.touched || form.submitCount > 0) && meta.error ? meta.error : undefined} + value={field.value ? JSON.stringify(field.value) : ''} + options={options} + disabled={withdrawAddresses.length === 1} + /> + ) } diff --git a/centrifuge-app/src/pages/Loan/RepayForm.tsx b/centrifuge-app/src/pages/Loan/RepayForm.tsx index be07e098c0..03ccbbde16 100644 --- a/centrifuge-app/src/pages/Loan/RepayForm.tsx +++ b/centrifuge-app/src/pages/Loan/RepayForm.tsx @@ -14,11 +14,12 @@ import { useCentrifugeUtils, wrapProxyCallsForAccount, } from '@centrifuge/centrifuge-react' -import { Button, CurrencyInput, InlineFeedback, Select, Shelf, Stack, Text } from '@centrifuge/fabric' +import { Box, Button, CurrencyInput, InlineFeedback, Select, Shelf, Stack, Text } from '@centrifuge/fabric' import Decimal from 'decimal.js-light' import { Field, FieldProps, Form, FormikProvider, useFormik } from 'formik' import * as React from 'react' import { combineLatest, switchMap } from 'rxjs' +import { useTheme } from 'styled-components' import { copyable } from '../../components/Report/utils' import { Tooltips } from '../../components/Tooltips' import { Dec } from '../../utils/Decimal' @@ -56,8 +57,7 @@ export function RepayForm({ loan }: { loan: CreatedLoan | ActiveLoan }) { return ( Sell - - + ) } @@ -65,15 +65,23 @@ export function RepayForm({ loan }: { loan: CreatedLoan | ActiveLoan }) { return ( {isCashLoan(loan) ? 'Withdraw' : 'Repay'} - - + ) } /** * Repay form for loans with `valuationMethod: outstandingDebt, discountedCashflow, cash` */ -function InternalRepayForm({ loan, destination }: { loan: ActiveLoan | CreatedLoan; destination: string }) { +function InternalRepayForm({ + loan, + destination, + setDestination, +}: { + loan: ActiveLoan | CreatedLoan + destination: string + setDestination: (destination: string) => void +}) { + const theme = useTheme() const pool = usePool(loan.poolId) const account = useBorrower(loan.poolId, loan.id) const balances = useBalances(account?.actingAddress) @@ -225,154 +233,184 @@ function InternalRepayForm({ loan, destination }: { loan: ActiveLoan | CreatedLo <> - - {({ field, form }: FieldProps) => { - return ( - form.setFieldValue('principal', value)} - onSetMax={() => { - form.setFieldValue('principal', maxPrincipal.gte(0) ? maxPrincipal : 0) + + + + {({ field, form }: FieldProps) => { + return ( + form.setFieldValue('principal', value)} + onSetMax={() => { + form.setFieldValue('principal', maxPrincipal.gte(0) ? maxPrincipal : 0) + }} + secondaryLabel={`${formatBalance(maxPrincipal, displayCurrency)} outstanding`} + /> + ) + }} + + {'outstandingInterest' in loan && loan.outstandingInterest.toDecimal().gt(0) && !isCashLoan(loan) && ( + + {({ field, form }: FieldProps) => { + return ( + form.setFieldValue('interest', value)} + onSetMax={() => form.setFieldValue('interest', maxInterest.gte(0) ? maxInterest : 0)} + /> + ) }} - secondaryLabel={`${formatBalance(maxPrincipal, displayCurrency)} outstanding`} - /> - ) - }} - - {'outstandingInterest' in loan && loan.outstandingInterest.toDecimal().gt(0) && !isCashLoan(loan) && ( - )} - name="interest" - > - {({ field, form }: FieldProps) => { - return ( - form.setFieldValue('interest', value)} - onSetMax={() => form.setFieldValue('interest', maxInterest.gte(0) ? maxInterest : 0)} - /> - ) - }} - - )} - {!isCashLoan(loan) && ( - + {({ field, form }: FieldProps) => { + return ( + form.setFieldValue('amountAdditional', value)} + /> + ) + }} + )} - > - {({ field, form }: FieldProps) => { - return ( - } - disabled={isRepayLoading} - currency={displayCurrency} - onChange={(value) => form.setFieldValue('amountAdditional', value)} - /> - ) - }} - - )} - {destination === 'other' && ( - - {({ field }: FieldProps) => { - return ( - + ) + }} + + )} + {poolFees.render()} - - {isCashLoan(loan) ? 'Amount' : 'Principal'} ( - {formatBalance(Dec(repayForm.values.principal || 0), displayCurrency, 2)}) is greater than the outstanding{' '} - {isCashLoan(loan) ? 'balance' : 'principal'} ({formatBalance(maxPrincipal, displayCurrency, 2)}). - + + {isCashLoan(loan) ? 'Amount' : 'Principal'} ( + {formatBalance(Dec(repayForm.values.principal || 0), displayCurrency, 2)}) is greater than the + outstanding {isCashLoan(loan) ? 'balance' : 'principal'} ( + {formatBalance(maxPrincipal, displayCurrency, 2)}). + - - Interest ({formatBalance(Dec(repayForm.values.interest || 0), displayCurrency, 2)}) is greater than the - outstanding interest ({formatBalance(maxInterest, displayCurrency, 2)}). - + + Interest ({formatBalance(Dec(repayForm.values.interest || 0), displayCurrency, 2)}) is greater than the + outstanding interest ({formatBalance(maxInterest, displayCurrency, 2)}). + - - The balance of the asset originator account ({formatBalance(balance, displayCurrency, 2)}) is insufficient. - Transfer {formatBalance(totalRepay.sub(balance), displayCurrency, 2)} to{' '} - {copyable(utils.formatAddress(account?.actingAddress || ''))} on Centrifuge. - + + The balance of the asset originator account ({formatBalance(balance, displayCurrency, 2)}) is + insufficient. Transfer {formatBalance(totalRepay.sub(balance), displayCurrency, 2)} to{' '} + {copyable(utils.formatAddress(account?.actingAddress || ''))} on Centrifuge. + + + - + Transaction summary - - - - - {maxAvailable === UNLIMITED ? 'No limit' : formatBalance(maxAvailable, displayCurrency, 2)} - - - + - - {isCashLoan(loan) ? 'Withdrawal amount' : 'Repayment amount'} + + + {maxAvailable === UNLIMITED ? 'No limit' : formatBalance(maxAvailable, displayCurrency, 2)} - {formatBalance(totalRepay, displayCurrency, 2)} - - {poolFees.renderSummary()} - + + + + {isCashLoan(loan) ? 'Withdrawal amount' : 'Repayment amount'} + + {formatBalance(totalRepay, displayCurrency, 2)} + + + + {poolFees.renderSummary()} + - {destination === 'reserve' ? ( - - Stablecoins will be transferred to the onchain reserve. - - ) : destination === 'other' ? ( - - - Virtual accounting process. No onchain stablecoin transfers are expected. This action will lead to a - decrease in the NAV of the pool. - - - ) : ( - - - Virtual accounting process. No onchain stablecoin transfers are expected. - - - )} + + {destination === 'reserve' ? ( + + + Stablecoins will be transferred to the onchain reserve. + + + ) : destination === 'other' ? ( + + + Virtual accounting process. No onchain stablecoin transfers are expected. This action will lead to + a decrease in the NAV of the pool. + + + ) : ( + + + Virtual accounting process. No onchain stablecoin transfers are expected. + + + )} + + diff --git a/centrifuge-app/src/pages/Loan/index.tsx b/centrifuge-app/src/pages/Loan/index.tsx index 373003aafa..85db80f604 100644 --- a/centrifuge-app/src/pages/Loan/index.tsx +++ b/centrifuge-app/src/pages/Loan/index.tsx @@ -61,7 +61,7 @@ const StyledRouterLinkButton = styled(RouterLinkButton)` margin-left: 30px; > span { - width: 35px; + width: 34px; } &:hover { background-color: ${({ theme }) => theme.colors.backgroundTertiary}; @@ -169,7 +169,7 @@ function Loan() { return ( - + diff --git a/fabric/src/components/InlineFeedback/index.tsx b/fabric/src/components/InlineFeedback/index.tsx index d7d6f4d0ab..7dcda45a8f 100644 --- a/fabric/src/components/InlineFeedback/index.tsx +++ b/fabric/src/components/InlineFeedback/index.tsx @@ -26,7 +26,7 @@ export function InlineFeedback({ status = 'default', children }: InlineFeedbackP - + {children} diff --git a/fabric/src/components/InputUnit/index.tsx b/fabric/src/components/InputUnit/index.tsx index f5f25902f4..0b6d13bd4f 100644 --- a/fabric/src/components/InputUnit/index.tsx +++ b/fabric/src/components/InputUnit/index.tsx @@ -42,7 +42,7 @@ export function InputUnit({ id, label, secondaryLabel, errorMessage, inputElemen {inputElement} {secondaryLabel && ( - + {secondaryLabel} )} @@ -63,7 +63,7 @@ export function InputLabel({ }) { return ( Date: Mon, 28 Oct 2024 16:24:38 +0100 Subject: [PATCH 05/20] Update toast --- centrifuge-app/src/pages/Loan/CorrectionForm.tsx | 2 +- fabric/src/components/Toast/index.tsx | 10 +++++++--- fabric/src/theme/tokens/theme.ts | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/centrifuge-app/src/pages/Loan/CorrectionForm.tsx b/centrifuge-app/src/pages/Loan/CorrectionForm.tsx index d2718c1f7e..5c1b1e3624 100644 --- a/centrifuge-app/src/pages/Loan/CorrectionForm.tsx +++ b/centrifuge-app/src/pages/Loan/CorrectionForm.tsx @@ -211,7 +211,7 @@ export function CorrectionForm({ loan }: { loan: ActiveLoan }) { Summary - + Old holdings diff --git a/fabric/src/components/Toast/index.tsx b/fabric/src/components/Toast/index.tsx index 7cab3a3eed..a819531ca9 100644 --- a/fabric/src/components/Toast/index.tsx +++ b/fabric/src/components/Toast/index.tsx @@ -65,14 +65,18 @@ export function Toast({ status = 'info', label, sublabel, onDismiss, onStatusCha onStatusChange && onStatusChange(status) }, [status, onStatusChange]) return ( - + - {label} - {sublabel} + + {label} + + + {sublabel} + {action} diff --git a/fabric/src/theme/tokens/theme.ts b/fabric/src/theme/tokens/theme.ts index e1de10fa22..73f8be47db 100644 --- a/fabric/src/theme/tokens/theme.ts +++ b/fabric/src/theme/tokens/theme.ts @@ -2,7 +2,7 @@ import { black, blueScale, gold, grayScale, yellowScale } from './colors' const statusDefault = grayScale[800] const statusInfo = yellowScale[800] -const statusOk = '#519b10' +const statusOk = '#277917' const statusWarning = yellowScale[800] const statusCritical = '#d43f2b' const statusPromote = '#f81071' From 5ad4363d7649897790c76a63e87b169805f88bfb Mon Sep 17 00:00:00 2001 From: katty barroso Date: Tue, 29 Oct 2024 10:54:26 +0100 Subject: [PATCH 06/20] Fix TS warnings --- centrifuge-app/src/pages/Loan/index.tsx | 19 ++++++++++++++----- centrifuge-js/src/types/subquery.ts | 1 + 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/centrifuge-app/src/pages/Loan/index.tsx b/centrifuge-app/src/pages/Loan/index.tsx index 85db80f604..f4ea999b64 100644 --- a/centrifuge-app/src/pages/Loan/index.tsx +++ b/centrifuge-app/src/pages/Loan/index.tsx @@ -1,4 +1,12 @@ -import { ActiveLoan, Loan as LoanType, Pool, PricingInfo, TinlakeLoan } from '@centrifuge/centrifuge-js' +import { + ActiveLoan, + AssetTransaction, + ExternalPricingInfo, + Loan as LoanType, + Pool, + PricingInfo, + TinlakeLoan, +} from '@centrifuge/centrifuge-js' import { Box, Button, @@ -162,7 +170,8 @@ function Loan() { const getCurrentValue = () => { if (loanId === '0') return pool.reserve.total - else return loan?.presentValue || 0 + if (loan && 'presentValue' in loan) return loan.presentValue + return 0 } if (metadataIsLoading) return @@ -223,9 +232,9 @@ function Loan() { }> )} @@ -290,7 +299,7 @@ function Loan() { pricing={loan.pricing as PricingInfo} maturityDate={loan.pricing.maturityDate ? new Date(loan.pricing.maturityDate) : undefined} originationDate={originationDate ? new Date(originationDate) : undefined} - loanStatus={loanStatus} + loanStatus={loanStatus ?? ''} /> diff --git a/centrifuge-js/src/types/subquery.ts b/centrifuge-js/src/types/subquery.ts index 5f781c7f0c..520f3e28bd 100644 --- a/centrifuge-js/src/types/subquery.ts +++ b/centrifuge-js/src/types/subquery.ts @@ -129,6 +129,7 @@ export type SubqueryAssetTransaction = { type: AssetType sumRealizedProfitFifo: string unrealizedProfitAtMarketPrice: string + currentPrice: string } fromAsset?: { id: string From b705fda46792f033a00e4b6ce56663e8fed47f73 Mon Sep 17 00:00:00 2001 From: katty barroso Date: Tue, 29 Oct 2024 15:03:56 +0100 Subject: [PATCH 07/20] Add feedback review --- .../src/components/AssetSummary.tsx | 16 +++--- .../Charts/AssetPerformanceChart.tsx | 10 ++-- centrifuge-app/src/components/PageSection.tsx | 2 +- .../PoolOverview/TransactionHistory.tsx | 4 +- .../src/pages/Loan/CorrectionForm.tsx | 5 +- .../src/pages/Loan/ExternalFinanceForm.tsx | 11 +--- .../src/pages/Loan/ExternalRepayForm.tsx | 5 +- centrifuge-app/src/pages/Loan/FinanceForm.tsx | 11 +--- centrifuge-app/src/pages/Loan/KeyMetrics.tsx | 4 +- centrifuge-app/src/pages/Loan/RepayForm.tsx | 11 +--- centrifuge-app/src/pages/Loan/index.tsx | 56 +++++++++++++++++-- .../src/pages/Pool/Assets/OffchainMenu.tsx | 2 +- .../src/components/InlineFeedback/index.tsx | 2 +- 13 files changed, 84 insertions(+), 55 deletions(-) diff --git a/centrifuge-app/src/components/AssetSummary.tsx b/centrifuge-app/src/components/AssetSummary.tsx index 4a36e5fac8..a3cb456af4 100644 --- a/centrifuge-app/src/components/AssetSummary.tsx +++ b/centrifuge-app/src/components/AssetSummary.tsx @@ -1,4 +1,3 @@ -import { Loan, TinlakeLoan } from '@centrifuge/centrifuge-js' import { Shelf, Stack, Text } from '@centrifuge/fabric' import * as React from 'react' import { useTheme } from 'styled-components' @@ -7,12 +6,12 @@ type Props = { data?: { label: React.ReactNode value: React.ReactNode + heading: boolean }[] children?: React.ReactNode - loan?: Loan | TinlakeLoan } -export function AssetSummary({ data, children, loan }: Props) { +export function AssetSummary({ data, children }: Props) { const theme = useTheme() return ( - - {data?.map(({ label, value }, index) => ( + + {data?.map(({ label, value, heading }, index) => ( - + {label} - {value} + {value} ))} {children} diff --git a/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx b/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx index 6bc29f5e73..9d59026560 100644 --- a/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx +++ b/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx @@ -153,7 +153,7 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) { : 'Cash balance'} - ({pool.currency.name ?? 'USD'}) + ({pool.currency.symbol ?? 'USD'}) {!(assetSnapshots && assetSnapshots[0]?.currentPrice?.toString() === '0') && ( @@ -233,9 +233,9 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) { {'Value'} {payload[0].payload.historicPV - ? formatBalance(payload[0].payload.historicPV, pool.currency.name, 2) + ? formatBalance(payload[0].payload.historicPV, pool.currency.symbol, 2) : payload[0].payload.futurePV - ? `~${formatBalance(payload[0].payload.futurePV, pool.currency.name, 2)}` + ? `~${formatBalance(payload[0].payload.futurePV, pool.currency.symbol, 2)}` : '-'} @@ -243,9 +243,9 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) { Price {payload[0].payload.historicPrice - ? formatBalance(payload[0].payload.historicPrice, pool.currency.name, 6) + ? formatBalance(payload[0].payload.historicPrice, pool.currency.symbol, 6) : payload[0].payload.futurePrice - ? `~${formatBalance(payload[0].payload.futurePrice, pool.currency.name, 6)}` + ? `~${formatBalance(payload[0].payload.futurePrice, pool.currency.symbol, 6)}` : '-'} diff --git a/centrifuge-app/src/components/PageSection.tsx b/centrifuge-app/src/components/PageSection.tsx index fcbbdaa726..0b0082a8b4 100644 --- a/centrifuge-app/src/components/PageSection.tsx +++ b/centrifuge-app/src/components/PageSection.tsx @@ -43,7 +43,7 @@ export function PageSection({ }: Props) { const [open, setOpen] = React.useState(defaultOpen) return ( - + {(title || titleAddition) && ( diff --git a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx index 610632ca31..032fe17e49 100644 --- a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx +++ b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx @@ -254,13 +254,13 @@ export const TransactionHistoryTable = ({ }, { align: 'left', - header: , + header: , cell: ({ amount, netFlow }: Row) => ( {amount ? `${activeAssetId && netFlow === 'negative' ? '-' : ''}${formatBalance(amount, 'USD', 2, 2)}` : ''} ), - sortKey: 'amount', + sortKey: 'quantity', width: '250px', }, { diff --git a/centrifuge-app/src/pages/Loan/CorrectionForm.tsx b/centrifuge-app/src/pages/Loan/CorrectionForm.tsx index 5c1b1e3624..f7045e356b 100644 --- a/centrifuge-app/src/pages/Loan/CorrectionForm.tsx +++ b/centrifuge-app/src/pages/Loan/CorrectionForm.tsx @@ -119,7 +119,8 @@ export function CorrectionForm({ loan }: { loan: ActiveLoan }) { Correction - + Summary diff --git a/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx b/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx index 4b3c599f00..b9f1da851a 100644 --- a/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx +++ b/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx @@ -164,7 +164,8 @@ export function ExternalFinanceForm({ Liquidity tab. - + Transaction summary diff --git a/centrifuge-app/src/pages/Loan/ExternalRepayForm.tsx b/centrifuge-app/src/pages/Loan/ExternalRepayForm.tsx index b445e56df0..cae51709c2 100644 --- a/centrifuge-app/src/pages/Loan/ExternalRepayForm.tsx +++ b/centrifuge-app/src/pages/Loan/ExternalRepayForm.tsx @@ -199,7 +199,8 @@ export function ExternalRepayForm({ - + Transaction summary diff --git a/centrifuge-app/src/pages/Loan/FinanceForm.tsx b/centrifuge-app/src/pages/Loan/FinanceForm.tsx index 294c595180..78e8a8236e 100644 --- a/centrifuge-app/src/pages/Loan/FinanceForm.tsx +++ b/centrifuge-app/src/pages/Loan/FinanceForm.tsx @@ -198,7 +198,8 @@ function InternalFinanceForm({ {!maturityDatePassed && ( - + Transaction summary diff --git a/centrifuge-app/src/pages/Loan/KeyMetrics.tsx b/centrifuge-app/src/pages/Loan/KeyMetrics.tsx index cb748ce051..1f5e24548b 100644 --- a/centrifuge-app/src/pages/Loan/KeyMetrics.tsx +++ b/centrifuge-app/src/pages/Loan/KeyMetrics.tsx @@ -127,13 +127,13 @@ export function KeyMetrics({ pool, loan }: Props) { sumRealizedProfitFifo ? { label: 'Realized P&L', - value: formatBalance(sumRealizedProfitFifo, pool.currency.symbol), + value: formatBalance(sumRealizedProfitFifo, pool.currency.symbol, 2, 2), } : (null as never), unrealizedProfitAtMarketPrice ? { label: 'Unrealized P&L', - value: formatBalance(unrealizedProfitAtMarketPrice, pool.currency.symbol), + value: formatBalance(unrealizedProfitAtMarketPrice, pool.currency.symbol, 2, 2), } : (null as never), ].filter(Boolean) diff --git a/centrifuge-app/src/pages/Loan/RepayForm.tsx b/centrifuge-app/src/pages/Loan/RepayForm.tsx index 03ccbbde16..7b8bfae6d1 100644 --- a/centrifuge-app/src/pages/Loan/RepayForm.tsx +++ b/centrifuge-app/src/pages/Loan/RepayForm.tsx @@ -234,7 +234,8 @@ function InternalRepayForm({ - + Transaction summary diff --git a/centrifuge-app/src/pages/Loan/index.tsx b/centrifuge-app/src/pages/Loan/index.tsx index f4ea999b64..16f155ac4d 100644 --- a/centrifuge-app/src/pages/Loan/index.tsx +++ b/centrifuge-app/src/pages/Loan/index.tsx @@ -25,6 +25,7 @@ import { useParams } from 'react-router' import styled from 'styled-components' import { AssetSummary } from '../../../src/components/AssetSummary' import { LoanLabel, getLoanLabelStatus } from '../../../src/components/LoanLabel' +import { Dec } from '../../../src/utils/Decimal' import AssetPerformanceChart from '../../components/Charts/AssetPerformanceChart' import { LabelValueStack } from '../../components/LabelValueStack' import { LayoutSection } from '../../components/LayoutBase/LayoutSection' @@ -144,6 +145,16 @@ function Loan() { const isOracle = loan && 'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'oracle' const loanStatus = loan && getLoanLabelStatus(loan)[1] + const sumRealizedProfitFifo = borrowerAssetTransactions?.reduce( + (sum, tx) => sum.add(tx.realizedProfitFifo?.toDecimal() ?? Dec(0)), + Dec(0) + ) + + const unrealizedProfitAtMarketPrice = borrowerAssetTransactions?.reduce( + (sum, tx) => sum.add(tx.unrealizedProfitAtMarketPrice?.toDecimal() ?? Dec(0)), + Dec(0) + ) + const currentFace = loan?.pricing && 'outstandingQuantity' in loan.pricing ? loan.pricing.outstandingQuantity.toDecimal().mul(loan.pricing.notional.toDecimal()) @@ -174,6 +185,16 @@ function Loan() { return 0 } + const getCurrentPrice = () => { + if (loan && 'currentPrice' in loan) return loan.currentPrice + return 0 + } + + const getValueProfit = () => { + if (loanStatus === 'Closed' || loanStatus === 'Repaid') return sumRealizedProfitFifo ?? 0 + else return unrealizedProfitAtMarketPrice ?? 0 + } + if (metadataIsLoading) return return ( @@ -189,12 +210,35 @@ function Loan() { {loan && !isTinlakeLoan(loan) && } diff --git a/centrifuge-app/src/pages/Pool/Assets/OffchainMenu.tsx b/centrifuge-app/src/pages/Pool/Assets/OffchainMenu.tsx index b6eb85a8fa..8703fb9aa7 100644 --- a/centrifuge-app/src/pages/Pool/Assets/OffchainMenu.tsx +++ b/centrifuge-app/src/pages/Pool/Assets/OffchainMenu.tsx @@ -35,7 +35,7 @@ const StyledButton = styled(Box)` font-family: Inter, sans-serif; ` -const LoanOption: React.FC = ({ loan }) => { +const LoanOption = ({ loan }: LoanOptionProps) => { const navigate = useNavigate() const location = useLocation() const nft = useCentNFT(loan.asset.collectionId, loan.asset.nftId, false, loan.poolId.startsWith('0x')) diff --git a/fabric/src/components/InlineFeedback/index.tsx b/fabric/src/components/InlineFeedback/index.tsx index 7dcda45a8f..fc08b4a0cd 100644 --- a/fabric/src/components/InlineFeedback/index.tsx +++ b/fabric/src/components/InlineFeedback/index.tsx @@ -26,7 +26,7 @@ export function InlineFeedback({ status = 'default', children }: InlineFeedbackP - + {children} From 2fb1051745a4f73a596038b60748e1222678e91a Mon Sep 17 00:00:00 2001 From: katty barroso Date: Wed, 30 Oct 2024 11:53:37 +0100 Subject: [PATCH 08/20] Adjust asset performance chart & make table responsive --- .../src/components/Charts/AssetPerformanceChart.tsx | 2 +- .../src/components/PoolOverview/TransactionHistory.tsx | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx b/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx index 9d59026560..5502cbabeb 100644 --- a/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx +++ b/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx @@ -217,7 +217,7 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) { tickLine={false} style={{ fontSize: '10px', fill: theme.colors.textPrimary }} tickFormatter={(tick: number) => formatBalanceAbbreviated(tick, '', 2)} - domain={selectedTabIndex === 0 ? priceRange : [0, 'auto']} + domain={selectedTabIndex === 0 ? priceRange : ['auto', 'auto']} width={90} /> diff --git a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx index 032fe17e49..e3513e657c 100644 --- a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx +++ b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx @@ -1,5 +1,5 @@ import { AssetTransaction, CurrencyBalance } from '@centrifuge/centrifuge-js' -import { AnchorButton, IconDownload, IconExternalLink, Shelf, Stack, Text } from '@centrifuge/fabric' +import { AnchorButton, Box, IconDownload, IconExternalLink, Shelf, Stack, Text } from '@centrifuge/fabric' import BN from 'bn.js' import { formatDate } from '../../utils/date' import { formatBalance } from '../../utils/formatting' @@ -279,7 +279,7 @@ export const TransactionHistoryTable = ({ ) }, - width: '110px', + width: '100px', }, ] @@ -308,7 +308,9 @@ export const TransactionHistoryTable = ({ )} - + + + ) } From e1c0f47c0b62958cb97f778dd635511edbbbb558 Mon Sep 17 00:00:00 2001 From: katty barroso Date: Wed, 30 Oct 2024 12:39:04 +0100 Subject: [PATCH 09/20] Fix total assets --- .../src/pages/Pool/Assets/index.tsx | 40 ++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/centrifuge-app/src/pages/Pool/Assets/index.tsx b/centrifuge-app/src/pages/Pool/Assets/index.tsx index deacc444f1..b686449483 100644 --- a/centrifuge-app/src/pages/Pool/Assets/index.tsx +++ b/centrifuge-app/src/pages/Pool/Assets/index.tsx @@ -60,10 +60,32 @@ export function PoolDetailAssets() { ) } - function hasValuationMethod(pricing: any): pricing is { valuationMethod: string; presentValue: CurrencyBalance } { + const hasValuationMethod = (pricing: any): pricing is { valuationMethod: string; presentValue: CurrencyBalance } => { return pricing && typeof pricing.valuationMethod === 'string' } + const getAmount = (loan: Loan) => { + switch (loan.status) { + case 'Closed': + return loan.totalRepaid + + case 'Active': + return loan.presentValue ?? (loan.outstandingDebt.isZero() ? loan.totalRepaid : loan.outstandingDebt) + + case 'Created': + return 0 + + default: + return pool.reserve.total + } + } + + const totalAssets = loans.reduce((sum, loan) => { + const amount = new CurrencyBalance(getAmount(loan as Loan), pool.currency.decimals).toDecimal() + + return sum.add(amount) + }, Dec(0)) + const offchainAssets = !isTinlakePool ? loans.filter( (loan) => hasValuationMethod((loan as Loan).pricing) && (loan as Loan).pricing.valuationMethod === 'cash' @@ -83,15 +105,15 @@ export function PoolDetailAssets() { const pageSummaryData: { label: React.ReactNode; value: React.ReactNode; heading?: boolean }[] = [ { - label: 'Total NAV', - value: formatBalance(pool.nav.total.toDecimal(), pool.currency.symbol), + label: `Total NAV (${pool.currency.symbol})`, + value: formatBalance(pool.nav.total.toDecimal()), heading: true, }, { - label: , + label: , value: ( - {formatBalance(pool.reserve.total || 0, pool.currency.symbol)} + {formatBalance(pool.reserve.total || 0)} ), @@ -100,13 +122,13 @@ export function PoolDetailAssets() { ...(!isTinlakePool && cashLoans.length ? [ { - label: , - value: , + label: , + value: , heading: false, }, { - label: 'Total assets', - value: formatBalance(totalPresentValue, pool.currency.symbol), + label: `Total Assets (${pool.currency.symbol})`, + value: formatBalance(totalAssets), heading: false, }, ] From b6a0400af8afad01f98584d9df642b35e2fc6a10 Mon Sep 17 00:00:00 2001 From: katty barroso Date: Wed, 30 Oct 2024 16:42:50 +0100 Subject: [PATCH 10/20] Add feedback review --- .../Charts/AssetPerformanceChart.tsx | 36 ++- centrifuge-app/src/components/LoanList.tsx | 212 ++++++++++++------ .../PoolOverview/TransactionHistory.tsx | 3 +- .../src/pages/Loan/TransactionTable.tsx | 2 +- centrifuge-app/src/pages/Loan/index.tsx | 4 +- .../src/pages/Pool/Assets/index.tsx | 36 +-- 6 files changed, 181 insertions(+), 112 deletions(-) diff --git a/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx b/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx index 5502cbabeb..bf3e75c2b2 100644 --- a/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx +++ b/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx @@ -30,8 +30,8 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) { const chartColor = theme.colors.accentPrimary const asset = useLoan(poolId, loanId) const assetSnapshots = useAssetSnapshots(poolId, loanId) - - const [selectedTabIndex, setSelectedTabIndex] = React.useState(0) + const isNonCash = asset && 'valuationMethod' in asset.pricing && asset?.pricing.valuationMethod !== 'cash' + const [selectedTabIndex, setSelectedTabIndex] = React.useState(isNonCash ? 0 : 1) const data: ChartData[] = React.useMemo(() => { if (!asset || !assetSnapshots) return [] @@ -147,13 +147,9 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) { - - {asset && 'valuationMethod' in asset.pricing && asset?.pricing.valuationMethod !== 'cash' - ? 'Asset performance' - : 'Cash balance'} - + {isNonCash ? 'Asset performance' : 'Cash balance'} - ({pool.currency.symbol ?? 'USD'}) + ({isNonCash ? pool.currency.symbol ?? 'USD' : 'USD'}) {!(assetSnapshots && assetSnapshots[0]?.currentPrice?.toString() === '0') && ( @@ -233,9 +229,17 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) { {'Value'} {payload[0].payload.historicPV - ? formatBalance(payload[0].payload.historicPV, pool.currency.symbol, 2) + ? formatBalance( + payload[0].payload.historicPV, + isNonCash ? 'USD' : pool.currency.symbol, + 2 + ) : payload[0].payload.futurePV - ? `~${formatBalance(payload[0].payload.futurePV, pool.currency.symbol, 2)}` + ? `~${formatBalance( + payload[0].payload.futurePV, + isNonCash ? 'USD' : pool.currency.symbol, + 2 + )}` : '-'} @@ -243,9 +247,17 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) { Price {payload[0].payload.historicPrice - ? formatBalance(payload[0].payload.historicPrice, pool.currency.symbol, 6) + ? formatBalance( + payload[0].payload.historicPrice, + isNonCash ? 'USD' : pool.currency.symbol, + 6 + ) : payload[0].payload.futurePrice - ? `~${formatBalance(payload[0].payload.futurePrice, pool.currency.symbol, 6)}` + ? `~${formatBalance( + payload[0].payload.futurePrice, + isNonCash ? 'USD' : pool.currency.symbol, + 6 + )}` : '-'} diff --git a/centrifuge-app/src/components/LoanList.tsx b/centrifuge-app/src/components/LoanList.tsx index 9e384768d0..3eeb751b54 100644 --- a/centrifuge-app/src/components/LoanList.tsx +++ b/centrifuge-app/src/components/LoanList.tsx @@ -1,7 +1,10 @@ import { useBasePath } from '@centrifuge/centrifuge-app/src/utils/useBasePath' import { CurrencyBalance, Loan, TinlakeLoan } from '@centrifuge/centrifuge-js' import { + AnchorButton, Box, + Button, + IconDownload, Pagination, PaginationContainer, Shelf, @@ -12,14 +15,17 @@ import { } from '@centrifuge/fabric' import get from 'lodash/get' import * as React from 'react' -import { useParams } from 'react-router' +import { useNavigate, useParams } from 'react-router' +import { formatNftAttribute } from '../../src/pages/Loan/utils' +import { LoanTemplate, LoanTemplateAttribute } from '../../src/types' +import { getCSVDownloadUrl } from '../../src/utils/getCSVDownloadUrl' import { nftMetadataSchema } from '../schemas' import { formatDate } from '../utils/date' import { formatBalance, formatPercentage } from '../utils/formatting' import { useFilters } from '../utils/useFilters' import { useMetadata } from '../utils/useMetadata' import { useCentNFT } from '../utils/useNFTs' -import { useAllPoolAssetSnapshots, usePool } from '../utils/usePools' +import { useAllPoolAssetSnapshots, usePool, usePoolMetadata } from '../utils/usePools' import { Column, DataTable, SortableTableHeader } from './DataTable' import { LoadBoundary } from './LoadBoundary' import { prefetchRoute } from './Root' @@ -44,6 +50,7 @@ export function LoanList({ loans }: Props) { const { pid: poolId } = useParams<{ pid: string }>() if (!poolId) throw new Error('Pool not found') + const navigate = useNavigate() const pool = usePool(poolId) const isTinlakePool = poolId?.startsWith('0x') const basePath = useBasePath() @@ -51,6 +58,20 @@ export function LoanList({ loans }: Props) { const loansData = isTinlakePool ? loans : (loans ?? []).filter((loan) => 'valuationMethod' in loan.pricing && loan.pricing.valuationMethod !== 'cash') + const { data: poolMetadata } = usePoolMetadata(pool) + const templateIds = poolMetadata?.loanTemplates?.map((s) => s.id) ?? [] + const templateId = templateIds.at(-1) + const { data: templateMetadata } = useMetadata(templateId) + + const additionalColumns: Column[] = + templateMetadata?.keyAttributes?.map((key) => { + const attr = templateMetadata.attributes![key] + return { + align: 'left', + header: attr.label, + cell: (l: Row) => , + } + }) || [] const snapshotsValues = snapshots?.reduce((acc: { [key: string]: any }, snapshot) => { @@ -120,20 +141,23 @@ export function LoanList({ loans }: Props) { cell: (l: Row) => , sortKey: 'idSortKey', }, - - { - align: 'left', - header: , - cell: (l: Row) => { - if (l.poolId.startsWith('0x') && l.id !== '0') { - return formatDate((l as TinlakeLoan).originationDate) - } - return l.status === 'Active' && 'valuationMethod' in l.pricing && l.pricing.valuationMethod !== 'cash' - ? formatDate(l.originationDate) - : '-' - }, - sortKey: 'originationDateSortKey', - }, + ...(additionalColumns?.length + ? additionalColumns.filter((attr) => attr.header !== 'Term') + : [ + { + align: 'left', + header: , + cell: (l: Row) => { + if (l.poolId.startsWith('0x') && l.id !== '0') { + return formatDate((l as TinlakeLoan).originationDate) + } + return l.status === 'Active' && 'valuationMethod' in l.pricing && l.pricing.valuationMethod !== 'cash' + ? formatDate(l.originationDate) + : '-' + }, + sortKey: 'originationDateSortKey', + }, + ]), ...(hasMaturityDate ? [ { @@ -156,7 +180,7 @@ export function LoanList({ loans }: Props) { : [ { align: 'left', - header: , + header: , cell: (l: Row) => , sortKey: 'outstandingDebtSortKey', }, @@ -189,6 +213,7 @@ export function LoanList({ loans }: Props) { header: , cell: (l: Row) => formatBalance(l.unrealizedPL ?? '', pool.currency, 2, 0), sortKey: 'unrealizedPLSortKey', + width: '140px', }, ]), ...(isTinlakePool @@ -199,6 +224,7 @@ export function LoanList({ loans }: Props) { header: , cell: (l: Row) => formatBalance(l.realizedPL ?? '', pool.currency, 2, 0), sortKey: 'realizedPLSortKey', + width: '140px', }, ]), ...(isTinlakePool @@ -209,35 +235,96 @@ export function LoanList({ loans }: Props) { header: , cell: (l: Row) => formatPercentage(l.portfolioPercentage ?? 0, true, undefined, 1), sortKey: 'portfolioSortKey', - width: '80px', }, ]), ].filter(Boolean) as Column[] const pagination = usePagination({ data: rows, pageSize: 20 }) + const csvData = React.useMemo(() => { + if (!rows.length) return undefined + + return rows.map((loan) => ({ + 'Asset ID': loan.id, + 'Maturity Date': loan.maturityDate ? loan.maturityDate : '-', + Quantity: `${getAmount(loan) ?? '-'}`, + 'Market Price': loan.marketPrice ? loan.marketPrice : '-', + 'Market Value': loan.marketValue ? loan.marketValue : '-', + 'Unrealized P&L': loan.unrealizedPL ? loan.unrealizedPL : '-', + 'Realized P&L': loan.realizedPL ? loan.realizedPL : '-', + 'Portfolio %': loan.portfolioPercentage ? loan.portfolioPercentage : '-', + })) + }, [rows, pool.currency]) + + const csvUrl = React.useMemo(() => csvData && getCSVDownloadUrl(csvData as any), [csvData]) + + return ( + <> + + Assets + + + + Download + + + + + + + + `${basePath}/${poolId}/assets/${row.id}`} + pageSize={20} + page={pagination.page} + defaultSortKey="maturityDate" + /> + + + {pagination.pageCount > 1 && ( + + + + )} + + + + ) +} + +function AssetMetadataField({ loan, name, attribute }: { loan: Row; name: string; attribute: LoanTemplateAttribute }) { + const isTinlakePool = loan.poolId.startsWith('0x') + const nft = useCentNFT(loan.asset.collectionId, loan.asset.nftId, false, isTinlakePool) + const { data: metadata, isLoading } = useMetadata(nft?.metadataUri, nftMetadataSchema) + return ( - - - - - `${basePath}/${poolId}/assets/${row.id}`} - pageSize={20} - page={pagination.page} - defaultSortKey="maturityDate" - /> - - - {pagination.pageCount > 1 && ( - - - - )} - - + + + {metadata?.properties?.[name] ? formatNftAttribute(metadata?.properties?.[name], attribute) : '-'} + + ) } @@ -253,7 +340,7 @@ export function AssetName({ loan }: { loan: Pick {loan.asset.nftId.length >= 9 @@ -269,7 +356,7 @@ export function AssetName({ loan }: { loan: Pick {metadata?.name} @@ -278,33 +365,32 @@ export function AssetName({ loan }: { loan: Pick{getAmount(loan)} +function Amount({ loan }: { loan: Row }) { + return {getAmount(loan, true)} } diff --git a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx index e3513e657c..779cb85df0 100644 --- a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx +++ b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx @@ -250,7 +250,7 @@ export const TransactionHistoryTable = ({ ) }, sortKey: 'transaction', - width: '60%', + width: '50%', }, { align: 'left', @@ -279,7 +279,6 @@ export const TransactionHistoryTable = ({ ) }, - width: '100px', }, ] diff --git a/centrifuge-app/src/pages/Loan/TransactionTable.tsx b/centrifuge-app/src/pages/Loan/TransactionTable.tsx index 4129234316..5c92a876db 100644 --- a/centrifuge-app/src/pages/Loan/TransactionTable.tsx +++ b/centrifuge-app/src/pages/Loan/TransactionTable.tsx @@ -213,7 +213,7 @@ export const TransactionTable = ({ : [ { align: 'left', - header: `Amount (${currency})`, + header: `Quantity (${currency})`, cell: (row: Row) => (row.amount ? `${formatBalance(row.amount, undefined, 2, 2)}` : '-'), }, { diff --git a/centrifuge-app/src/pages/Loan/index.tsx b/centrifuge-app/src/pages/Loan/index.tsx index 16f155ac4d..2409f12ac5 100644 --- a/centrifuge-app/src/pages/Loan/index.tsx +++ b/centrifuge-app/src/pages/Loan/index.tsx @@ -233,7 +233,7 @@ function Loan() { ] : [ { - label: `Current value (${pool.currency.symbol ?? 'USD'})`, + label: `Current value (USD)`, value: `${formatBalance(getCurrentValue(), undefined, 2, 2)}`, heading: true, }, @@ -283,7 +283,7 @@ function Loan() { )} - {'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'cash' && ( + {'valuationMethod' in loan.pricing && loan.pricing.valuationMethod !== 'cash' && ( }> diff --git a/centrifuge-app/src/pages/Pool/Assets/index.tsx b/centrifuge-app/src/pages/Pool/Assets/index.tsx index b686449483..fa2e856a17 100644 --- a/centrifuge-app/src/pages/Pool/Assets/index.tsx +++ b/centrifuge-app/src/pages/Pool/Assets/index.tsx @@ -1,5 +1,5 @@ import { CurrencyBalance, Loan } from '@centrifuge/centrifuge-js' -import { Box, Button, IconChevronRight, IconDownload, IconPlus, Shelf, Text } from '@centrifuge/fabric' +import { Box, IconChevronRight, IconPlus, Shelf, Text } from '@centrifuge/fabric' import * as React from 'react' import { useNavigate, useParams } from 'react-router' import styled from 'styled-components' @@ -49,6 +49,8 @@ export function PoolDetailAssets() { (loan) => 'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'cash' ) + console.log(pool) + if (!pool) return null if (!loans?.length) { @@ -96,13 +98,6 @@ export function PoolDetailAssets() { Dec(0) ) - const totalPresentValue = loans.reduce((sum, loan) => { - if (hasValuationMethod(loan.pricing) && loan.pricing.valuationMethod !== 'cash') { - return sum.add(loan.pricing.presentValue?.toDecimal() || Dec(0)) - } - return sum - }, Dec(0)) - const pageSummaryData: { label: React.ReactNode; value: React.ReactNode; heading?: boolean }[] = [ { label: `Total NAV (${pool.currency.symbol})`, @@ -140,30 +135,7 @@ export function PoolDetailAssets() { - - Assets - - - - - - + From 45c5c195402528f3501e8c6f7795c0611b5931b7 Mon Sep 17 00:00:00 2001 From: katty barroso Date: Wed, 30 Oct 2024 16:56:18 +0100 Subject: [PATCH 11/20] Add feedback qa --- .../Charts/AssetPerformanceChart.tsx | 10 +++---- centrifuge-app/src/components/LoanList.tsx | 8 +++--- .../src/pages/Pool/Assets/index.tsx | 27 +++---------------- 3 files changed, 13 insertions(+), 32 deletions(-) diff --git a/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx b/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx index bf3e75c2b2..19e0a70428 100644 --- a/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx +++ b/centrifuge-app/src/components/Charts/AssetPerformanceChart.tsx @@ -226,18 +226,18 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) { {payload.map(({ value }, index) => ( <> - {'Value'} + Value {payload[0].payload.historicPV ? formatBalance( payload[0].payload.historicPV, - isNonCash ? 'USD' : pool.currency.symbol, + isNonCash ? pool.currency.symbol : 'USD', 2 ) : payload[0].payload.futurePV ? `~${formatBalance( payload[0].payload.futurePV, - isNonCash ? 'USD' : pool.currency.symbol, + isNonCash ? pool.currency.symbol : 'USD', 2 )}` : '-'} @@ -249,13 +249,13 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) { {payload[0].payload.historicPrice ? formatBalance( payload[0].payload.historicPrice, - isNonCash ? 'USD' : pool.currency.symbol, + isNonCash ? pool.currency.symbol : 'USD', 6 ) : payload[0].payload.futurePrice ? `~${formatBalance( payload[0].payload.futurePrice, - isNonCash ? 'USD' : pool.currency.symbol, + isNonCash ? pool.currency.symbol : 'USD', 6 )}` : '-'} diff --git a/centrifuge-app/src/components/LoanList.tsx b/centrifuge-app/src/components/LoanList.tsx index 3eeb751b54..51834894bf 100644 --- a/centrifuge-app/src/components/LoanList.tsx +++ b/centrifuge-app/src/components/LoanList.tsx @@ -68,8 +68,9 @@ export function LoanList({ loans }: Props) { const attr = templateMetadata.attributes![key] return { align: 'left', - header: attr.label, + header: , cell: (l: Row) => , + sortKey: attr.label, } }) || [] @@ -232,7 +233,7 @@ export function LoanList({ loans }: Props) { : [ { align: 'left', - header: , + header: , cell: (l: Row) => formatPercentage(l.portfolioPercentage ?? 0, true, undefined, 1), sortKey: 'portfolioSortKey', }, @@ -364,8 +365,7 @@ export function AssetName({ loan }: { loan: Pick ) } - -function getAmount(l: Row, format?: boolean) { +export function getAmount(l: Row, format?: boolean) { const pool = usePool(l.poolId) switch (l.status) { case 'Closed': diff --git a/centrifuge-app/src/pages/Pool/Assets/index.tsx b/centrifuge-app/src/pages/Pool/Assets/index.tsx index fa2e856a17..5336140979 100644 --- a/centrifuge-app/src/pages/Pool/Assets/index.tsx +++ b/centrifuge-app/src/pages/Pool/Assets/index.tsx @@ -1,12 +1,12 @@ import { CurrencyBalance, Loan } from '@centrifuge/centrifuge-js' import { Box, IconChevronRight, IconPlus, Shelf, Text } from '@centrifuge/fabric' import * as React from 'react' -import { useNavigate, useParams } from 'react-router' +import { useParams } from 'react-router' import styled from 'styled-components' import { RouterTextLink } from '../../../../src/components/TextLink' import { useBasePath } from '../../../../src/utils/useBasePath' import { LoadBoundary } from '../../../components/LoadBoundary' -import { LoanList } from '../../../components/LoanList' +import { LoanList, getAmount } from '../../../components/LoanList' import { PageSummary } from '../../../components/PageSummary' import { RouterLinkButton } from '../../../components/RouterLinkButton' import { Tooltips } from '../../../components/Tooltips' @@ -37,7 +37,6 @@ export function PoolDetailAssetsTab() { export function PoolDetailAssets() { const { pid: poolId } = useParams<{ pid: string }>() - const navigate = useNavigate() if (!poolId) throw new Error('Pool not found') @@ -49,8 +48,6 @@ export function PoolDetailAssets() { (loan) => 'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'cash' ) - console.log(pool) - if (!pool) return null if (!loans?.length) { @@ -66,24 +63,8 @@ export function PoolDetailAssets() { return pricing && typeof pricing.valuationMethod === 'string' } - const getAmount = (loan: Loan) => { - switch (loan.status) { - case 'Closed': - return loan.totalRepaid - - case 'Active': - return loan.presentValue ?? (loan.outstandingDebt.isZero() ? loan.totalRepaid : loan.outstandingDebt) - - case 'Created': - return 0 - - default: - return pool.reserve.total - } - } - const totalAssets = loans.reduce((sum, loan) => { - const amount = new CurrencyBalance(getAmount(loan as Loan), pool.currency.decimals).toDecimal() + const amount = new CurrencyBalance(getAmount(loan as any), pool.currency.decimals).toDecimal() return sum.add(amount) }, Dec(0)) @@ -117,7 +98,7 @@ export function PoolDetailAssets() { ...(!isTinlakePool && cashLoans.length ? [ { - label: , + label: , value: , heading: false, }, From 484b28ddab262974628bdc294ac054f479ada90c Mon Sep 17 00:00:00 2001 From: katty barroso Date: Wed, 30 Oct 2024 19:24:43 +0100 Subject: [PATCH 12/20] Fix ts warnings --- .../InvestRedeemCentrifugeProvider.tsx | 2 +- .../InvestRedeem/InvestRedeemDrawer.tsx | 2 +- centrifuge-app/src/components/LoanList.tsx | 37 +++++++++++-------- .../src/components/PoolCard/index.tsx | 28 +++++++------- centrifuge-app/src/components/PoolList.tsx | 3 -- .../PoolOverview/TrancheTokenCards.tsx | 29 +++++++-------- .../src/components/Portfolio/Transactions.tsx | 2 - .../src/pages/Pool/Assets/index.tsx | 2 +- 8 files changed, 52 insertions(+), 53 deletions(-) diff --git a/centrifuge-app/src/components/InvestRedeem/InvestRedeemCentrifugeProvider.tsx b/centrifuge-app/src/components/InvestRedeem/InvestRedeemCentrifugeProvider.tsx index f4228c8f34..237ffeb66c 100644 --- a/centrifuge-app/src/components/InvestRedeem/InvestRedeemCentrifugeProvider.tsx +++ b/centrifuge-app/src/components/InvestRedeem/InvestRedeemCentrifugeProvider.tsx @@ -97,7 +97,7 @@ export function InvestRedeemCentrifugeProvider({ poolId, trancheId, children }: }, 300) return () => clearTimeout(timer) - }, [isDataLoading]) + }, [isDataLoading, connectedType]) const state: InvestRedeemState = { poolId, diff --git a/centrifuge-app/src/components/InvestRedeem/InvestRedeemDrawer.tsx b/centrifuge-app/src/components/InvestRedeem/InvestRedeemDrawer.tsx index 2e94a167f0..545298dc85 100644 --- a/centrifuge-app/src/components/InvestRedeem/InvestRedeemDrawer.tsx +++ b/centrifuge-app/src/components/InvestRedeem/InvestRedeemDrawer.tsx @@ -74,7 +74,7 @@ export function InvestRedeemDrawer({ ) return { sumRealizedProfitFifoByPeriod, sumUnrealizedProfitAtMarketPrice } - }, [dailyPoolStates]) + }, [dailyPoolStates, pool.currency.decimals]) return ( diff --git a/centrifuge-app/src/components/LoanList.tsx b/centrifuge-app/src/components/LoanList.tsx index 51834894bf..9b028a1c58 100644 --- a/centrifuge-app/src/components/LoanList.tsx +++ b/centrifuge-app/src/components/LoanList.tsx @@ -1,5 +1,5 @@ import { useBasePath } from '@centrifuge/centrifuge-app/src/utils/useBasePath' -import { CurrencyBalance, Loan, TinlakeLoan } from '@centrifuge/centrifuge-js' +import { CurrencyBalance, Loan, Pool, TinlakeLoan } from '@centrifuge/centrifuge-js' import { AnchorButton, Box, @@ -16,6 +16,7 @@ import { import get from 'lodash/get' import * as React from 'react' import { useNavigate, useParams } from 'react-router' +import { TinlakePool } from 'src/utils/tinlake/useTinlakePools' import { formatNftAttribute } from '../../src/pages/Loan/utils' import { LoanTemplate, LoanTemplateAttribute } from '../../src/types' import { getCSVDownloadUrl } from '../../src/utils/getCSVDownloadUrl' @@ -97,7 +98,7 @@ export function LoanList({ loans }: Props) { return aId.localeCompare(bId) }) - }, [isTinlakePool, loansData]) + }, [loansData]) const filters = useFilters({ data: loansWithLabelStatus as Loan[], @@ -245,17 +246,21 @@ export function LoanList({ loans }: Props) { const csvData = React.useMemo(() => { if (!rows.length) return undefined - return rows.map((loan) => ({ - 'Asset ID': loan.id, - 'Maturity Date': loan.maturityDate ? loan.maturityDate : '-', - Quantity: `${getAmount(loan) ?? '-'}`, - 'Market Price': loan.marketPrice ? loan.marketPrice : '-', - 'Market Value': loan.marketValue ? loan.marketValue : '-', - 'Unrealized P&L': loan.unrealizedPL ? loan.unrealizedPL : '-', - 'Realized P&L': loan.realizedPL ? loan.realizedPL : '-', - 'Portfolio %': loan.portfolioPercentage ? loan.portfolioPercentage : '-', - })) - }, [rows, pool.currency]) + return rows.map((loan) => { + const quantity = getAmount(loan, pool) + + return { + 'Asset ID': loan.id, + 'Maturity Date': loan.maturityDate ? loan.maturityDate : '-', + Quantity: `${quantity ?? '-'}`, + 'Market Price': loan.marketPrice ? loan.marketPrice : '-', + 'Market Value': loan.marketValue ? loan.marketValue : '-', + 'Unrealized P&L': loan.unrealizedPL ? loan.unrealizedPL : '-', + 'Realized P&L': loan.realizedPL ? loan.realizedPL : '-', + 'Portfolio %': loan.portfolioPercentage ? loan.portfolioPercentage : '-', + } + }) + }, [rows]) const csvUrl = React.useMemo(() => csvData && getCSVDownloadUrl(csvData as any), [csvData]) @@ -365,8 +370,7 @@ export function AssetName({ loan }: { loan: Pick ) } -export function getAmount(l: Row, format?: boolean) { - const pool = usePool(l.poolId) +export function getAmount(l: Row, pool: Pool | TinlakePool, format?: boolean) { switch (l.status) { case 'Closed': return format ? formatBalance(l.totalRepaid) : l.totalRepaid @@ -392,5 +396,6 @@ export function getAmount(l: Row, format?: boolean) { } function Amount({ loan }: { loan: Row }) { - return {getAmount(loan, true)} + const pool = usePool(loan.poolId) + return {getAmount(loan, pool, true)} } diff --git a/centrifuge-app/src/components/PoolCard/index.tsx b/centrifuge-app/src/components/PoolCard/index.tsx index 7959a12476..82b4109a3b 100644 --- a/centrifuge-app/src/components/PoolCard/index.tsx +++ b/centrifuge-app/src/components/PoolCard/index.tsx @@ -161,19 +161,6 @@ export function PoolCard({ ) } - const calculateApy = (tranche: TrancheWithCurrency) => { - const daysSinceCreation = createdAt ? daysBetween(createdAt, new Date()) : 0 - if (poolId === DYF_POOL_ID) return centrifugeTargetAPYs[DYF_POOL_ID][0] - if (poolId === NS3_POOL_ID && tranche.seniority === 0) return centrifugeTargetAPYs[NS3_POOL_ID][0] - if (poolId === NS3_POOL_ID && tranche.seniority === 1) return centrifugeTargetAPYs[NS3_POOL_ID][1] - if (daysSinceCreation > 30 && tranche.yield30DaysAnnualized) - return formatPercentage(tranche.yield30DaysAnnualized, true, {}, 1) - if (tranche.interestRatePerSec) { - return formatPercentage(tranche.interestRatePerSec.toAprPercent(), true, {}, 1) - } - return '-' - } - const tranchesData = useMemo(() => { return tranches ?.map((tranche: TrancheWithCurrency) => { @@ -185,6 +172,19 @@ export function PoolCard({ tranche.currency.decimals ).toDecimal() + const calculateApy = (tranche: TrancheWithCurrency) => { + const daysSinceCreation = createdAt ? daysBetween(createdAt, new Date()) : 0 + if (poolId === DYF_POOL_ID) return centrifugeTargetAPYs[DYF_POOL_ID][0] + if (poolId === NS3_POOL_ID && tranche.seniority === 0) return centrifugeTargetAPYs[NS3_POOL_ID][0] + if (poolId === NS3_POOL_ID && tranche.seniority === 1) return centrifugeTargetAPYs[NS3_POOL_ID][1] + if (daysSinceCreation > 30 && tranche.yield30DaysAnnualized) + return formatPercentage(tranche.yield30DaysAnnualized, true, {}, 1) + if (tranche.interestRatePerSec) { + return formatPercentage(tranche.interestRatePerSec.toAprPercent(), true, {}, 1) + } + return '-' + } + return { seniority: tranche.seniority, name: trancheName, @@ -199,7 +199,7 @@ export function PoolCard({ } }) .reverse() - }, [calculateApy, isTinlakePool, metaData?.tranches, tinlakeKey, tranches]) + }, [isTinlakePool, metaData?.tranches, tinlakeKey, tranches]) return ( diff --git a/centrifuge-app/src/components/PoolList.tsx b/centrifuge-app/src/components/PoolList.tsx index 0aac5b5e68..5becd2238a 100644 --- a/centrifuge-app/src/components/PoolList.tsx +++ b/centrifuge-app/src/components/PoolList.tsx @@ -6,7 +6,6 @@ import { useLocation } from 'react-router' import styled from 'styled-components' import { getPoolValueLocked } from '../utils/getPoolValueLocked' import { TinlakePool } from '../utils/tinlake/useTinlakePools' -import { useIsAboveBreakpoint } from '../utils/useIsAboveBreakpoint' import { useListedPools } from '../utils/useListedPools' import { useMetadataMulti } from '../utils/useMetadata' import { PoolCard, PoolCardProps } from './PoolCard' @@ -42,8 +41,6 @@ export function PoolList() { const { search } = useLocation() const [showArchived, setShowArchived] = React.useState(false) const [listedPools, , metadataIsLoading] = useListedPools() - const isLarge = useIsAboveBreakpoint('L') - const isMedium = useIsAboveBreakpoint('M') const centPools = listedPools.filter(({ id }) => !id.startsWith('0x')) as Pool[] const centPoolsMetaData: PoolMetaDataPartial[] = useMetadataMulti( diff --git a/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx b/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx index a0cc7aeef1..be41ed4ab6 100644 --- a/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx +++ b/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx @@ -42,20 +42,6 @@ export const TrancheTokenCards = ({ return 'mezzanine' } - const calculateApy = (trancheToken: Token) => { - if (isTinlakePool && getTrancheText(trancheToken) === 'senior') return formatPercentage(trancheToken.apy) - if (isTinlakePool && trancheToken.seniority === 0) return '15%' - if (poolId === DYF_POOL_ID) return centrifugeTargetAPYs[poolId as CentrifugeTargetAPYs][0] - if (poolId === NS3_POOL_ID && trancheToken.seniority === 0) - return centrifugeTargetAPYs[poolId as CentrifugeTargetAPYs][0] - if (poolId === NS3_POOL_ID && trancheToken.seniority === 1) - return centrifugeTargetAPYs[poolId as CentrifugeTargetAPYs][1] - if (daysSinceCreation < 30) return 'N/A' - return trancheToken.yield30DaysAnnualized - ? formatPercentage(new Perquintill(trancheToken.yield30DaysAnnualized)) - : '-' - } - const getTarget = (tranche: Token) => (isTinlakePool && tranche.seniority === 0) || poolId === DYF_POOL_ID || poolId === NS3_POOL_ID @@ -132,10 +118,23 @@ export const TrancheTokenCards = ({ }, }, ] - }, [pool.tranches, metadata, poolId]) + }, [pool.tranches, metadata, poolId, pool?.currency.symbol]) const dataTable = useMemo(() => { return trancheTokens.map((tranche) => { + const calculateApy = (trancheToken: Token) => { + if (isTinlakePool && getTrancheText(trancheToken) === 'senior') return formatPercentage(trancheToken.apy) + if (isTinlakePool && trancheToken.seniority === 0) return '15%' + if (poolId === DYF_POOL_ID) return centrifugeTargetAPYs[poolId as CentrifugeTargetAPYs][0] + if (poolId === NS3_POOL_ID && trancheToken.seniority === 0) + return centrifugeTargetAPYs[poolId as CentrifugeTargetAPYs][0] + if (poolId === NS3_POOL_ID && trancheToken.seniority === 1) + return centrifugeTargetAPYs[poolId as CentrifugeTargetAPYs][1] + if (daysSinceCreation < 30) return 'N/A' + return trancheToken.yield30DaysAnnualized + ? formatPercentage(new Perquintill(trancheToken.yield30DaysAnnualized)) + : '-' + } return { tokenName: tranche.name, apy: calculateApy(tranche), diff --git a/centrifuge-app/src/components/Portfolio/Transactions.tsx b/centrifuge-app/src/components/Portfolio/Transactions.tsx index d6189bc428..4887b7d431 100644 --- a/centrifuge-app/src/components/Portfolio/Transactions.tsx +++ b/centrifuge-app/src/components/Portfolio/Transactions.tsx @@ -13,7 +13,6 @@ import { usePagination, } from '@centrifuge/fabric' import * as React from 'react' -import { useTheme } from 'styled-components' import { TransactionTypeChip } from '../../components/Portfolio/TransactionTypeChip' import { formatDate } from '../../utils/date' import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl' @@ -44,7 +43,6 @@ type Row = { export function Transactions({ onlyMostRecent, narrow, txTypes, address, trancheId }: TransactionsProps) { const explorer = useGetExplorerUrl() - const theme = useTheme() const columns = [ { align: 'left', diff --git a/centrifuge-app/src/pages/Pool/Assets/index.tsx b/centrifuge-app/src/pages/Pool/Assets/index.tsx index 5336140979..6647adff56 100644 --- a/centrifuge-app/src/pages/Pool/Assets/index.tsx +++ b/centrifuge-app/src/pages/Pool/Assets/index.tsx @@ -64,7 +64,7 @@ export function PoolDetailAssets() { } const totalAssets = loans.reduce((sum, loan) => { - const amount = new CurrencyBalance(getAmount(loan as any), pool.currency.decimals).toDecimal() + const amount = new CurrencyBalance(getAmount(loan as any, pool), pool.currency.decimals).toDecimal() return sum.add(amount) }, Dec(0)) From 8832b004d77ba10819b470684dc96fa937921b0a Mon Sep 17 00:00:00 2001 From: katty barroso Date: Wed, 30 Oct 2024 19:33:39 +0100 Subject: [PATCH 13/20] Add tooltip to total nav --- centrifuge-app/src/components/Tooltips.tsx | 4 ++++ centrifuge-app/src/pages/Pool/Assets/index.tsx | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/centrifuge-app/src/components/Tooltips.tsx b/centrifuge-app/src/components/Tooltips.tsx index 94790e92a8..854d2b19c1 100644 --- a/centrifuge-app/src/components/Tooltips.tsx +++ b/centrifuge-app/src/components/Tooltips.tsx @@ -342,6 +342,10 @@ export const tooltipText = { label: 'Expense ratio', body: 'The operating expenses of the fund as a percentage of the total NAV', }, + totalNavMinus: { + label: 'Total NAV', + body: 'Total nav minus accrued fees', + }, } export type TooltipsProps = { diff --git a/centrifuge-app/src/pages/Pool/Assets/index.tsx b/centrifuge-app/src/pages/Pool/Assets/index.tsx index 6647adff56..578a6c9cce 100644 --- a/centrifuge-app/src/pages/Pool/Assets/index.tsx +++ b/centrifuge-app/src/pages/Pool/Assets/index.tsx @@ -81,7 +81,7 @@ export function PoolDetailAssets() { const pageSummaryData: { label: React.ReactNode; value: React.ReactNode; heading?: boolean }[] = [ { - label: `Total NAV (${pool.currency.symbol})`, + label: , value: formatBalance(pool.nav.total.toDecimal()), heading: true, }, From 4fa6f4df18989a40b6641ddcc0d4b5de6e1ef33c Mon Sep 17 00:00:00 2001 From: katty barroso Date: Wed, 30 Oct 2024 19:40:17 +0100 Subject: [PATCH 14/20] Fix bug on path to view all transactions --- centrifuge-app/src/components/PoolFees/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/centrifuge-app/src/components/PoolFees/index.tsx b/centrifuge-app/src/components/PoolFees/index.tsx index 6325955e5d..a7014fb055 100644 --- a/centrifuge-app/src/components/PoolFees/index.tsx +++ b/centrifuge-app/src/components/PoolFees/index.tsx @@ -290,7 +290,7 @@ export function PoolFees() { Find a full overview of all pending and executed fee transactions. - View all transactions + View all transactions Date: Thu, 31 Oct 2024 10:45:45 +0100 Subject: [PATCH 15/20] Add chart to onchain reserve --- .../src/components/Charts/SimpleLineChart.tsx | 102 ++++++++++++++++++ centrifuge-app/src/pages/Loan/index.tsx | 40 +++++-- 2 files changed, 134 insertions(+), 8 deletions(-) create mode 100644 centrifuge-app/src/components/Charts/SimpleLineChart.tsx diff --git a/centrifuge-app/src/components/Charts/SimpleLineChart.tsx b/centrifuge-app/src/components/Charts/SimpleLineChart.tsx new file mode 100644 index 0000000000..561af846c2 --- /dev/null +++ b/centrifuge-app/src/components/Charts/SimpleLineChart.tsx @@ -0,0 +1,102 @@ +import { CurrencyBalance, CurrencyMetadata } from '@centrifuge/centrifuge-js' +import { Card, Shelf, Stack, Text } from '@centrifuge/fabric' +import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts' +import { useTheme } from 'styled-components' +import { formatDate } from '../../utils/date' +import { formatBalance, formatBalanceAbbreviated } from '../../utils/formatting' +import { TooltipContainer, TooltipTitle } from './Tooltip' + +type ChartData = { + name: string + yAxis: Number +} + +interface Props { + data: ChartData[] + currency?: CurrencyMetadata +} + +export const SimpleLineChart = ({ data, currency }: Props) => { + const theme = useTheme() + const chartColor = theme.colors.accentPrimary + + return ( + + + {!data.length && ( + + No data available + + )} + + + {data?.length ? ( + + + + + + + + + { + return new Date(tick).toLocaleString('en-US', { day: 'numeric', month: 'short' }) + }} + style={{ fontSize: 8, fill: theme.colors.textPrimary, letterSpacing: '-0.7px' }} + dy={4} + interval={10} + angle={-40} + textAnchor="end" + /> + { + const balance = new CurrencyBalance(tick, currency?.decimals || 0) + return formatBalanceAbbreviated(balance, '', 0) + }} + width={90} + /> + + { + if (payload && payload?.length > 0) { + return ( + + {formatDate(payload[0].payload.name)} + {payload.map(({ value }, index) => { + return ( + + Value + + {formatBalance( + new CurrencyBalance(value?.toString() ?? 0, currency?.decimals || 0), + 'USD', + 2, + 2 + )} + + + ) + })} + + ) + } + return null + }} + /> + + + + + ) : null} + + + + ) +} diff --git a/centrifuge-app/src/pages/Loan/index.tsx b/centrifuge-app/src/pages/Loan/index.tsx index 2409f12ac5..6c24331373 100644 --- a/centrifuge-app/src/pages/Loan/index.tsx +++ b/centrifuge-app/src/pages/Loan/index.tsx @@ -24,6 +24,7 @@ import * as React from 'react' import { useParams } from 'react-router' import styled from 'styled-components' import { AssetSummary } from '../../../src/components/AssetSummary' +import { SimpleLineChart } from '../../../src/components/Charts/SimpleLineChart' import { LoanLabel, getLoanLabelStatus } from '../../../src/components/LoanLabel' import { Dec } from '../../../src/utils/Decimal' import AssetPerformanceChart from '../../components/Charts/AssetPerformanceChart' @@ -80,6 +81,8 @@ const StyledRouterLinkButton = styled(RouterLinkButton)` } ` +const positiveNetflows = ['DEPOSIT_FROM_INVESTMENTS', 'INCREASE_DEBT'] + function ActionButtons({ loan }: { loan: LoanType }) { const canBorrow = useCanBorrowAsset(loan.poolId, loan.id) const [financeShown, setFinanceShown] = React.useState(false) @@ -145,6 +148,20 @@ function Loan() { const isOracle = loan && 'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'oracle' const loanStatus = loan && getLoanLabelStatus(loan)[1] + const getNetflow = (value: Number, type: string) => { + if (positiveNetflows.includes(type)) return value + else return -value + } + + const onchainReserveChart = React.useMemo(() => { + return borrowerAssetTransactions?.map((transaction) => { + return { + name: transaction.timestamp.toString(), + yAxis: getNetflow(transaction.amount?.toNumber() ?? 0, transaction.type), + } + }) + }, [borrowerAssetTransactions]) + const sumRealizedProfitFifo = borrowerAssetTransactions?.reduce( (sum, tx) => sum.add(tx.realizedProfitFifo?.toDecimal() ?? Dec(0)), Dec(0) @@ -244,14 +261,21 @@ function Loan() { {loanId === '0' && ( - - - + <> + + + + + + + + + )} {loan && pool && ( From e45a4d04aee0ca51305876dee0f624aa84bc6723 Mon Sep 17 00:00:00 2001 From: katty barroso Date: Thu, 31 Oct 2024 11:45:58 +0100 Subject: [PATCH 16/20] Add loading buttons to side drawer --- .../src/pages/Loan/CorrectionForm.tsx | 36 +++++++++----- .../src/pages/Loan/ExternalFinanceForm.tsx | 2 +- .../src/pages/Loan/ExternalRepayForm.tsx | 47 ++++++++++++------- centrifuge-app/src/pages/Loan/RepayForm.tsx | 45 ++++++++++++------ 4 files changed, 88 insertions(+), 42 deletions(-) diff --git a/centrifuge-app/src/pages/Loan/CorrectionForm.tsx b/centrifuge-app/src/pages/Loan/CorrectionForm.tsx index f7045e356b..24b838db41 100644 --- a/centrifuge-app/src/pages/Loan/CorrectionForm.tsx +++ b/centrifuge-app/src/pages/Loan/CorrectionForm.tsx @@ -1,6 +1,16 @@ import { ActiveLoan, CurrencyBalance, Pool, Price } from '@centrifuge/centrifuge-js' import { useCentrifugeApi, useCentrifugeTransaction, wrapProxyCallsForAccount } from '@centrifuge/centrifuge-react' -import { Box, Button, CurrencyInput, Shelf, Stack, Text, TextInput } from '@centrifuge/fabric' +import { + Box, + Button, + CurrencyInput, + IconCheckCircle, + IconClock, + Shelf, + Stack, + Text, + TextInput, +} from '@centrifuge/fabric' import Decimal from 'decimal.js-light' import { Field, FieldProps, Form, FormikProvider, useFormik } from 'formik' import * as React from 'react' @@ -15,6 +25,7 @@ import { useBorrower } from '../../utils/usePermissions' import { usePool } from '../../utils/usePools' import { combine, max, maxPriceVariance, positiveNumber, required } from '../../utils/validation' import { useChargePoolFees } from './ChargeFeesFields' +import { StyledSuccessButton } from './ExternalFinanceForm' import { isCashLoan, isExternalLoan, isInternalLoan } from './utils' export type CorrectionValues = { @@ -30,6 +41,7 @@ export function CorrectionForm({ loan }: { loan: ActiveLoan }) { const pool = usePool(loan.poolId) as Pool const account = useBorrower(loan.poolId, loan.id) const poolFees = useChargePoolFees(loan.poolId, loan.id) + const [transactionSuccess, setTransactionSuccess] = React.useState(false) const { initial: availableFinancing } = useAvailableFinancing(loan.poolId, loan.id) const api = useCentrifugeApi() const { execute: doFinanceTransaction, isLoading: isFinanceLoading } = useCentrifugeTransaction( @@ -82,8 +94,7 @@ export function CorrectionForm({ loan }: { loan: ActiveLoan }) { }, { onSuccess: () => { - correctionForm.setFieldValue('fees', [], false) - correctionForm.setFieldValue('reason', '', false) + setTransactionSuccess(true) }, } ) @@ -235,15 +246,18 @@ export function CorrectionForm({ loan }: { loan: ActiveLoan }) { {poolFees.renderSummary()} - - + {transactionSuccess ? ( + }>Transaction successful + ) : ( + + )} diff --git a/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx b/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx index b9f1da851a..da9a58140f 100644 --- a/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx +++ b/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx @@ -46,7 +46,7 @@ export type FinanceValues = { fees: { id: string; amount: '' | number | Decimal }[] } -const StyledSuccessButton = styled(Button)` +export const StyledSuccessButton = styled(Button)` span { color: ${({ theme }) => theme.colors.textPrimary}; background-color: ${({ theme }) => theme.colors.statusOkBg}; diff --git a/centrifuge-app/src/pages/Loan/ExternalRepayForm.tsx b/centrifuge-app/src/pages/Loan/ExternalRepayForm.tsx index cae51709c2..c286192fda 100644 --- a/centrifuge-app/src/pages/Loan/ExternalRepayForm.tsx +++ b/centrifuge-app/src/pages/Loan/ExternalRepayForm.tsx @@ -5,7 +5,17 @@ import { useCentrifugeUtils, wrapProxyCallsForAccount, } from '@centrifuge/centrifuge-react' -import { Box, Button, CurrencyInput, InlineFeedback, Shelf, Stack, Text } from '@centrifuge/fabric' +import { + Box, + Button, + CurrencyInput, + IconCheckCircle, + IconClock, + InlineFeedback, + Shelf, + Stack, + Text, +} from '@centrifuge/fabric' import { BN } from 'bn.js' import Decimal from 'decimal.js-light' import { Field, FieldProps, Form, FormikProvider, useFormik } from 'formik' @@ -23,6 +33,7 @@ import { usePool } from '../../utils/usePools' import { combine, maxNotRequired, nonNegativeNumberNotRequired } from '../../utils/validation' import { useChargePoolFees } from './ChargeFeesFields' import { ErrorMessage } from './ErrorMessage' +import { StyledSuccessButton } from './ExternalFinanceForm' import { SourceSelect } from './SourceSelect' type RepayValues = { @@ -57,6 +68,7 @@ export function ExternalRepayForm({ const destinationLoan = loans?.find((l) => l.id === destination) as ActiveLoan const displayCurrency = destination === 'reserve' ? pool.currency.symbol : 'USD' const utils = useCentrifugeUtils() + const [transactionSuccess, setTransactionSuccess] = React.useState(false) const { execute: doRepayTransaction, isLoading: isRepayLoading } = useCentrifugeTransaction( 'Sell asset', @@ -101,7 +113,7 @@ export function ExternalRepayForm({ }, { onSuccess: () => { - repayForm.resetForm() + setTransactionSuccess(true) }, } ) @@ -356,20 +368,23 @@ export function ExternalRepayForm({ - + {transactionSuccess ? ( + }>Transaction successful + ) : ( + + )} diff --git a/centrifuge-app/src/pages/Loan/RepayForm.tsx b/centrifuge-app/src/pages/Loan/RepayForm.tsx index 7b8bfae6d1..1c31332001 100644 --- a/centrifuge-app/src/pages/Loan/RepayForm.tsx +++ b/centrifuge-app/src/pages/Loan/RepayForm.tsx @@ -14,7 +14,18 @@ import { useCentrifugeUtils, wrapProxyCallsForAccount, } from '@centrifuge/centrifuge-react' -import { Box, Button, CurrencyInput, InlineFeedback, Select, Shelf, Stack, Text } from '@centrifuge/fabric' +import { + Box, + Button, + CurrencyInput, + IconCheckCircle, + IconClock, + InlineFeedback, + Select, + Shelf, + Stack, + Text, +} from '@centrifuge/fabric' import Decimal from 'decimal.js-light' import { Field, FieldProps, Form, FormikProvider, useFormik } from 'formik' import * as React from 'react' @@ -36,6 +47,7 @@ import { } from '../../utils/validation' import { useChargePoolFees } from './ChargeFeesFields' import { ErrorMessage } from './ErrorMessage' +import { StyledSuccessButton } from './ExternalFinanceForm' import { ExternalRepayForm } from './ExternalRepayForm' import { SourceSelect } from './SourceSelect' import { isCashLoan, isExternalLoan } from './utils' @@ -92,6 +104,7 @@ function InternalRepayForm({ const destinationLoan = loans?.find((l) => l.id === destination) as Loan const displayCurrency = destination === 'reserve' ? pool.currency.symbol : 'USD' const utils = useCentrifugeUtils() + const [transactionSuccess, setTransactionSuccess] = React.useState(false) const { execute: doRepayTransaction, isLoading: isRepayLoading } = useCentrifugeTransaction( isCashLoan(loan) ? 'Withdraw funds' : 'Repay asset', @@ -137,7 +150,7 @@ function InternalRepayForm({ }, { onSuccess: () => { - repayForm.resetForm() + setTransactionSuccess(true) }, } ) @@ -409,18 +422,22 @@ function InternalRepayForm({ - + {transactionSuccess ? ( + }>Transaction successful + ) : ( + + )} From 909ad82a3bd680eed579701f47233519df56f4c8 Mon Sep 17 00:00:00 2001 From: katty barroso Date: Tue, 5 Nov 2024 14:42:30 +0100 Subject: [PATCH 17/20] Add feedback --- .../src/components/LayoutBase/styles.tsx | 2 +- centrifuge-app/src/components/Tooltips.tsx | 26 ++++++++++++++++--- centrifuge-app/src/pages/Loan/index.tsx | 7 +++-- .../src/pages/Pool/Assets/index.tsx | 21 +++++++++++---- .../components/WalletMenu/ConnectButton.tsx | 2 +- fabric/src/components/Button/WalletButton.tsx | 1 - fabric/src/components/Tooltip/index.tsx | 13 ++++++++-- fabric/src/icon-svg/icon-chevron-down.svg | 4 +-- fabric/src/icon-svg/icon-download.svg | 6 ++--- 9 files changed, 62 insertions(+), 20 deletions(-) diff --git a/centrifuge-app/src/components/LayoutBase/styles.tsx b/centrifuge-app/src/components/LayoutBase/styles.tsx index 9faef617e6..e51f59ff7a 100644 --- a/centrifuge-app/src/components/LayoutBase/styles.tsx +++ b/centrifuge-app/src/components/LayoutBase/styles.tsx @@ -142,7 +142,7 @@ export const WalletInner = styled(Stack)` height: 80px; justify-content: center; pointer-events: auto; - width: 250px; + width: 200px; @media (min-width: ${({ theme }) => theme.breakpoints[BREAK_POINT_COLUMNS]}) { justify-content: flex-end; diff --git a/centrifuge-app/src/components/Tooltips.tsx b/centrifuge-app/src/components/Tooltips.tsx index 854d2b19c1..56fc99eff3 100644 --- a/centrifuge-app/src/components/Tooltips.tsx +++ b/centrifuge-app/src/components/Tooltips.tsx @@ -1,8 +1,18 @@ import { Tooltip as FabricTooltip, Text, TextProps } from '@centrifuge/fabric' import * as React from 'react' import { useParams } from 'react-router' +import styled from 'styled-components' import { usePool } from '../utils/usePools' +const StyledText = styled(Text)<{ hoverable?: boolean }>` + ${({ hoverable }) => + hoverable && + ` + &:hover { + text-decoration: underline + } + `} +` function ValueLockedTooltipBody({ poolId }: { poolId?: string }) { const { pid: poolIdParam } = useParams<{ pid: string }>() const pool = usePool(poolId || poolIdParam || '', false) @@ -354,21 +364,31 @@ export type TooltipsProps = { props?: any size?: 'med' | 'sm' | 'xs' color?: string + hoverable?: boolean } & Partial> -export function Tooltips({ type, label: labelOverride, size = 'sm', props, color, ...textProps }: TooltipsProps) { +export function Tooltips({ + type, + label: labelOverride, + size = 'sm', + props, + color, + hoverable = false, + ...textProps +}: TooltipsProps) { const { label, body } = type ? tooltipText[type] : { label: labelOverride, body: textProps.body } return ( {typeof label === 'string' ? ( - {labelOverride || label} - + ) : ( label )} diff --git a/centrifuge-app/src/pages/Loan/index.tsx b/centrifuge-app/src/pages/Loan/index.tsx index 6c24331373..527e1eea8b 100644 --- a/centrifuge-app/src/pages/Loan/index.tsx +++ b/centrifuge-app/src/pages/Loan/index.tsx @@ -69,12 +69,15 @@ const StyledRouterLinkButton = styled(RouterLinkButton)` padding: 0px; width: fit-content; margin-left: 30px; + border: 4px solid transparent; > span { width: 34px; + border: 4px solid transparent; } &:hover { - background-color: ${({ theme }) => theme.colors.backgroundTertiary}; + background-color: ${({ theme }) => theme.colors.backgroundSecondary}; + border: ${({ theme }) => `4px solid ${theme.colors.backgroundTertiary}`}; span { color: ${({ theme }) => theme.colors.textPrimary}; } @@ -279,7 +282,7 @@ function Loan() { )} {loan && pool && ( - + }> diff --git a/centrifuge-app/src/pages/Pool/Assets/index.tsx b/centrifuge-app/src/pages/Pool/Assets/index.tsx index 578a6c9cce..e212d2cd35 100644 --- a/centrifuge-app/src/pages/Pool/Assets/index.tsx +++ b/centrifuge-app/src/pages/Pool/Assets/index.tsx @@ -64,7 +64,10 @@ export function PoolDetailAssets() { } const totalAssets = loans.reduce((sum, loan) => { - const amount = new CurrencyBalance(getAmount(loan as any, pool), pool.currency.decimals).toDecimal() + const amount = + hasValuationMethod(loan.pricing) && loan.pricing.valuationMethod !== 'cash' + ? new CurrencyBalance(getAmount(loan as any, pool), pool.currency.decimals).toDecimal() + : 0 return sum.add(amount) }, Dec(0)) @@ -79,14 +82,17 @@ export function PoolDetailAssets() { Dec(0) ) + const total = isTinlakePool ? pool.nav.total : pool.reserve.total.toDecimal().add(offchainReserve).add(totalAssets) + const totalNAV = isTinlakePool ? pool.nav.total : Dec(total).sub(pool.fees.totalPaid.toDecimal()) + const pageSummaryData: { label: React.ReactNode; value: React.ReactNode; heading?: boolean }[] = [ { - label: , - value: formatBalance(pool.nav.total.toDecimal()), + label: `Total NAV (${pool.currency.symbol})`, + value: formatBalance(totalNAV), heading: true, }, { - label: , + label: , value: ( {formatBalance(pool.reserve.total || 0)} @@ -98,7 +104,7 @@ export function PoolDetailAssets() { ...(!isTinlakePool && cashLoans.length ? [ { - label: , + label: , value: , heading: false, }, @@ -107,6 +113,11 @@ export function PoolDetailAssets() { value: formatBalance(totalAssets), heading: false, }, + { + label: `Accrued fees (${pool.currency.symbol})`, + value: `-${formatBalance(pool.fees.totalPaid)}`, + heading: false, + }, ] : []), ] diff --git a/centrifuge-react/src/components/WalletMenu/ConnectButton.tsx b/centrifuge-react/src/components/WalletMenu/ConnectButton.tsx index 0190f0cc7e..4cb6592e25 100644 --- a/centrifuge-react/src/components/WalletMenu/ConnectButton.tsx +++ b/centrifuge-react/src/components/WalletMenu/ConnectButton.tsx @@ -6,7 +6,7 @@ type Props = WalletButtonProps & { label?: string } -export function ConnectButton({ label = 'Connect', ...rest }: Props) { +export function ConnectButton({ label = 'Connect wallet', ...rest }: Props) { const { showNetworks, pendingConnect } = useWallet() return ( diff --git a/fabric/src/components/Button/WalletButton.tsx b/fabric/src/components/Button/WalletButton.tsx index a09a120c0c..2ced79aab2 100644 --- a/fabric/src/components/Button/WalletButton.tsx +++ b/fabric/src/components/Button/WalletButton.tsx @@ -22,7 +22,6 @@ export type WalletButtonProps = Omit< const StyledButton = styled.button` display: inline-block; - width: 100%; padding: 0; border: none; appearance: none; diff --git a/fabric/src/components/Tooltip/index.tsx b/fabric/src/components/Tooltip/index.tsx index f926323331..e21423e7f1 100644 --- a/fabric/src/components/Tooltip/index.tsx +++ b/fabric/src/components/Tooltip/index.tsx @@ -60,10 +60,18 @@ const placements: { const Container = styled(Stack)<{ pointer: PlacementAxis }>` background-color: ${({ theme }) => theme.colors.backgroundInverted}; filter: ${({ theme }) => `drop-shadow(${theme.shadows.cardInteractive})`}; + opacity: 0; + transform: translateY(-10px); + transition: opacity 0.2s ease, transform 0.2s ease; + will-change: opacity, transform; + + &.show { + opacity: 1; + transform: translateY(0); + } &::before { --size: 5px; - content: ''; position: absolute; ${({ pointer }) => placements[pointer!]} @@ -77,7 +85,7 @@ export function Tooltip({ body, children, disabled, - delay = 1000, + delay = 200, bodyWidth, bodyPadding, triggerStyle, @@ -108,6 +116,7 @@ export function Tooltip({ {...tooltipElementProps} {...rest} ref={overlayRef} + className={state.isOpen ? 'show' : ''} backgroundColor="backgroundPrimary" p={bodyPadding ?? 1} borderRadius="tooltip" diff --git a/fabric/src/icon-svg/icon-chevron-down.svg b/fabric/src/icon-svg/icon-chevron-down.svg index f87d1768a3..e70d2ea0d0 100644 --- a/fabric/src/icon-svg/icon-chevron-down.svg +++ b/fabric/src/icon-svg/icon-chevron-down.svg @@ -1,3 +1,3 @@ - - + + diff --git a/fabric/src/icon-svg/icon-download.svg b/fabric/src/icon-svg/icon-download.svg index b1b7145f29..ea20391771 100644 --- a/fabric/src/icon-svg/icon-download.svg +++ b/fabric/src/icon-svg/icon-download.svg @@ -1,5 +1,5 @@ - - - + + + From b3b0d0af02f79198d5080c246ec9df9797273949 Mon Sep 17 00:00:00 2001 From: katty barroso Date: Tue, 5 Nov 2024 14:46:14 +0100 Subject: [PATCH 18/20] Add ongoing assets --- centrifuge-app/src/components/LoanList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/centrifuge-app/src/components/LoanList.tsx b/centrifuge-app/src/components/LoanList.tsx index 9b028a1c58..bd2347f209 100644 --- a/centrifuge-app/src/components/LoanList.tsx +++ b/centrifuge-app/src/components/LoanList.tsx @@ -267,7 +267,7 @@ export function LoanList({ loans }: Props) { return ( <> - Assets + {filters.data.map((loan) => loan.status === 'Active').length} ongoing assets