diff --git a/centrifuge-app/src/components/AssetSummary.tsx b/centrifuge-app/src/components/AssetSummary.tsx
index 3b078fd42f..a3cb456af4 100644
--- a/centrifuge-app/src/components/AssetSummary.tsx
+++ b/centrifuge-app/src/components/AssetSummary.tsx
@@ -1,41 +1,33 @@
-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?: {
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 (
-
-
-
- Details
- {loan && }
-
-
-
- {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 85cf10f542..19e0a70428 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 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 []
@@ -157,58 +120,72 @@ 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
-
+
+
+ {isNonCash ? 'Asset performance' : 'Cash balance'}
+
+ ({isNonCash ? pool.currency.symbol ?? 'USD' : '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 +202,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 +211,9 @@ function AssetPerformanceChart({ pool, poolId, loanId }: Props) {
formatBalanceAbbreviated(tick, '', 2)}
- domain={activeFilter.value === 'price' ? priceRange : [0, 'auto']}
+ domain={selectedTabIndex === 0 ? priceRange : ['auto', 'auto']}
width={90}
/>
@@ -249,22 +226,38 @@ 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,
+ isNonCash ? pool.currency.symbol : 'USD',
+ 2
+ )
: payload[0].payload.futurePV
- ? `~${formatBalance(payload[0].payload.futurePV, 'USD', 2)}`
+ ? `~${formatBalance(
+ payload[0].payload.futurePV,
+ isNonCash ? pool.currency.symbol : 'USD',
+ 2
+ )}`
: '-'}
- {'Price'}
-
+ Price
+
{payload[0].payload.historicPrice
- ? formatBalance(payload[0].payload.historicPrice, 'USD', 6)
+ ? formatBalance(
+ payload[0].payload.historicPrice,
+ isNonCash ? pool.currency.symbol : 'USD',
+ 6
+ )
: payload[0].payload.futurePrice
- ? `~${formatBalance(payload[0].payload.futurePrice, 'USD', 6)}`
+ ? `~${formatBalance(
+ payload[0].payload.futurePrice,
+ isNonCash ? pool.currency.symbol : 'USD',
+ 6
+ )}`
: '-'}
@@ -277,7 +270,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 && (
{
+ 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/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/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/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/LoanList.tsx b/centrifuge-app/src/components/LoanList.tsx
index 9b5876f27f..18be9bec1a 100644
--- a/centrifuge-app/src/components/LoanList.tsx
+++ b/centrifuge-app/src/components/LoanList.tsx
@@ -1,8 +1,10 @@
import { useBasePath } from '@centrifuge/centrifuge-app/src/utils/useBasePath'
-import { CurrencyBalance, Loan, Rate, TinlakeLoan } from '@centrifuge/centrifuge-js'
+import { CurrencyBalance, Loan, Pool, TinlakeLoan } from '@centrifuge/centrifuge-js'
import {
+ AnchorButton,
Box,
- IconChevronRight,
+ Button,
+ IconDownload,
Pagination,
PaginationContainer,
Shelf,
@@ -13,105 +15,124 @@ import {
} from '@centrifuge/fabric'
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 { 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'
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, usePoolMetadata } 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')
+ const navigate = useNavigate()
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 { 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 aId.localeCompare(bId)
- })
- }, [isTinlakePool, loans])
- const filters = useFilters({
- data: loansWithLabelStatus,
- })
-
- React.useEffect(() => {
- prefetchRoute('/pools/1/assets/1')
- }, [])
const additionalColumns: Column[] =
- templateMetadata?.keyAttributes?.map((key) => {
+ templateMetadata?.keyAttributes?.map((key, index) => {
const attr = templateMetadata.attributes![key]
return {
align: 'left',
- header: attr.label,
+ header: ,
cell: (l: Row) => ,
+ sortKey: attr.label.toLowerCase(),
}
}) || []
- 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 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 loansWithLabelStatus = React.useMemo(() => {
+ return loansData.sort((a, b) => {
+ const aId = get(a, 'id') as string
+ const bId = get(b, 'id') as string
+
+ return aId.localeCompare(bId)
+ })
+ }, [loansData])
+
+ const filters = useFilters({
+ data: loansWithLabelStatus as Loan[],
+ })
+
+ React.useEffect(() => {
+ prefetchRoute('/pools/1/assets/1')
+ }, [])
+
+ const rows: Row[] = filters.data.map((loan) => {
+ const snapshot = snapshotsValues?.[loan.id]
+ const marketValue = snapshot?.marketValue?.toDecimal().toNumber() ?? 0
+
+ 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)
@@ -120,11 +141,10 @@ export function LoanList({ loans }: Props) {
align: 'left',
header: ,
cell: (l: Row) => ,
- sortKey: 'idSortKey',
- width: 'minmax(300px, 1fr)',
+ sortKey: 'id',
},
...(additionalColumns?.length
- ? additionalColumns
+ ? additionalColumns.filter((attr) => attr.sortKey !== 'term')
: [
{
align: 'left',
@@ -137,7 +157,7 @@ export function LoanList({ loans }: Props) {
? formatDate(l.originationDate)
: '-'
},
- sortKey: 'originationDateSortKey',
+ sortKey: 'originationDate',
},
]),
...(hasMaturityDate
@@ -157,98 +177,141 @@ 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: 'outstandingDebt',
+ },
+ ]),
+ ...(isTinlakePool
+ ? []
+ : [
+ {
+ align: 'left',
+ header: ,
+ cell: (l: Row) => formatBalance(l.marketPrice ?? 0, pool.currency, 2, 0),
+ sortKey: 'marketPrice',
+ },
+ ]),
+ ...(isTinlakePool
+ ? []
+ : [
+ {
+ align: 'left',
+ header: ,
+ cell: (l: Row) => formatBalance(l.marketValue ?? 0, pool.currency, 2, 0),
+ sortKey: 'marketValue',
+ },
+ ]),
+ ...(isTinlakePool
+ ? []
+ : [
+ {
+ align: 'left',
+ header: ,
+ cell: (l: Row) => formatBalance(l.unrealizedPL ?? 0, pool.currency, 2, 0),
+ sortKey: 'unrealizedPL',
+ width: '140px',
+ },
+ ]),
+ ...(isTinlakePool
+ ? []
+ : [
+ {
+ align: 'left',
+ header: ,
+ cell: (l: Row) => formatBalance(l.realizedPL ?? 0, pool.currency, 2, 0),
+ sortKey: 'realizedPL',
+ width: '140px',
+ },
+ ]),
+ ...(isTinlakePool
+ ? []
+ : [
+ {
+ align: 'left',
+ header: ,
+ cell: (l: Row) => formatPercentage(l.portfolioPercentage ?? 0, true, undefined, 1),
+ sortKey: 'portfolioPercentage',
+ },
+ ]),
].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 })
+ const csvData = React.useMemo(() => {
+ if (!rows.length) return undefined
+
+ 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, pool])
+
+ const csvUrl = React.useMemo(() => csvData && getCSVDownloadUrl(csvData as any), [csvData])
+
return (
-
-
-
-
- `${basePath}/${poolId}/assets/${row.id}`}
- pageSize={20}
- page={pagination.page}
- pinnedData={pinnedData}
- defaultSortKey="maturityDate"
- />
-
-
- {pagination.pageCount > 1 && (
-
-
-
- )}
-
-
+ <>
+
+ {filters.data.map((loan) => loan.status === 'Active').length} ongoing assets
+
+
+
+ Download
+
+
+
+
+
+
+
+ `${basePath}/${poolId}/assets/${row.id}`}
+ pageSize={20}
+ page={pagination.page}
+ defaultSortKey="maturityDate"
+ />
+
+
+ {pagination.pageCount > 1 && (
+
+
+
+ )}
+
+
+ >
)
}
@@ -275,23 +338,7 @@ export function AssetName({ loan }: { loan: Pick
-
-
-
-
- Onchain reserve
} />
-
-
- )
- }
+ if (loan.id === '0') return
if (isTinlakePool) {
return (
@@ -299,7 +346,7 @@ export function AssetName({ loan }: { loan: Pick
{loan.asset.nftId.length >= 9
@@ -310,30 +357,12 @@ export function AssetName({ loan }: { loan: Pick
-
-
-
-
- {metadata?.name}} />
-
-
- )
- }
-
return (
{metadata?.name}
@@ -341,34 +370,32 @@ export function AssetName({ loan }: { loan: Pick
)
}
+export function getAmount(l: Row, pool: Pool | TinlakePool, format?: boolean) {
+ switch (l.status) {
+ case 'Closed':
+ return format ? formatBalance(l.totalRepaid) : l.totalRepaid
-function Amount({ loan }: { loan: Row }) {
- const pool = usePool(loan.poolId)
-
- function getAmount(l: Row) {
- switch (l.status) {
- case 'Closed':
- return formatBalance(l.totalRepaid, pool?.currency.symbol)
-
- case 'Active':
- if ('presentValue' in l) {
- return formatBalance(l.presentValue, pool?.currency.symbol)
- }
+ case 'Active':
+ if ('presentValue' in l) {
+ return format ? formatBalance(l.presentValue) : l.presentValue
+ }
- if (l.outstandingDebt.isZero()) {
- return formatBalance(l.totalRepaid, pool?.currency.symbol)
- }
+ if (l.outstandingDebt.isZero()) {
+ return format ? formatBalance(l.totalRepaid) : l.totalRepaid
+ }
- return formatBalance(l.outstandingDebt, pool?.currency.symbol)
+ return format ? formatBalance(l.outstandingDebt) : l.outstandingDebt
- // @ts-expect-error
- case '':
- return formatBalance(pool.reserve.total, pool?.currency.symbol)
+ // @ts-expect-error
+ case '':
+ return format ? formatBalance(pool.reserve.total) : pool.reserve.total
- default:
- return `0 ${pool?.currency.symbol}`
- }
+ default:
+ return `0`
}
+}
- return {getAmount(loan)}
+function Amount({ loan }: { loan: Row }) {
+ const pool = usePool(loan.poolId)
+ return {getAmount(loan, pool, true)}
}
diff --git a/centrifuge-app/src/components/PageSection.tsx b/centrifuge-app/src/components/PageSection.tsx
index 8681196346..0b0082a8b4 100644
--- a/centrifuge-app/src/components/PageSection.tsx
+++ b/centrifuge-app/src/components/PageSection.tsx
@@ -43,17 +43,7 @@ export function PageSection({
}: Props) {
const [open, setOpen] = React.useState(defaultOpen)
return (
-
+
{(title || titleAddition) && (
diff --git a/centrifuge-app/src/components/PageSummary.tsx b/centrifuge-app/src/components/PageSummary.tsx
index 17d1dac6b4..edf769f8f7 100644
--- a/centrifuge-app/src/components/PageSummary.tsx
+++ b/centrifuge-app/src/components/PageSummary.tsx
@@ -1,4 +1,4 @@
-import { Shelf, Stack, Text } from '@centrifuge/fabric'
+import { Box, Shelf, Stack, Text } from '@centrifuge/fabric'
import * as React from 'react'
import { useTheme } from 'styled-components'
@@ -6,6 +6,7 @@ type Props = {
data?: {
label: React.ReactNode
value: React.ReactNode
+ heading?: boolean
}[]
children?: React.ReactNode
}
@@ -14,21 +15,24 @@ export function PageSummary({ data, children }: Props) {
const theme = useTheme()
return (
- {data?.map(({ label, value }, index) => (
+ {data?.map(({ label, value, heading }, index) => (
{label}
- {value}
+
+ {value}
+
))}
- {children}
+ {children}
)
}
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/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
!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/PoolOverview/TransactionHistory.tsx b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx
index 38718364ef..779cb85df0 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'
@@ -225,6 +225,7 @@ export const TransactionHistoryTable = ({
),
sortKey: 'transactionDate',
+ width: '200px',
},
{
align: 'left',
@@ -249,19 +250,21 @@ export const TransactionHistoryTable = ({
)
},
sortKey: 'transaction',
+ width: '50%',
},
{
- align: 'right',
- header: ,
+ align: 'left',
+ header: ,
cell: ({ amount, netFlow }: Row) => (
{amount ? `${activeAssetId && netFlow === 'negative' ? '-' : ''}${formatBalance(amount, 'USD', 2, 2)}` : ''}
),
- sortKey: 'amount',
+ sortKey: 'quantity',
+ width: '250px',
},
{
- align: 'right',
+ align: 'center',
header: 'View transaction',
cell: ({ hash }: Row) => {
return (
@@ -282,9 +285,7 @@ export const TransactionHistoryTable = ({
return (
-
- Transaction history
-
+ Transaction history
{transactions?.length! > 8 && preview && (
@@ -306,7 +307,9 @@ export const TransactionHistoryTable = ({
)}
-
+
+
+
)
}
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/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/components/Tooltips.tsx b/centrifuge-app/src/components/Tooltips.tsx
index 94790e92a8..67c170adbe 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',
},
+ totalNavMinusFees: {
+ label: 'Total NAV',
+ body: 'Total nav minus accrued fees',
+ },
}
export type TooltipsProps = {
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..24b838db41 100644
--- a/centrifuge-app/src/pages/Loan/CorrectionForm.tsx
+++ b/centrifuge-app/src/pages/Loan/CorrectionForm.tsx
@@ -1,10 +1,21 @@
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,
+ 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'
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'
@@ -14,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 = {
@@ -25,9 +37,11 @@ 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)
+ const [transactionSuccess, setTransactionSuccess] = React.useState(false)
const { initial: availableFinancing } = useAvailableFinancing(loan.poolId, loan.id)
const api = useCentrifugeApi()
const { execute: doFinanceTransaction, isLoading: isFinanceLoading } = useCentrifugeTransaction(
@@ -80,8 +94,7 @@ export function CorrectionForm({ loan }: { loan: ActiveLoan }) {
},
{
onSuccess: () => {
- correctionForm.setFieldValue('fees', [], false)
- correctionForm.setFieldValue('reason', '', false)
+ setTransactionSuccess(true)
},
}
)
@@ -116,105 +129,112 @@ 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 ? '+' : ''}
@@ -226,15 +246,18 @@ export function CorrectionForm({ loan }: { loan: ActiveLoan }) {
{poolFees.renderSummary()}
-
-
+ {transactionSuccess ? (
+ }>Transaction successful
+ ) : (
+ : undefined}
+ >
+ {isFinanceLoading ? 'Transaction Pending' : 'Adjust'}
+
+ )}
diff --git a/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx b/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx
index 041c7faeb6..da9a58140f 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 }[]
}
+export 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,70 @@ 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 +248,65 @@ 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
+ ) : (
+ : undefined}
+ >
+ {isFinanceLoading ? 'Transaction Pending' : 'Purchase'}
+
+ )}
diff --git a/centrifuge-app/src/pages/Loan/ExternalRepayForm.tsx b/centrifuge-app/src/pages/Loan/ExternalRepayForm.tsx
index 18e8b025e8..c286192fda 100644
--- a/centrifuge-app/src/pages/Loan/ExternalRepayForm.tsx
+++ b/centrifuge-app/src/pages/Loan/ExternalRepayForm.tsx
@@ -5,12 +5,23 @@ import {
useCentrifugeUtils,
wrapProxyCallsForAccount,
} from '@centrifuge/centrifuge-react'
-import { 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'
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 +33,8 @@ 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 = {
price: number | '' | Decimal
@@ -36,7 +49,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)
@@ -46,6 +68,7 @@ export function ExternalRepayForm({ loan, destination }: { loan: ExternalLoan; d
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',
@@ -90,7 +113,7 @@ export function ExternalRepayForm({ loan, destination }: { loan: ExternalLoan; d
},
{
onSuccess: () => {
- repayForm.resetForm()
+ setTransactionSuccess(true)
},
}
)
@@ -187,94 +210,102 @@ 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,58 +323,68 @@ 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.
+
+
+ )}
+
-
+ {transactionSuccess ? (
+ }>Transaction successful
+ ) : (
+ : undefined}
+ >
+ {isRepayLoading ? 'Transaction Pending' : 'Sell'}
+
+ )}
diff --git a/centrifuge-app/src/pages/Loan/FinanceForm.tsx b/centrifuge-app/src/pages/Loan/FinanceForm.tsx
index 016793aec3..78e8a8236e 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,115 @@ 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()}
+ {source === 'other' && (
+
+ {({ field }: 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 +314,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 +391,18 @@ function WithdrawSelect({ withdrawAddresses, poolId }: { withdrawAddresses: With
)
return (
-
-
+ {transactionSuccess ? (
+ }>Transaction successful
+ ) : (
+ : undefined}
+ >
+ {isRepayLoading ? 'Transaction Pending' : isCashLoan(loan) ? 'Withdraw' : 'Repay'}
+
+ )}
diff --git a/centrifuge-app/src/pages/Loan/TransactionTable.tsx b/centrifuge-app/src/pages/Loan/TransactionTable.tsx
index 3ce9bcda10..5c92a876db 100644
--- a/centrifuge-app/src/pages/Loan/TransactionTable.tsx
+++ b/centrifuge-app/src/pages/Loan/TransactionTable.tsx
@@ -19,6 +19,7 @@ type Props = {
poolType?: string
maturityDate?: Date
originationDate: Date | undefined
+ loanStatus: string
}
type Row = {
@@ -31,6 +32,7 @@ type Row = {
position: Decimal
yieldToMaturity: Decimal | null
realizedProfitFifo: CurrencyBalance | null
+ unrealizedProfitAtMarketPrice: CurrencyBalance | null
}
export const TransactionTable = ({
@@ -41,6 +43,7 @@ export const TransactionTable = ({
pricing,
poolType,
maturityDate,
+ loanStatus,
}: Props) => {
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
+ )
: '-',
},
{
@@ -204,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)}` : '-'),
},
{
@@ -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..527e1eea8b 100644
--- a/centrifuge-app/src/pages/Loan/index.tsx
+++ b/centrifuge-app/src/pages/Loan/index.tsx
@@ -1,28 +1,36 @@
-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,
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 { 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'
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 +55,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 +62,30 @@ 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;
+ border: 4px solid transparent;
+
+ > span {
+ width: 34px;
+ border: 4px solid transparent;
+ }
+ &:hover {
+ background-color: ${({ theme }) => theme.colors.backgroundSecondary};
+ border: ${({ theme }) => `4px solid ${theme.colors.backgroundTertiary}`};
+ span {
+ color: ${({ theme }) => theme.colors.textPrimary};
+ }
+ }
+`
+
+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)
@@ -68,7 +93,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 +117,26 @@ function ActionButtons({ loan }: { loan: LoanType }) {
{!(loan.pricing.maturityDate && new Date() > new Date(loan.pricing.maturityDate)) ||
!loan.pricing.maturityDate ? (
-
- >
+
)
}
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 +148,32 @@ 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 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)
+ )
+
+ const unrealizedProfitAtMarketPrice = borrowerAssetTransactions?.reduce(
+ (sum, tx) => sum.add(tx.unrealizedProfitAtMarketPrice?.toDecimal() ?? Dec(0)),
+ Dec(0)
+ )
const currentFace =
loan?.pricing && 'outstandingQuantity' in loan.pricing
@@ -149,34 +199,77 @@ function Loan() {
const originationDate = loan && 'originationDate' in loan ? new Date(loan?.originationDate).toISOString() : undefined
+ const getCurrentValue = () => {
+ if (loanId === '0') return pool.reserve.total
+ if (loan && 'presentValue' in loan) return loan.presentValue
+ 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 (
-
-
-
- {poolMetadata?.pool?.name ?? 'Pool assets'}
-
+
+
+
+
+
+ {name}
+
+ {loan && }
+
-
-
- {name}
-
- {loan && }
-
+
+ }
- />
+ >
+ {loan && !isTinlakeLoan(loan) && }
+
+
{loanId === '0' && (
<>
-
+
+
+
+
+
)}
{loan && pool && (
-
-
+
+
}>
- {'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'oracle' && (
+ {isOracle && (
}>
@@ -206,13 +299,13 @@ function Loan() {
gridAutoRows="minContent"
gap={[2, 2, 2]}
>
- {'valuationMethod' in loan.pricing && loan.pricing.valuationMethod === 'oracle' && (
+ {isOracle && (
}>
)}
@@ -228,11 +321,9 @@ function Loan() {
if (!isPublic) return null
return (
}>
-
+
-
- {section.name}
-
+ {section.name}
-
-
+
) : (
-
-
-
- Transaction history
-
-
-
-
-
+
+ Transaction history
+
+
)
) : null}
@@ -319,6 +400,6 @@ function Loan() {
) : null}
-
+
)
}
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..9b4d574087
--- /dev/null
+++ b/centrifuge-app/src/pages/Pool/Assets/OffchainMenu.tsx
@@ -0,0 +1,87 @@
+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;
+ &:hover {
+ text-decoration: underline;
+ cursor: pointer;
+ }
+`
+
+const LoanOption = ({ loan }: LoanOptionProps) => {
+ 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) => (
+
+
+
+ )}
+ />
+ )
+}
diff --git a/centrifuge-app/src/pages/Pool/Assets/index.tsx b/centrifuge-app/src/pages/Pool/Assets/index.tsx
index e7345950b7..41c735f452 100644
--- a/centrifuge-app/src/pages/Pool/Assets/index.tsx
+++ b/centrifuge-app/src/pages/Pool/Assets/index.tsx
@@ -1,12 +1,12 @@
-import { ActiveLoan, Loan } from '@centrifuge/centrifuge-js'
-import { Box, Shelf, Text } from '@centrifuge/fabric'
+import { CurrencyBalance, Loan } from '@centrifuge/centrifuge-js'
+import { Box, IconChevronRight, 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 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'
@@ -16,6 +16,16 @@ 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;
+ &:hover {
+ text-decoration: underline;
+ }
+`
export function PoolDetailAssetsTab() {
return (
@@ -36,6 +46,10 @@ export function PoolDetailAssets() {
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,18 +62,18 @@ export function PoolDetailAssets() {
)
}
- function hasValuationMethod(pricing: any): pricing is { valuationMethod: string } {
+ const 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 totalAssets = loans.reduce((sum, loan) => {
+ 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))
const offchainAssets = !isTinlakePool
? loans.filter(
@@ -71,48 +85,41 @@ 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 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 }[] = [
+ const pageSummaryData: { label: React.ReactNode; value: React.ReactNode; heading?: boolean }[] = [
{
- label: ,
- value: formatBalance(pool.nav.total.toDecimal(), pool.currency.symbol),
+ label: `Total NAV (${pool.currency.symbol})`,
+ value: formatBalance(totalNAV),
+ heading: true,
},
{
- label: (
-
-
-
-
+ label: ,
+ value: (
+
+ {formatBalance(pool.reserve.total || 0)}
+
+
),
- 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: `Total Assets (${pool.currency.symbol})`,
+ value: formatBalance(totalAssets),
+ heading: false,
},
- { label: , value: ongoingAssets.length || 0 },
{
- label: 'Overdue assets',
- value: 0 ? 'statusCritical' : 'inherit'}>{overdueAssets.length},
+ label: `Accrued fees (${pool.currency.symbol})`,
+ value: `-${formatBalance(pool.fees.totalPaid)}`,
+ heading: false,
},
]
: []),
@@ -123,7 +130,7 @@ export function PoolDetailAssets() {
-
+
>
@@ -134,8 +141,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/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts
index e8aa7c9efd..cc995d9d89 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/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
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/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/InlineFeedback/index.tsx b/fabric/src/components/InlineFeedback/index.tsx
index d7d6f4d0ab..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}
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 (
+
- {label}
- {sublabel}
+
+ {label}
+
+
+ {sublabel}
+
{action}
diff --git a/fabric/src/components/Tooltip/index.tsx b/fabric/src/components/Tooltip/index.tsx
index 725f0e8455..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"
@@ -116,7 +125,7 @@ export function Tooltip({
pointer={pointer}
>
{!!title && (
-
+
{title}
)}
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 @@
-