Skip to content

Commit

Permalink
Centrifuge App: Fix write-off policies (#1515)
Browse files Browse the repository at this point in the history
  • Loading branch information
onnovisser authored Aug 1, 2023
1 parent d495183 commit 35585ec
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useCentrifugeTransaction } from '@centrifuge/centrifuge-react'
import { Button, Shelf, Stack, Text } from '@centrifuge/fabric'
import { useLoanChanges, usePoolOrders } from '../../../utils/usePools'

const POOL_CHANGE_DELAY = 1000 * 60 * 60 * 24 * 7 // Currently hard-coded to 1 week on chain, will probably change to a constant we can query

// Currently only showing write-off policy changes
export function PendingLoanChanges({ poolId }: { poolId: string }) {
const poolOrders = usePoolOrders(poolId)
const hasLockedRedemptions = (poolOrders?.reduce((acc, cur) => acc + cur.activeRedeem.toFloat(), 0) ?? 0) > 0
const loanChanges = useLoanChanges(poolId)
const policyChanges = loanChanges?.filter(({ change }) => !!change.loan?.policy?.length)

const {
execute: executeApply,
isLoading: isApplyLoading,
lastCreatedTransaction,
} = useCentrifugeTransaction('Apply write-off policy', (cent) => cent.pools.applyWriteOffPolicyUpdate)

return (
<Stack gap={1}>
{policyChanges?.map((policy) => {
const waitingPeriodDone = new Date(policy.submittedAt).getTime() + POOL_CHANGE_DELAY < Date.now()

return (
<Shelf gap={2}>
<Text>Pending policy update</Text>
<Text>
{!waitingPeriodDone
? 'In waiting period'
: hasLockedRedemptions
? 'Blocked by locked redemptions'
: 'Can be applied'}
</Text>
<Button
variant="secondary"
small
onClick={() => executeApply([poolId, policy.hash])}
loading={isApplyLoading && lastCreatedTransaction?.args[1] === policy.hash}
style={{ marginLeft: 'auto' }}
>
Apply
</Button>
</Shelf>
)
})}
</Stack>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { PageSection } from '../../../components/PageSection'
import { formatPercentage } from '../../../utils/formatting'
import { useSuitableAccounts } from '../../../utils/usePermissions'
import { useConstants, useWriteOffGroups } from '../../../utils/usePools'
import { PendingLoanChanges } from './PendingLoanChanges'
import { WriteOffInput } from './WriteOffInput'

export type Row = WriteOffGroup
Expand Down Expand Up @@ -60,7 +61,7 @@ export function WriteOffGroups() {

const { execute, isLoading } = useCentrifugeTransaction(
'Update write-off policy',
(cent) => cent.pools.updateWriteOffGroups,
(cent) => cent.pools.updateWriteOffPolicy,
{
onSuccess: () => {
setIsEditing(false)
Expand Down Expand Up @@ -91,7 +92,7 @@ export function WriteOffGroups() {
let highestPenalty = 0
let previousDays = -1
writeOffGroups.forEach((g) => {
if (g.writeOff <= highestWriteOff) {
if ((g.writeOff as number) <= highestWriteOff) {
let index = values.writeOffGroups.findIndex((gr) => gr.days === g.days && gr.writeOff === g.writeOff)
index = index === -1 ? 0 : index
errors = setIn(
Expand All @@ -103,7 +104,7 @@ export function WriteOffGroups() {
highestWriteOff = g.writeOff as number
}

if (g.penaltyInterest < highestPenalty) {
if ((g.penaltyInterest as number) < highestPenalty) {
let index = values.writeOffGroups.findIndex((gr) => gr.days === g.days && gr.writeOff === g.writeOff)
index = index === -1 ? 0 : index
errors = setIn(
Expand Down Expand Up @@ -207,6 +208,7 @@ export function WriteOffGroups() {
) : (
<DataTable data={sortedSavedGroups} columns={columns} />
)}
<PendingLoanChanges poolId={poolId} />
</Stack>
</PageSection>
</Form>
Expand Down
14 changes: 13 additions & 1 deletion centrifuge-app/src/utils/usePools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,19 @@ export function useConstants() {
}

export function useWriteOffGroups(poolId: string) {
const [result] = useCentrifugeQuery(['writeOffGroups', poolId], (cent) => cent.pools.getWriteOffGroups([poolId]))
const [result] = useCentrifugeQuery(['writeOffGroups', poolId], (cent) => cent.pools.getWriteOffPolicy([poolId]))

return result
}

export function useLoanChanges(poolId: string) {
const [result] = useCentrifugeQuery(['loanChanges', poolId], (cent) => cent.pools.getProposedLoanChanges([poolId]))

return result
}

export function usePoolChanges(poolId: string) {
const [result] = useCentrifugeQuery(['poolChanges', poolId], (cent) => cent.pools.getProposedPoolChanges([poolId]))

return result
}
Expand Down
125 changes: 107 additions & 18 deletions centrifuge-js/src/modules/pools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2301,25 +2301,30 @@ export function getPoolsModule(inst: Centrifuge) {
)
}

function getWriteOffGroups(args: [poolId: string]) {
function getWriteOffPolicy(args: [poolId: string]) {
const [poolId] = args
const $api = inst.getApi()

return $api.pipe(
switchMap((api) => api.query.loans.writeOffPolicy(poolId)),
map((writeOffGroupsValues) => {
const writeOffGroups = writeOffGroupsValues.toJSON() as {
overdueDays: number
penalty: string
percentage: string
}[]
return writeOffGroups.map((g) => {
return {
overdueDays: g.overdueDays as number,
penaltyInterestRate: new Rate(hexToBN(g.penalty)),
percentage: new Rate(hexToBN(g.percentage)),
triggers: ({ principalOverdue: number } | { priceOutdated: number })[]
status: {
percentage: string
penalty: string
}
})
}[]
return writeOffGroups
.map((g) => {
return {
overdueDays: (g.triggers.find((t) => 'principalOverdue' in t) as { principalOverdue: number })
?.principalOverdue,
penaltyInterestRate: new Rate(hexToBN(g.status.penalty)),
percentage: new Rate(hexToBN(g.status.percentage)),
}
})
.filter((g) => g.overdueDays != null)
})
)
}
Expand Down Expand Up @@ -2422,7 +2427,39 @@ export function getPoolsModule(inst: Centrifuge) {
)
}

function updateWriteOffGroups(
function getProposedLoanChanges(args: [poolId: string]) {
const [poolId] = args
const $api = inst.getApi()

const $events = inst.getEvents().pipe(
filter(({ api, events }) => {
const event = events.find(({ event }) => api.events.poolSystem.ProposedChange.is(event))

if (!event) return false

const { poolId: eventPoolId } = (event.toHuman() as any).event.data
return eventPoolId.replace(/\D/g, '') === poolId
})
)

return $api.pipe(
switchMap((api) => api.query.poolSystem.notedChange.entries(poolId)),
map((changes) => {
return changes.map(([key, value]) => {
const hash = (key.toHuman() as any)[1] as string
const data = value.toPrimitive() as { change: any; submittedTime: number }
return {
hash,
submittedAt: new Date(data.submittedTime * 1000).toISOString(),
change: data.change,
}
})
}),
repeatWhen(() => $events)
)
}

function updateWriteOffPolicy(
args: [poolId: string, writeOffGroups: { percentage: BN; overdueDays: number; penaltyInterestRate: BN }[]],
options?: TransactionOptions
) {
Expand All @@ -2431,19 +2468,67 @@ export function getPoolsModule(inst: Centrifuge) {

return $api.pipe(
switchMap((api) => {
const submittable = api.tx.loans.updateWriteOffPolicy(
const submittable = api.tx.loans.proposeWriteOffPolicy(
poolId,
writeOffGroups.map((g) => ({
percentage: g.percentage.toString(),
overdueDays: g.overdueDays,
penalty: g.penaltyInterestRate.toString(),
triggers: [{ PrincipalOverdue: g.overdueDays }],
status: {
percentage: g.percentage.toString(),
penalty: g.penaltyInterestRate.toString(),
},
}))
)
return inst.wrapSignAndSend(api, submittable, options)
})
)
}

function applyWriteOffPolicyUpdate(args: [poolId: string, hash: string], options?: TransactionOptions) {
const [poolId, hash] = args
const $api = inst.getApi()

return $api.pipe(
switchMap((api) => {
const submittable = api.tx.loans.applyWriteOffPolicy(poolId, hash)
return inst.wrapSignAndSend(api, submittable, options)
})
)
}

function getProposedPoolChanges(args: [poolId: string]) {
const [poolId] = args
const $api = inst.getApi()

return $api.pipe(
switchMap((api) => (console.log('api', api), api.query.poolSystem.scheduledUpdate(poolId))),
map((updateData) => {
const update = updateData.toPrimitive() as any
if (!update) return null
return {
changes: {
tranches: update.tranches.noChange === null ? null : update.tranches.newValue,
trancheMetadata: update.trancheMetadata.noChange === null ? null : update.trancheMetadata.newValue,
minEpochTime: update.minEpochTime.noChange === null ? null : update.minEpochTime.newValue,
maxNavAge: update.maxNavAge.noChange === null ? null : update.maxNavAge.newValue,
},
submittedAt: new Date(update.submittedAt * 1000).toISOString(),
}
})
)
}

function applyPoolUpdate(args: [poolId: string], options?: TransactionOptions) {
const [poolId] = args
const $api = inst.getApi()

return $api.pipe(
switchMap((api) => {
const submittable = api.tx.poolRegistry.executeUpdate(poolId)
return inst.wrapSignAndSend(api, submittable, options)
})
)
}

function adminWriteOff(
args: [poolId: string, loanId: string, writeOffGroupId: number],
options?: TransactionOptions
Expand Down Expand Up @@ -2519,8 +2604,12 @@ export function getPoolsModule(inst: Centrifuge) {
getPoolOrders,
getLoans,
getPendingCollect,
getWriteOffGroups,
updateWriteOffGroups,
getWriteOffPolicy,
getProposedLoanChanges,
getProposedPoolChanges,
updateWriteOffPolicy,
applyWriteOffPolicyUpdate,
applyPoolUpdate,
adminWriteOff,
getAvailablePoolId,
getDailyPoolStates,
Expand Down

0 comments on commit 35585ec

Please sign in to comment.