Skip to content

Commit

Permalink
Pool fees follow up (#1933)
Browse files Browse the repository at this point in the history
* Fix button spacing in holdings

* Update font color of tertiary button

* Use tertiary buttons for poolfees table

* Show actions only to allowed viewers

* Handle cancel better on update fee charge

* Update label

* Add pool type input to create pool

* Add validation for new input

* Set correct default fees based on poolType

* Add ~ to pending fees

* Add pending fees for fixed fees

* Fix tilde logic

* Fix balance types

* Account for epoch time if limit is amountPerSecond

* Use last nav for pending fee calculation

* Clean up naming

* Remove ~ and add pending fees to fixed fee calc

* Fix input jumping

* Fix multiply by zero on first epoch where previous nav is 0

* Remove logs

* Add type decorations for nav runtime api

* Fix PoolNav types

* Fetch nav value from runtime instead of chain storage

* Fix decimal error in fees
  • Loading branch information
sophialittlejohn authored Feb 12, 2024
1 parent 3ef8453 commit fde7c31
Show file tree
Hide file tree
Showing 12 changed files with 401 additions and 288 deletions.
17 changes: 13 additions & 4 deletions centrifuge-app/src/components/PoolFees/ChargeFeesDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export const ChargeFeesDrawer = ({ onClose, isOpen }: ChargeFeesProps) => {
<Stack gap="4px">
<Text variant="label2">Limit</Text>
<Text variant="body3">{`${formatPercentage(
feeChainData?.amounts.percentOfNav.toDecimal() || 0
feeChainData?.amounts.percentOfNav.toPercent() || 0
)} of NAV`}</Text>
</Stack>
<Stack gap="4px">
Expand All @@ -113,8 +113,8 @@ export const ChargeFeesDrawer = ({ onClose, isOpen }: ChargeFeesProps) => {
<Shelf alignItems="flex-start" gap={1}>
<IconInfo size="16px" />
<Text variant="body3" color="textSecondary">
Fee charges have been placed. Charging of fees will be finalized by the issuer of the pool when
executing orders.
Fee charges have been placed. Fees will be paid when orders are executed and sufficient liquidity is
available.
</Text>
</Shelf>
</Stack>
Expand Down Expand Up @@ -163,7 +163,16 @@ export const ChargeFeesDrawer = ({ onClose, isOpen }: ChargeFeesProps) => {
>
{updateCharge ? 'Update c' : 'C'}harge
</Button>
<Button variant="secondary" onClick={onClose}>
<Button
variant="secondary"
onClick={() => {
if (updateCharge) {
setUpdateCharge(false)
} else {
onClose()
}
}}
>
Cancel
</Button>
</ButtonGroup>
Expand Down
10 changes: 5 additions & 5 deletions centrifuge-app/src/components/PoolFees/EditFeesDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export const EditFeesDrawer = ({ onClose, isOpen }: ChargeFeesProps) => {
fee: {
name: fee.feeName,
destination: fee.receivingAddress,
amount: Rate.fromFloat(Dec(fee?.percentOfNav || 0)),
amount: Rate.fromPercent(Dec(fee?.percentOfNav || 0)),
feeId: fee.feeId,
type: 'ChargedUpTo',
limit: 'ShareOfPortfolioValuation',
Expand Down Expand Up @@ -182,7 +182,7 @@ export const EditFeesDrawer = ({ onClose, isOpen }: ChargeFeesProps) => {
{feeMetadata?.name}
</Text>
<Text variant="body2" color="textSecondary">
{formatPercentage(feeChainData?.amounts.percentOfNav.toDecimal() || 0, true, {}, 3)} of NAV
{formatPercentage(feeChainData?.amounts.percentOfNav.toPercent() || 0, true, {}, 3)} of NAV
</Text>
</Grid>
)
Expand All @@ -203,9 +203,9 @@ export const EditFeesDrawer = ({ onClose, isOpen }: ChargeFeesProps) => {
</Text>
{form.values.poolFees.map((values, index) => {
return (
<Shelf key={`poolFees.${index}`} alignItems="center" gap={4}>
<Shelf key={`poolFees.${index}`} gap={4}>
<Stack gap={2} borderBottom="0.5px solid borderPrimary" pb={3} maxWidth="350px">
<Shelf gap={2}>
<Shelf gap={2} alignItems="flex-start">
<Field name={`poolFees.${index}.feeName`}>
{({ field, meta }: FieldProps) => {
return (
Expand All @@ -223,7 +223,7 @@ export const EditFeesDrawer = ({ onClose, isOpen }: ChargeFeesProps) => {
return (
<NumberInput
{...field}
label="Current percentage"
label="Max fees in % of NAV"
symbol="%"
disabled={!poolAdmin || updateFeeTxLoading}
errorMessage={(meta.touched && meta.error) || ''}
Expand Down
75 changes: 39 additions & 36 deletions centrifuge-app/src/components/PoolFees/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { Rate, TokenBalance } from '@centrifuge/centrifuge-js'
import { useCentrifugeQuery, useCentrifugeTransaction } from '@centrifuge/centrifuge-react'
import { Shelf, Text, truncate } from '@centrifuge/fabric'
import { addressToHex, 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 * as React from 'react'
import { useHistory, useLocation, useParams } from 'react-router'
import { NavLink } from 'react-router-dom'
import styled from 'styled-components'
import { formatBalance, formatPercentage } from '../../utils/formatting'
import { usePoolAdmin } from '../../utils/usePermissions'
import { usePool, usePoolMetadata } from '../../utils/usePools'
Expand Down Expand Up @@ -45,7 +43,7 @@ const columns = [
cell: (row: Row) => {
return (
<Text variant="body3">
{row.percentOfNav ? `${formatPercentage(row.percentOfNav?.toDecimal(), true, {}, 3)} of NAV` : ''}
{row.percentOfNav ? `${formatPercentage(row.percentOfNav?.toPercent(), true, {}, 3)} of NAV` : ''}
</Text>
)
},
Expand All @@ -54,7 +52,9 @@ const columns = [
align: 'left',
header: 'Pending fees',
cell: (row: Row) => {
return <Text variant="body3">{row.pendingFees ? formatBalance(row.pendingFees, row.poolCurrency, 2) : ''}</Text>
return row?.pendingFees ? (
<Text variant="body3">{formatBalance(row.pendingFees, row.poolCurrency, 2)}</Text>
) : null
},
},
{
Expand All @@ -68,7 +68,7 @@ const columns = [
align: 'left',
header: 'Action',
cell: (row: Row) => {
return <Text variant="body3">{row.action}</Text>
return row.action
},
},
]
Expand All @@ -85,6 +85,7 @@ export function PoolFees() {
const drawer = params.get('charge')
const changes = useProposedFeeChanges(poolId)
const poolAdmin = usePoolAdmin(poolId)
const address = useAddress()
const { execute: applyNewFee } = useCentrifugeTransaction('Apply new fee', (cent) => cent.pools.applyNewFee)

const data = React.useMemo(() => {
Expand All @@ -94,17 +95,27 @@ export function PoolFees() {
?.map((feeChainData) => {
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,
type: feeChainData?.type,
percentOfNav: feeChainData?.amounts?.percentOfNav,
pendingFees: fixedFee ? null : feeChainData?.amounts.pending,
pendingFees: feeChainData?.amounts.pending,
receivingAddress: feeChainData?.destination,
action: fixedFee ? null : (
<StyledLink to={`?charge=${feeChainData?.id}`}>
<Text variant="body3">Charge</Text>
</StyledLink>
),
action:
(isAllowedToCharge || poolAdmin) && !fixedFee ? (
<RouterLinkButton
small
variant="tertiary"
icon={<IconSwitch size="20px" />}
to={`?charge=${feeChainData?.id}`}
>
Charge
</RouterLinkButton>
) : (
<Box height="32px"></Box>
),
poolCurrency: pool.currency.symbol,
}
})
Expand All @@ -119,21 +130,24 @@ export function PoolFees() {
...activeFees,
...changes.map(({ change, hash }) => {
return {
name: '',
name: poolMetadata?.pool?.poolFees?.find((f) => f.id === change.feeId)?.name,
type: change.type,
percentOfNav: change.amounts.percentOfNav,
pendingFees: undefined,
receivingAddress: change.destination,
action: (
<StyledLink
style={{ outline: 'none', border: 'none', background: 'none' }}
as="button"
action: poolAdmin ? (
<Button
variant="tertiary"
icon={<IconCheckInCircle size="20px" />}
onClick={() => {
applyNewFee([poolId, hash])
}}
small
>
<Text variant="body3">Apply changes</Text>
</StyledLink>
Apply changes
</Button>
) : (
<Box height="32px"></Box>
),
poolCurrency: pool.currency.symbol,
}
Expand Down Expand Up @@ -192,18 +206,6 @@ export function PoolFees() {
)
}

const StyledLink = styled(NavLink)<{ $disabled?: boolean }>(
{
display: 'inline-block',
outline: '0',
textDecoration: 'none',
':hover': {
textDecoration: 'underline',
},
},
(props) => props.$disabled && { pointerEvents: 'none' }
)

export function useProposedFeeChanges(poolId: string) {
const [result] = useCentrifugeQuery(['feeChanges', poolId], (cent) =>
cent.pools.getProposedPoolSystemChanges([poolId])
Expand All @@ -215,11 +217,12 @@ export function useProposedFeeChanges(poolId: string) {
.map(({ change, hash }) => {
return {
change: {
destination: change.poolFee.appendFee[1].destination,
type: Object.keys(change.poolFee.appendFee[1].feeType)[0],
destination: change.poolFee.appendFee[2].destination,
type: Object.keys(change.poolFee.appendFee[2].feeType)[0],
amounts: {
percentOfNav: new Rate(change.poolFee.appendFee[1].feeType.chargedUpTo.limit.shareOfPortfolioValuation),
percentOfNav: new Rate(change.poolFee.appendFee[2].feeType.chargedUpTo.limit.shareOfPortfolioValuation),
},
feeId: change.poolFee.appendFee[0],
},
hash,
}
Expand Down
8 changes: 4 additions & 4 deletions centrifuge-app/src/components/Portfolio/Holdings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,23 +111,23 @@ const columns: Column[] = [
View on Tinlake
</AnchorButton>
) : canInvestRedeem ? (
<>
<Shelf>
<RouterLinkButton to={`?redeem=${poolId}-${trancheId}`} small variant="tertiary" icon={IconMinus}>
Redeem
</RouterLinkButton>
<RouterLinkButton to={`?invest=${poolId}-${trancheId}`} small variant="tertiary" icon={IconPlus}>
Invest
</RouterLinkButton>
</>
</Shelf>
) : connectedNetwork === 'Centrifuge' ? (
<>
<Shelf>
<RouterLinkButton to={`?receive=${currency?.symbol}`} small variant="tertiary" icon={IconDownload}>
Receive
</RouterLinkButton>
<RouterLinkButton to={`?send=${currency?.symbol}`} small variant="tertiary" icon={IconSend}>
Send
</RouterLinkButton>
</>
</Shelf>
) : null}
</Grid>
)
Expand Down
4 changes: 4 additions & 0 deletions centrifuge-app/src/components/Tooltips.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,10 @@ export const tooltipText = {
label: 'T-Bill APR',
body: 'Based on 3- to 6-month T-bills returns. See pool details for further information.',
},
poolType: {
label: 'Pool type',
body: 'An open pool allows 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.',
},
}

export type TooltipsProps = {
Expand Down
66 changes: 35 additions & 31 deletions centrifuge-app/src/pages/IssuerCreatePool/PoolFeeInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,36 +13,40 @@ const FEE_TYPES = [
{ label: 'Fixed', value: 'Fixed' },
]

const DEFAULT_FEE = {
open: {
publicCredit: {
fee: 0.075,
name: 'Public Securities & Equities fees',
},
privateCredit: {
fee: 0.4,
name: 'Private Credit & Securities fees',
},
},
closed: {
publicCredit: {
fee: 0.02,
name: 'Public Securities & Equities fees',
},
privateCredit: {
fee: 0.15,
name: 'Private Credit & Securities fees',
},
},
}

export const PoolFeeSection: React.FC = () => {
const fmk = useFormikContext<PoolMetadataInput>()
const { values } = fmk
const address = useAddress()

React.useEffect(() => {
const isPrivateCredit = values.assetClass === 'privateCredit'

const defaultFees = [
{
name: 'Private Credit & Securities fees',
feeType: 'Fixed',
walletAddress: import.meta.env.REACT_APP_TREASURY,
percentOfNav: isPrivateCredit ? 0.15 : 0.4,
},
{
name: 'Public Securities & Equities fees',
feeType: 'Fixed',
walletAddress: import.meta.env.REACT_APP_TREASURY,
percentOfNav: isPrivateCredit ? 0.02 : 0.075,
},
]

defaultFees.forEach((fee, index) => {
fmk.setFieldValue(`poolFees.${index}.name`, fee.name)
fmk.setFieldValue(`poolFees.${index}.feeType`, fee.feeType)
fmk.setFieldValue(`poolFees.${index}.walletAddress`, fee.walletAddress)
fmk.setFieldValue(`poolFees.${index}.percentOfNav`, fee.percentOfNav)
})
}, [values.assetClass, address])
fmk.setFieldValue(`poolFees.0.name`, DEFAULT_FEE[values.poolType][values.assetClass].name)
fmk.setFieldValue(`poolFees.0.feeType`, 'Fixed')
fmk.setFieldValue(`poolFees.0.walletAddress`, import.meta.env.REACT_APP_TREASURY)
fmk.setFieldValue(`poolFees.0.percentOfNav`, DEFAULT_FEE[values.poolType][values.assetClass].fee)
}, [values.assetClass, values.poolType, address])

return (
<FieldArray name="poolFees">
Expand Down Expand Up @@ -76,15 +80,15 @@ export const PoolFeeInput: React.FC = () => {
return (
<FieldArray name="poolFees">
{(fldArr) => (
<Grid gridTemplateColumns={'1fr 1fr 1fr 1fr 40px'} gap={2} rowGap={3} alignItems="center">
<Grid gridTemplateColumns={'1fr 1fr 1fr 1fr 40px'} gap={2} rowGap={3}>
{values.poolFees.map((s, index) => (
<React.Fragment key={index}>
<FieldWithErrorMessage
as={TextInput}
label="Name"
maxLength={30}
name={`poolFees.${index}.name`}
disabled={index < 2}
disabled={index < 1}
/>

<Field name={`poolFees.${index}.feeType`}>
Expand All @@ -100,32 +104,32 @@ export const PoolFeeInput: React.FC = () => {
errorMessage={meta.touched && meta.error ? meta.error : undefined}
value={field.value}
options={FEE_TYPES}
disabled={index < 2}
disabled={index < 1}
/>
)
}}
</Field>

<FieldWithErrorMessage
as={NumberInput}
label="Fees in % of NAV"
label="Max fees in % of NAV"
min={0}
max={100}
symbol="%"
name={`poolFees.${index}.percentOfNav`}
disabled={index < 2}
disabled={index < 1}
/>

<FieldWithErrorMessage
as={TextInput}
label="Wallet address"
name={`poolFees.${index}.walletAddress`}
disabled={index < 2}
disabled={index < 1}
/>

<Box pt={1}>
<Button
disabled={index < 2}
disabled={index < 1}
variant="tertiary"
icon={IconMinusCircle}
onClick={() => fldArr.remove(index)}
Expand Down
Loading

0 comments on commit fde7c31

Please sign in to comment.