diff --git a/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx b/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx index 333ef7ea7..507c104ae 100644 --- a/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx +++ b/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx @@ -10,6 +10,7 @@ import { daysBetween, formatDate } from '../../utils/date' import { formatBalance, formatBalanceAbbreviated, formatPercentage } from '../../utils/formatting' import { useLoans } from '../../utils/useLoans' import { useDailyPoolStates, usePool } from '../../utils/usePools' +import { DYF_POOL_ID } from '../PoolCard' import { Tooltips, tooltipText } from '../Tooltips' import { TooltipContainer, TooltipTitle } from './Tooltip' import { getOneDayPerMonth, getRangeNumber } from './utils' @@ -160,7 +161,7 @@ function PoolPerformanceChart() { nav: todayAssetValue, juniorTokenPrice: tranchePrices.juniorTokenPrice ?? 0, seniorTokenPrice: tranchePrices.seniorTokenPrice ?? null, - juniorAPY: pool.id === '1655476167' ? 15 : todayJuniorApy, + juniorAPY: pool.id === DYF_POOL_ID ? 15 : todayJuniorApy, seniorAPY: todaySeniorApy, } } @@ -181,7 +182,7 @@ function PoolPerformanceChart() { nav: todayAssetValue, price: todayPrice, currency: pool.currency.symbol, - juniorAPY: pool.id === '1655476167' ? 15 : todayJuniorApy, + juniorAPY: pool.id === DYF_POOL_ID ? 15 : todayJuniorApy, seniorAPY: todaySeniorApy, ...trancheTodayPrice, } diff --git a/centrifuge-app/src/components/IssuerSection.tsx b/centrifuge-app/src/components/IssuerSection.tsx index be76a0e04..d18150fc2 100644 --- a/centrifuge-app/src/components/IssuerSection.tsx +++ b/centrifuge-app/src/components/IssuerSection.tsx @@ -221,27 +221,46 @@ const Links = ({ links }: { links: { label: string; href?: string; show: boolean } export function RatingDetails({ metadata }: IssuerSectionProps) { - const rating = metadata?.pool?.rating + const ratings = metadata?.pool?.poolRatings + const cent = useCentrifuge() - return rating?.ratingAgency || rating?.ratingValue || rating?.ratingReportUrl ? ( + return ratings?.length ? ( Pool rating - - - {rating.ratingAgency && ( - {rating.ratingAgency}} /> - )} - {rating.ratingValue && ( - {rating.ratingValue}} /> - )} - - - - {rating?.ratingReportUrl && ( - - View full report - - )} + + {ratings?.map((rating) => { + return ( + + + + {rating.agency && ( + {rating.agency}} /> + )} + {rating.value && ( + {rating.value}} /> + )} + + + + {rating?.reportUrl && ( + + View full report + + )} + {rating?.reportFile && ( + + Download report + + )} + + + ) + })} ) : null diff --git a/centrifuge-app/src/components/PoolCard/index.tsx b/centrifuge-app/src/components/PoolCard/index.tsx index 3e0c17542..f861de872 100644 --- a/centrifuge-app/src/components/PoolCard/index.tsx +++ b/centrifuge-app/src/components/PoolCard/index.tsx @@ -25,8 +25,6 @@ export type MetaData = { } } -type TinlakeTranchesKey = 'silver' | 'blocktowerThree' | 'blocktowerFour' - type TrancheWithCurrency = Pick const StyledRouterTextLink = styled(RouterTextLink)` @@ -58,32 +56,55 @@ const StyledCard = styled(Card)` ` const tinlakeTranches = { - silver: { - Junior: '15%', - Senior: '7%', + NS2: { + name: 'New Silver 3', + tranches: [ + { name: 'Junior', apr: '15%', minInvestment: '-' }, + { name: 'Senior', apr: '7%', minInvestment: '5K' }, + ], shortDescription: ' Real estate bridge loans for fix and flip projects, maturing in 12-24 months.', - InvestorType: 'Qualified Investors', + investorType: 'Qualified Investors', }, - blocktowerThree: { - Junior: '15%', - Senior: '4%', + BT3: { + name: 'BlockTower Series 3', + tranches: [ + { name: 'Junior', apr: '15%', minInvestment: '-' }, + { name: 'Senior', apr: '4%', minInvestment: '-' }, + ], shortDescription: ' Investment-grade consumer ABS, auto ABS, and CLOs under 4 years.', - InvestorType: 'Private', + investorType: 'Private', }, - blocktowerFour: { - Junior: '15%', - Senior: '4%', + BT4: { + name: 'BlockTower Series 4', + tranches: [ + { name: 'Junior', apr: '15%', minInvestment: '-' }, + { name: 'Senior', apr: '4%', minInvestment: '-' }, + ], shortDescription: 'Investment-grade consumer ABS, auto ABS, and CLOs under 4 years.', - InvestorType: 'Private', + investorType: 'Private', }, none: { - Junior: '-', - Senior: '-', + name: '-', + tranches: [ + { name: 'Junior', apr: '-', minInvestment: '-' }, + { name: 'Senior', apr: '-', minInvestment: '-' }, + ], shortDescription: '', - InvestorType: '-', + investorType: '-', }, } +export const DYF_POOL_ID = '1655476167' +export const NS3_POOL_ID = '1615768079' + +export type CentrifugeTargetAPYs = keyof typeof centrifugeTargetAPYs +export const centrifugeTargetAPYs = { + [DYF_POOL_ID]: ['15%'], + [NS3_POOL_ID]: ['8.0%', '16%'], +} + +type TinlakeTranchesKey = keyof typeof tinlakeTranches + export type PoolCardProps = { poolId?: string name?: string @@ -112,25 +133,14 @@ export function PoolCard({ }: PoolCardProps) { const theme = useTheme() const isOneTranche = tranches && tranches?.length === 1 - const isTinlakePool = - poolId === '0x53b2d22d07E069a3b132BfeaaD275b10273d381E' || - poolId === '0x90040F96aB8f291b6d43A8972806e977631aFFdE' || - poolId === '0x55d86d51Ac3bcAB7ab7d2124931FbA106c8b60c7' - - const tinlakeObjKey = () => { - if (name?.includes('Silver')) return 'silver' - else if (name?.includes('BlockTower Series 3')) return 'blocktowerThree' - else if (name?.includes('BlockTower Series 4')) return 'blocktowerFour' - else return 'none' - } + const isTinlakePool = poolId?.startsWith('0x') - const getTinlakeMinInvestment = (trancheName: 'Junior' | 'Senior') => { - if (name?.includes('Silver') && trancheName === 'Senior') return '5K' - else return '-' - } + const tinlakeKey = (Object.keys(tinlakeTranches).find( + (key) => tinlakeTranches[key as TinlakeTranchesKey].name === name + ) || 'none') as TinlakeTranchesKey const renderText = (text: string, isApr?: boolean) => { - if (isApr && poolId === '1615768079') { + if (isApr && poolId === NS3_POOL_ID) { return ( @@ -151,9 +161,9 @@ export function PoolCard({ const calculateApy = (tranche: TrancheWithCurrency) => { const daysSinceCreation = createdAt ? daysBetween(createdAt, new Date()) : 0 - if (poolId === '1655476167') return '15%' - if (poolId === '1615768079' && tranche.seniority === 0) return '8.0%' - if (poolId === '1615768079' && tranche.seniority === 1) return '16%' + 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) { @@ -165,7 +175,6 @@ export function PoolCard({ const tranchesData = useMemo(() => { return tranches ?.map((tranche: TrancheWithCurrency) => { - const key = tinlakeObjKey() as TinlakeTranchesKey const words = tranche.currency.name.trim().split(' ') const metadata = metaData?.tranches[tranche.id] ?? null const trancheName = words[words.length - 1] @@ -176,16 +185,18 @@ export function PoolCard({ return { name: trancheName, - apr: isTinlakePool ? tinlakeTranches[key][trancheName as 'Junior' | 'Senior'] : calculateApy(tranche), + apr: isTinlakePool + ? tinlakeTranches[tinlakeKey].tranches.find((t) => t.name === trancheName)?.apr + : calculateApy(tranche), minInvestment: isTinlakePool - ? getTinlakeMinInvestment(trancheName as 'Junior' | 'Senior') + ? tinlakeTranches[tinlakeKey].tranches.find((t) => t.name === trancheName)?.minInvestment : metadata && metadata.minInitialInvestment ? `$${formatBalanceAbbreviated(investmentBalance, '', 0)}` : '-', } }) .reverse() - }, [calculateApy, getTinlakeMinInvestment, isTinlakePool, metaData?.tranches, tinlakeObjKey, tranches]) + }, [calculateApy, isTinlakePool, metaData?.tranches, tinlakeKey, tranches]) return ( @@ -240,7 +251,7 @@ export function PoolCard({ )} - {poolId === '1655476167' ? 'Target' : 'APY'} + {poolId === DYF_POOL_ID ? 'Target' : 'APY'} {tranchesData?.map((tranche) => renderText(`${tranche.apr}`, true))} @@ -256,7 +267,7 @@ export function PoolCard({ {isTinlakePool - ? tinlakeTranches[tinlakeObjKey()].shortDescription + ? tinlakeTranches[tinlakeKey].shortDescription : metaData?.pool?.issuer?.shortDescription} @@ -269,7 +280,7 @@ export function PoolCard({ Investor type {' '} - {isTinlakePool ? tinlakeTranches[tinlakeObjKey()].InvestorType : metaData?.pool?.investorType ?? '-'} + {isTinlakePool ? tinlakeTranches[tinlakeKey].investorType : metaData?.pool?.investorType ?? '-'} diff --git a/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx b/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx index cd5f263b6..f36c050f6 100644 --- a/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx +++ b/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx @@ -1,5 +1,5 @@ import { CurrencyBalance, DailyTrancheState, Price } from '@centrifuge/centrifuge-js' -import { NetworkIcon, formatBalanceAbbreviated } from '@centrifuge/centrifuge-react' +import { NetworkIcon, formatBalanceAbbreviated, useCentrifuge } from '@centrifuge/centrifuge-react' import { Box, Card, IconArrowRightWhite, IconMoody, IconSp, Shelf, Stack, Text, Tooltip } from '@centrifuge/fabric' import capitalize from 'lodash/capitalize' import startCase from 'lodash/startCase' @@ -10,6 +10,7 @@ import { formatBalance, formatPercentage } from '../../utils/formatting' import { useAverageMaturity } from '../../utils/useAverageMaturity' import { useActiveDomains } from '../../utils/useLiquidityPools' import { useDailyTranchesStates, usePool, usePoolFees, usePoolMetadata } from '../../utils/usePools' +import { centrifugeTargetAPYs } from '../PoolCard' import { PoolStatus } from '../PoolCard/PoolStatus' import { getPoolStatus } from '../PoolList' import { Spinner } from '../Spinner' @@ -32,10 +33,7 @@ type Tranche = Pick & { } } -type TinlakeDataKey = - | '0x53b2d22d07E069a3b132BfeaaD275b10273d381E' - | '0x55d86d51Ac3bcAB7ab7d2124931FbA106c8b60c7' - | '0x90040F96aB8f291b6d43A8972806e977631aFFdE' +type TinlakeDataKey = keyof typeof tinlakeData const tinlakeData = { '0x53b2d22d07E069a3b132BfeaaD275b10273d381E': '7% - 15% target', @@ -77,6 +75,7 @@ export const KeyMetrics = ({ poolId }: Props) => { const tranchesIds = pool.tranches.map((tranche) => tranche.id) const dailyTranches = useDailyTranchesStates(tranchesIds) const theme = useTheme() + const cent = useCentrifuge() const averageMaturity = useAverageMaturity(poolId) const expenseRatio = useMemo(() => { @@ -107,11 +106,6 @@ export const KeyMetrics = ({ poolId }: Props) => { }) }, [metadata?.tranches, pool.currency.decimals]) - const getHardCodedApy = () => { - if (poolId === '1655476167') return '15%' - if (poolId === '1615768079') return '8% - 16%' - } - const isBT3BT4 = poolId === '0x53b2d22d07E069a3b132BfeaaD275b10273d381E' || poolId === '0x90040F96aB8f291b6d43A8972806e977631aFFdE' || @@ -123,11 +117,11 @@ export const KeyMetrics = ({ poolId }: Props) => { value: `${capitalize(startCase(metadata?.pool?.asset?.class))} - ${metadata?.pool?.asset?.subClass}`, }, { - metric: poolId === '1655476167' || poolId === '1615768079' ? 'Target APY' : '30-day APY', + metric: centrifugeTargetAPYs[poolId as keyof typeof centrifugeTargetAPYs] ? 'Target APY' : '30-day APY', value: tinlakeData[poolId as TinlakeDataKey] ? tinlakeData[poolId as TinlakeDataKey] - : poolId === '1655476167' || poolId === '1615768079' - ? getHardCodedApy() + : centrifugeTargetAPYs[poolId as keyof typeof centrifugeTargetAPYs] + ? centrifugeTargetAPYs[poolId as keyof typeof centrifugeTargetAPYs].join(' - ') : tranchesAPY?.length ? tranchesAPY.map((tranche, index) => { const formatted = formatPercentage(tranche) @@ -171,36 +165,45 @@ export const KeyMetrics = ({ poolId }: Props) => { metric: 'Pool structure', value: isBT3BT4 ? 'Revolving' : metadata?.pool?.poolStructure ?? '-', }, - ...(metadata?.pool?.rating?.ratingValue + ...(metadata?.pool?.poolRatings?.length ? [ { metric: 'Rating', value: ( - - } - > - - {metadata?.pool?.rating?.ratingAgency?.includes('moody') ? ( - - ) : ( - - )} - {metadata?.pool?.rating?.ratingValue} - - + + {metadata?.pool?.poolRatings.map((rating) => ( + + } + > + + {rating.agency?.includes('moody') ? : } + {rating.value} + + + ))} + ), }, ] @@ -259,21 +262,26 @@ const TooltipBody = ({ {links ? ( links.map((link, index) => ( - + + + + {link.text} + + + + + )) + ) : ( + + {subtitle} - )) - ) : ( - - - {subtitle} - - + + )} - ) } diff --git a/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx b/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx index 024f8bb18..5c9a84602 100644 --- a/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx +++ b/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx @@ -7,6 +7,7 @@ import { daysBetween } from '../../utils/date' import { formatBalance, formatPercentage } from '../../utils/formatting' import { usePool } from '../../utils/usePools' import { DataTable } from '../DataTable' +import { CentrifugeTargetAPYs, DYF_POOL_ID, NS3_POOL_ID, centrifugeTargetAPYs } from '../PoolCard' import { PoolMetaDataPartial } from '../PoolList' export const TrancheTokenCards = ({ @@ -32,9 +33,11 @@ export const TrancheTokenCards = ({ const columnConfig = useMemo(() => { const calculateApy = (trancheToken: Token) => { if (isTinlakePool && getTrancheText(trancheToken) === 'senior') return formatPercentage(trancheToken.apy) - if (poolId === '1655476167') return '15%' - if (poolId === '1615768079' && trancheToken.seniority === 0) return '8%' - if (poolId === '1615768079' && trancheToken.seniority === 1) return '16%' + 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)) @@ -49,7 +52,7 @@ export const TrancheTokenCards = ({ width: '40%', }, { - header: poolId === '1655476167' || poolId === '1615768079' ? 'Target' : 'APY', + header: poolId === DYF_POOL_ID || poolId === NS3_POOL_ID ? 'Target' : 'APY', align: 'left', formatter: (v: any) => (v ? calculateApy(v) : '-'), }, diff --git a/centrifuge-app/src/pages/IssuerCreatePool/PoolRatingInput.tsx b/centrifuge-app/src/pages/IssuerCreatePool/PoolRatingInput.tsx index c46c6c94d..b78183697 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/PoolRatingInput.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/PoolRatingInput.tsx @@ -1,20 +1,74 @@ -import { Grid, Stack, Text, TextInput } from '@centrifuge/fabric' +import { PoolMetadataInput } from '@centrifuge/centrifuge-js' +import { Box, Button, FileUpload, Grid, IconMinusCircle, Shelf, Stack, Text, TextInput } from '@centrifuge/fabric' +import { Field, FieldArray, FieldProps, useFormikContext } from 'formik' import { FieldWithErrorMessage } from '../../components/FieldWithErrorMessage' export function PoolRatingInput() { + const form = useFormikContext() return ( - - Pool rating - - - - - - + + {({ push, remove }) => ( + + + Pool rating + + + + {form.values.poolRatings.length + ? form.values.poolRatings.map((rating, index) => ( + + <> + + + + + + {({ field, meta, form }: FieldProps) => ( + { + form.setFieldTouched(`poolRatings.${index}.reportFile`, true, false) + form.setFieldValue(`poolRatings.${index}.reportFile`, file) + }} + accept="application/pdf" + label="Report PDF" + placeholder="Choose file" + errorMessage={meta.touched && meta.error ? meta.error : undefined} + /> + )} + + + + +