Skip to content

Commit

Permalink
Pool fees feedback (#1975)
Browse files Browse the repository at this point in the history
* Add copy to clipboard to receiving address in table

* Add total pending fees to page summary

* Show action column only to receiving address and pool admins

* Use maxPayable as amount for non fixed charges

* Add fee position column and numbered fees

* Fix disabled dropdown label

* Rename protocol fees

* Add tooltip to fee type label

* Add fee position to metadata and update label of fixed fee input

* Ajust edit fees drawer

* Rename title in charge fees drawer

* Remove log

* Add * to required fields in create pool form

* Update centrifuge-app/src/pages/IssuerCreatePool/PoolFeeInput.tsx

* Fix fixed pending fees

* Prettier

* Fix reserve in chart

---------

Co-authored-by: JP <[email protected]>
  • Loading branch information
sophialittlejohn and JP authored Feb 27, 2024
1 parent e49bf01 commit a3d9cba
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 154 deletions.
4 changes: 2 additions & 2 deletions centrifuge-app/src/components/PoolFees/ChargeFeesDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const ChargeFeesDrawer = ({ onClose, isOpen }: ChargeFeesProps) => {
}
return errors
},
onSubmit: (values, actions) => {
onSubmit: (values) => {
if (!feeIndex) throw new Error('feeIndex not found')
if (!values.amount) throw new Error('amount not found')
chargeFeeTx([
Expand Down Expand Up @@ -87,7 +87,7 @@ export const ChargeFeesDrawer = ({ onClose, isOpen }: ChargeFeesProps) => {
</Text>
</Stack>
<Stack gap="4px">
<Text variant="label2">Limit</Text>
<Text variant="label2">Max fees</Text>
<Text variant="body3">{`${formatPercentage(
feeChainData?.amounts.percentOfNav.toPercent() || 0
)} of NAV`}</Text>
Expand Down
5 changes: 3 additions & 2 deletions centrifuge-app/src/components/PoolFees/EditFeesDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ import {
import { Field, FieldArray, FieldProps, Form, FormikProvider, useFormik } from 'formik'
import React from 'react'
import { useParams } from 'react-router'
import { Dec } from '../../utils/Decimal'
import { isEvmAddress, isSubstrateAddress } from '../../utils/address'
import { copyToClipboard } from '../../utils/copyToClipboard'
import { Dec } from '../../utils/Decimal'
import { formatPercentage } from '../../utils/formatting'
import { usePoolAdmin, useSuitableAccounts } from '../../utils/usePermissions'
import { usePool, usePoolMetadata } from '../../utils/usePools'
Expand Down Expand Up @@ -145,9 +145,10 @@ export const EditFeesDrawer = ({ onClose, isOpen }: ChargeFeesProps) => {
destination: fee.receivingAddress,
amount: Rate.fromPercent(Dec(fee?.percentOfNav || 0)),
feeId: fee.feeId,
type: 'ChargedUpTo',
feeType: 'chargedUpTo',
limit: 'ShareOfPortfolioValuation',
account: account.actingAddress,
feePosition: 'Top of waterfall',
},
}
})
Expand Down
167 changes: 111 additions & 56 deletions centrifuge-app/src/components/PoolFees/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { addressToHex, Rate, TokenBalance } from '@centrifuge/centrifuge-js'
import { addressToHex, CurrencyBalance, Rate, TokenBalance } from '@centrifuge/centrifuge-js'
import { useAddress, useCentrifugeQuery, useCentrifugeTransaction } from '@centrifuge/centrifuge-react'
import { Box, Button, IconCheckInCircle, IconSwitch, Shelf, Text, truncate } from '@centrifuge/fabric'
import { Box, Button, IconCheckInCircle, IconSwitch, Shelf, Text } from '@centrifuge/fabric'
import { BN } from 'bn.js'
import * as React from 'react'
import { useHistory, useLocation, useParams } from 'react-router'
import { CopyToClipboard } from '../../utils/copyToClipboard'
import { formatBalance, formatPercentage } from '../../utils/formatting'
import { usePoolAdmin } from '../../utils/usePermissions'
import { usePool, usePoolMetadata } from '../../utils/usePools'
import { DataTable } from '../DataTable'
import { PageSection } from '../PageSection'
import { PageSummary } from '../PageSummary'
import { RouterLinkButton } from '../RouterLinkButton'
import { Tooltips } from '../Tooltips'
import { ChargeFeesDrawer } from './ChargeFeesDrawer'
import { EditFeesDrawer } from './EditFeesDrawer'

Expand All @@ -20,59 +24,10 @@ type Row = {
receivingAddress?: string
action: null | React.ReactNode
poolCurrency?: string
index: number
feePosition: 'Top of waterfall'
}

const columns = [
{
align: 'left',
header: 'Name',
cell: (row: Row) => {
return <Text variant="body3">{row.name}</Text>
},
},
{
align: 'left',
header: 'Type',
cell: (row: Row) => {
return <Text variant="body3">{row.type === 'fixed' ? 'Fixed % of NAV' : 'Direct charge'}</Text>
},
},
{
align: 'left',
header: 'Percentage',
cell: (row: Row) => {
return (
<Text variant="body3">
{row.percentOfNav ? `${formatPercentage(row.percentOfNav?.toPercent(), true, {}, 3)} of NAV` : ''}
</Text>
)
},
},
{
align: 'left',
header: 'Pending fees',
cell: (row: Row) => {
return row?.pendingFees ? (
<Text variant="body3">{formatBalance(row.pendingFees, row.poolCurrency, 2)}</Text>
) : null
},
},
{
align: 'left',
header: 'Receiving address',
cell: (row: Row) => {
return <Text variant="body3">{row.receivingAddress ? truncate(row.receivingAddress) : ''}</Text>
},
},
{
align: 'left',
header: 'Action',
cell: (row: Row) => {
return row.action
},
},
]

export function PoolFees() {
const { pid: poolId } = useParams<{ pid: string }>()
const pool = usePool(poolId)
Expand All @@ -88,21 +43,104 @@ export function PoolFees() {
const address = useAddress()
const { execute: applyNewFee } = useCentrifugeTransaction('Apply new fee', (cent) => cent.pools.applyNewFee)

const columns = [
{
align: 'left',
header: 'Name',
cell: (row: Row) => {
return (
<Shelf gap={1}>
<Box
borderRadius="50%"
height="16px"
width="16px"
backgroundColor="backgroundSecondary"
display="flex"
justifyContent="center"
alignItems="center"
>
<Text variant="body3">{row.index + 1}</Text>
</Box>
<Text variant="body3">{row.name}</Text>
</Shelf>
)
},
},
{
align: 'left',
header: 'Type',
cell: (row: Row) => {
return <Text variant="body3">{row.type === 'fixed' ? 'Fixed % of NAV' : 'Direct charge'}</Text>
},
},
{
align: 'left',
header: 'Fee position',
cell: (row: Row) => {
return <Text variant="body3">{row.feePosition}</Text>
},
},
{
align: 'left',
header: 'Percentage/Limit',
cell: (row: Row) => {
return row.percentOfNav ? (
<Text variant="body3">
{row.type === 'fixed'
? `${formatPercentage(row.percentOfNav.toPercent(), true, {}, 3)} of NAV`
: `<${formatPercentage(row.percentOfNav.toPercent(), true, {}, 3)} of non-cash NAV`}
</Text>
) : null
},
},
{
align: 'left',
header: 'Pending fees',
cell: (row: Row) => {
return row?.pendingFees ? (
<Text variant="body3">{formatBalance(row.pendingFees, row.poolCurrency, 4)}</Text>
) : null
},
},
{
align: 'left',
header: 'Receiving address',
cell: (row: Row) => {
return (
<Text variant="body3">
<CopyToClipboard variant="body3" address={row.receivingAddress || ''} />
</Text>
)
},
},
...(!!poolAdmin || pool?.poolFees?.map((fee) => addressToHex(fee.destination)).includes(address! as `0x${string}`)
? [
{
align: 'left',
header: 'Action',
cell: (row: Row) => row.action,
},
]
: []),
]

const data = React.useMemo(() => {
const activeFees =
pool.poolFees
?.filter((feeChainData) => poolMetadata?.pool?.poolFees?.find((f) => f.id === feeChainData.id))
?.map((feeChainData) => {
?.map((feeChainData, index) => {
const feeMetadata = poolMetadata?.pool?.poolFees?.find((f) => f.id === feeChainData.id)
const fixedFee = feeChainData?.type === 'fixed'
const isAllowedToCharge = feeChainData?.destination && addressToHex(feeChainData.destination) === address

return {
name: feeMetadata!.name,
index,
name: feeMetadata?.name,
type: feeChainData?.type,
percentOfNav: feeChainData?.amounts?.percentOfNav,
pendingFees: feeChainData?.amounts.pending,
receivingAddress: feeChainData?.destination,
feePosition: feeMetadata?.feePosition,
action:
(isAllowedToCharge || poolAdmin) && !fixedFee ? (
<RouterLinkButton
Expand All @@ -128,8 +166,9 @@ export function PoolFees() {
if (changes?.length) {
return [
...activeFees,
...changes.map(({ change, hash }) => {
...changes.map(({ change, hash }, index) => {
return {
index: activeFees.length + index,
name: poolMetadata?.pool?.poolFees?.find((f) => f.id === change.feeId)?.name,
type: change.type,
percentOfNav: change.amounts.percentOfNav,
Expand Down Expand Up @@ -166,6 +205,20 @@ export function PoolFees() {
}
}, [drawer])

const pageSummaryData: { label: React.ReactNode; value: React.ReactNode }[] = [
{
label: <Tooltips type="totalPendingFees" />,
value: formatBalance(
new CurrencyBalance(
pool.poolFees?.reduce((acc, fee) => acc.add(fee.amounts.pending), new BN(0)) || new BN(0),
pool.currency.decimals
) || 0,
pool.currency.symbol,
2
),
},
]

return (
<>
<ChargeFeesDrawer
Expand All @@ -182,6 +235,7 @@ export function PoolFees() {
push(pathname)
}}
/>
<PageSummary data={pageSummaryData} />
<PageSection
title="Fee structure"
headerRight={
Expand All @@ -191,6 +245,7 @@ export function PoolFees() {
</RouterLinkButton>
) : null
}
subtitle="Fees are settled using available liquidity before investments or redemptions, prioritizing and paying the highest fees first"
>
{data?.length ? (
<DataTable data={data || []} columns={columns} />
Expand Down
38 changes: 25 additions & 13 deletions centrifuge-app/src/components/Tooltips.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Text, TextProps, Tooltip as FabricTooltip } from '@centrifuge/fabric'
import { Tooltip as FabricTooltip, Text, TextProps } from '@centrifuge/fabric'
import * as React from 'react'
import { useParams } from 'react-router'
import { usePool } from '../utils/usePools'
Expand Down Expand Up @@ -262,18 +262,30 @@ export const tooltipText = {
label: 'Pool type',
body: 'An open pool can have multiple unrelated token holders and can onboard third party investors. A closed pool has very limited distributions and is not available for investment on the app.',
},
totalNav: {
label: "Total NAV",
body: "The total Net Asset Value (NAV) reflects the combined present value of assets, cash held in the onchain reserve of the pool, and cash in the bank account designated as offchain cash."
} ,
onchainReserve: {
label: "Onchain reserve",
body: "The onchain reserve represents the amount of available liquidity in the pool available for asset originations and redemptions."
},
offchainCash: {
label: "Offchain cash",
body: "Offchain cash represents funds held in a traditional bank account or custody account."
}
totalPendingFees: {
label: 'Total pending fees',
body: 'The total pending fees represent the sum of all added fees.',
},
feeType: {
label: 'Fee type',
body: 'The protocol fee is mandatory and will be charged every epoch automatically. The fee amount is dependent on the asset class.',
},
feePosition: {
label: 'Fee position',
body: 'Fees are settled using available liquidity before investments or redemptions, prioritizing and paying the highest fees first.',
},
totalNav: {
label: 'Total NAV',
body: 'The total Net Asset Value (NAV) reflects the combined present value of assets, cash held in the onchain reserve of the pool, and cash in the bank account designated as offchain cash.',
},
onchainReserve: {
label: 'Onchain reserve',
body: 'The onchain reserve represents the amount of available liquidity in the pool available for asset originations and redemptions.',
},
offchainCash: {
label: 'Offchain cash',
body: 'Offchain cash represents funds held in a traditional bank account or custody account.',
},
}

export type TooltipsProps = {
Expand Down
Loading

0 comments on commit a3d9cba

Please sign in to comment.