diff --git a/centrifuge-app/src/components/PoolChangesBanner.tsx b/centrifuge-app/src/components/PoolChangesBanner.tsx index 316986714a..9607820f49 100644 --- a/centrifuge-app/src/components/PoolChangesBanner.tsx +++ b/centrifuge-app/src/components/PoolChangesBanner.tsx @@ -6,25 +6,23 @@ import { RouterTextLink } from './TextLink' export type PoolChangesBannerProps = { poolId: string } -const STORAGE_KEY = 'poolChangesBannerDismissedAt' +const STORAGE_KEY = 'poolChangesBannerDismissed' export function PoolChangesBanner({ poolId }: PoolChangesBannerProps) { - const changes = usePoolChanges(poolId) - const loanChanges = useLoanChanges(poolId) + const poolChanges = usePoolChanges(poolId) + const { policyChanges } = useLoanChanges(poolId) const [isOpen, setIsOpen] = React.useState(false) React.useEffect(() => { - const dismissedAt = new Date(localStorage.getItem(STORAGE_KEY) ?? 0) - if ( - (changes && new Date(changes.submittedAt) > dismissedAt) || - (loanChanges?.length && new Date(loanChanges.at(-1)!.submittedAt) > dismissedAt) - ) { + const dismissed = !!sessionStorage.getItem(STORAGE_KEY) + const hasReady = policyChanges?.some((change) => change.status === 'ready') || poolChanges?.status === 'ready' + if (!dismissed && hasReady) { setIsOpen(true) } - }, [changes, loanChanges]) + }, [poolChanges, policyChanges]) function onClose() { - localStorage.setItem(STORAGE_KEY, new Date(Date.now()).toISOString()) + sessionStorage.setItem(STORAGE_KEY, '1') setIsOpen(false) } diff --git a/centrifuge-app/src/pages/IssuerPool/Configuration/EpochAndTranches.tsx b/centrifuge-app/src/pages/IssuerPool/Configuration/EpochAndTranches.tsx index b2067df591..bc6806f457 100644 --- a/centrifuge-app/src/pages/IssuerPool/Configuration/EpochAndTranches.tsx +++ b/centrifuge-app/src/pages/IssuerPool/Configuration/EpochAndTranches.tsx @@ -1,6 +1,6 @@ import { CurrencyBalance, Perquintill, PoolMetadata, PoolMetadataInput, Rate } from '@centrifuge/centrifuge-js' -import { useCentrifugeTransaction } from '@centrifuge/centrifuge-react' -import { Button, Grid, NumberInput, Shelf, Stack, Text, Thumbnail } from '@centrifuge/fabric' +import { useCentrifugeConsts, useCentrifugeTransaction } from '@centrifuge/centrifuge-react' +import { Button, Grid, NumberInput, Shelf, Stack, StatusChip, Text, Thumbnail } from '@centrifuge/fabric' import { Form, FormikProvider, useFormik } from 'formik' import * as React from 'react' import { useParams } from 'react-router' @@ -12,7 +12,7 @@ import { LabelValueStack } from '../../../components/LabelValueStack' import { PageSection } from '../../../components/PageSection' import { formatBalance, formatPercentage } from '../../../utils/formatting' import { useSuitableAccounts } from '../../../utils/usePermissions' -import { useConstants, usePool, usePoolChanges, usePoolMetadata } from '../../../utils/usePools' +import { usePool, usePoolChanges, usePoolMetadata } from '../../../utils/usePools' import { TrancheInput } from '../../IssuerCreatePool/TrancheInput' import { validate } from '../../IssuerCreatePool/validate' @@ -28,6 +28,11 @@ export function EpochAndTranches() { const [account] = useSuitableAccounts({ poolId, poolRole: ['PoolAdmin'], proxyType: ['Borrow'] }) const changes = usePoolChanges(poolId) + const { execute: executeApply, isLoading: isApplyLoading } = useCentrifugeTransaction( + 'Apply pool update', + (cent) => cent.pools.applyPoolUpdate + ) + const columns: Column[] = [ { align: 'left', @@ -86,7 +91,7 @@ export function EpochAndTranches() { [pool, metadata] ) - const consts = useConstants() + const consts = useCentrifugeConsts() const epochHours = Math.floor((pool?.parameters.minEpochTime ?? 0) / 3600) const epochMinutes = Math.floor(((pool?.parameters.minEpochTime ?? 0) / 60) % 60) @@ -199,7 +204,7 @@ export function EpochAndTranches() { const hasChanges = Object.entries(form.values).some(([k, v]) => (initialValues as any)[k] !== v) - const delay = consts?.minUpdateDelay ? consts.minUpdateDelay / (60 * 60 * 24) : null + const delay = consts.poolSystem.minUpdateDelay / (60 * 60 * 24) const trancheData = [...tranches].reverse() @@ -207,14 +212,15 @@ export function EpochAndTranches() {
+ Epoch and tranches{' '} + {changes && changes.status !== 'ready' && Pending changes} + } + subtitle={`Changes take ${ + delay < 0.5 ? `${Math.ceil(delay / 24)} hour(s)` : `${Math.round(delay)} day(s)` + } to take effect`} headerRight={ isEditing ? ( @@ -233,9 +239,22 @@ export function EpochAndTranches() { ) : ( - + + {changes?.status === 'ready' && ( + + )} + + ) } > diff --git a/centrifuge-app/src/pages/IssuerPool/Configuration/PendingLoanChanges.tsx b/centrifuge-app/src/pages/IssuerPool/Configuration/PendingLoanChanges.tsx deleted file mode 100644 index 983f077b09..0000000000 --- a/centrifuge-app/src/pages/IssuerPool/Configuration/PendingLoanChanges.tsx +++ /dev/null @@ -1,49 +0,0 @@ -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 ( - - {policyChanges?.map((policy) => { - const waitingPeriodDone = new Date(policy.submittedAt).getTime() + POOL_CHANGE_DELAY < Date.now() - - return ( - - Pending policy update - - {!waitingPeriodDone - ? 'In waiting period' - : hasLockedRedemptions - ? 'Blocked by locked redemptions' - : 'Can be applied'} - - - - ) - })} - - ) -} diff --git a/centrifuge-app/src/pages/IssuerPool/Configuration/WriteOffGroups.tsx b/centrifuge-app/src/pages/IssuerPool/Configuration/WriteOffGroups.tsx index 210f5d91ce..b8e4e7118a 100644 --- a/centrifuge-app/src/pages/IssuerPool/Configuration/WriteOffGroups.tsx +++ b/centrifuge-app/src/pages/IssuerPool/Configuration/WriteOffGroups.tsx @@ -1,6 +1,6 @@ import { Rate, WriteOffGroup } from '@centrifuge/centrifuge-js' -import { useCentrifugeTransaction } from '@centrifuge/centrifuge-react' -import { Box, Button, Stack } from '@centrifuge/fabric' +import { useCentrifugeConsts, useCentrifugeTransaction } from '@centrifuge/centrifuge-react' +import { Box, Button, Shelf, Stack, StatusChip } from '@centrifuge/fabric' import { FieldArray, Form, FormikErrors, FormikProvider, setIn, useFormik } from 'formik' import * as React from 'react' import { useParams } from 'react-router' @@ -9,8 +9,7 @@ import { Column, DataTable } from '../../../components/DataTable' 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 { useLoanChanges, useWriteOffGroups } from '../../../utils/usePools' import { WriteOffInput } from './WriteOffInput' export type Row = WriteOffGroup @@ -53,8 +52,15 @@ export type WriteOffGroupValues = { writeOffGroups: WriteOffGroupInput[] } export function WriteOffGroups() { const { pid: poolId } = useParams<{ pid: string }>() const [isEditing, setIsEditing] = React.useState(false) - const consts = useConstants() + const consts = useCentrifugeConsts() const [account] = useSuitableAccounts({ poolId, poolRole: ['PoolAdmin'] }) + const { policyChanges } = useLoanChanges(poolId) + const latestPolicyChange = policyChanges?.at(-1) + + const { execute: executeApply, isLoading: isApplyLoading } = useCentrifugeTransaction( + 'Apply write-off policy', + (cent) => cent.pools.applyWriteOffPolicyUpdate + ) const savedGroups = useWriteOffGroups(poolId) const sortedSavedGroups = [...(savedGroups ?? [])].sort((a, b) => a.overdueDays - b.overdueDays) @@ -160,7 +166,7 @@ export function WriteOffGroups() { }} small key="edit" - disabled={form.values.writeOffGroups.length >= (consts?.maxWriteOffPolicySize ?? 5) || !account} + disabled={form.values.writeOffGroups.length >= consts.loans.maxWriteOffPolicySize || !account} > Add another @@ -172,7 +178,14 @@ export function WriteOffGroups() { + Write-off policy{' '} + {latestPolicyChange && latestPolicyChange.status !== 'ready' && ( + Pending changes + )} + + } subtitle="At least one write-off activity is required" headerRight={ <> @@ -193,9 +206,22 @@ export function WriteOffGroups() { ) : ( - + + {latestPolicyChange?.status === 'ready' && ( + + )} + + )} } @@ -209,7 +235,6 @@ export function WriteOffGroups() { ) : ( )} - diff --git a/centrifuge-app/src/utils/usePools.ts b/centrifuge-app/src/utils/usePools.ts index 9f202b312a..ad1a0260da 100644 --- a/centrifuge-app/src/utils/usePools.ts +++ b/centrifuge-app/src/utils/usePools.ts @@ -1,7 +1,7 @@ import Centrifuge, { ActiveLoan, BorrowerTransaction, Pool, PoolMetadata } from '@centrifuge/centrifuge-js' -import { useCentrifuge, useCentrifugeQuery, useWallet } from '@centrifuge/centrifuge-react' +import { useCentrifugeConsts, useCentrifugeQuery, useWallet } from '@centrifuge/centrifuge-react' import BN from 'bn.js' -import { useEffect } from 'react' +import { useEffect, useMemo } from 'react' import { useQuery } from 'react-query' import { combineLatest, map, Observable } from 'rxjs' import { Dec } from './Decimal' @@ -239,43 +239,73 @@ export function usePoolMetadata( return typeof pool?.metadata === 'string' ? data : tinlakeData } -export function useConstants() { - const centrifuge = useCentrifuge() - const { data } = useQuery( - ['constants'], - async () => { - const api = await centrifuge.getApiPromise() - return { - minUpdateDelay: Number(api.consts.poolSystem.minUpdateDelay.toHuman()), - maxTranches: Number(api.consts.poolSystem.maxTranches.toHuman()), - challengeTime: Number(api.consts.poolSystem.challengeTime.toHuman()), - maxWriteOffPolicySize: Number(api.consts.loans.maxWriteOffPolicySize.toHuman()), - } - }, - { - staleTime: Infinity, - } - ) - - return data -} - export function useWriteOffGroups(poolId: string) { const [result] = useCentrifugeQuery(['writeOffGroups', poolId], (cent) => cent.pools.getWriteOffPolicy([poolId])) return result } +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 + export function useLoanChanges(poolId: string) { + const poolOrders = usePoolOrders(poolId) + const [result] = useCentrifugeQuery(['loanChanges', poolId], (cent) => cent.pools.getProposedLoanChanges([poolId])) - return result + const policyChanges = useMemo(() => { + const hasLockedRedemptions = (poolOrders?.reduce((acc, cur) => acc + cur.activeRedeem.toFloat(), 0) ?? 0) > 0 + + return result + ?.filter(({ change }) => !!change.loan?.policy?.length) + .map((policy) => { + const waitingPeriodDone = new Date(policy.submittedAt).getTime() + POOL_CHANGE_DELAY < Date.now() + return { + ...policy, + status: !waitingPeriodDone + ? ('waiting' as const) + : hasLockedRedemptions + ? ('blocked' as const) + : ('ready' as const), + } + }) + }, [poolOrders, result]) + + return { policyChanges } } export function usePoolChanges(poolId: string) { + const pool = usePool(poolId) + const poolOrders = usePoolOrders(poolId) + const consts = useCentrifugeConsts() const [result] = useCentrifugeQuery(['poolChanges', poolId], (cent) => cent.pools.getProposedPoolChanges([poolId])) - return result + return useMemo( + () => { + if (!result) return result + const submittedTime = new Date(result.submittedAt).getTime() + const waitingPeriodDone = submittedTime + consts.poolSystem.minUpdateDelay * 1000 < Date.now() + const hasLockedRedemptions = (poolOrders?.reduce((acc, cur) => acc + cur.activeRedeem.toFloat(), 0) ?? 0) > 0 + const isEpochOngoing = pool.epoch.status === 'ongoing' + const epochNeedsClosing = submittedTime > new Date(pool.epoch.lastClosed).getTime() + return { + ...result, + status: !waitingPeriodDone + ? ('waiting' as const) + : hasLockedRedemptions || !isEpochOngoing || epochNeedsClosing + ? ('blocked' as const) + : ('ready' as const), + blockedBy: epochNeedsClosing + ? ('epochNeedsClosing' as const) + : hasLockedRedemptions + ? ('redemptions' as const) + : !isEpochOngoing + ? ('epochIsClosing' as const) + : null, + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [result, poolOrders, pool] + ) } export function usePodUrl(poolId: string) { diff --git a/centrifuge-js/src/modules/proxies.ts b/centrifuge-js/src/modules/proxies.ts index 279d8989fd..46c349d754 100644 --- a/centrifuge-js/src/modules/proxies.ts +++ b/centrifuge-js/src/modules/proxies.ts @@ -11,7 +11,12 @@ export function getProxiesModule(inst: CentrifugeBase) { const $api = inst.getApi() const $events = inst.getEvents().pipe( filter(({ api, events }) => { - const event = events.find(({ event }) => api.events.proxy.PureCreated.is(event)) + const event = events.find( + ({ event }) => + api.events.proxy.PureCreated.is(event) || + api.events.proxy.PureAdded.is(event) || + api.events.proxy.PureRemoved.is(event) + ) return !!event }) ) diff --git a/centrifuge-react/src/components/CentrifugeProvider/CentrifugeProvider.tsx b/centrifuge-react/src/components/CentrifugeProvider/CentrifugeProvider.tsx index ccc121fa42..79b8112684 100644 --- a/centrifuge-react/src/components/CentrifugeProvider/CentrifugeProvider.tsx +++ b/centrifuge-react/src/components/CentrifugeProvider/CentrifugeProvider.tsx @@ -70,6 +70,7 @@ export function useCentrifugeConsts() { .add(depositPerByte.mul(new BN(LOAN_NFT_DATA_BYTES))), chainDecimals ), + maxWriteOffPolicySize: Number(api.consts.loans.maxWriteOffPolicySize.toPrimitive()), }, proxy: { proxyDepositBase: new CurrencyBalance(consts.proxy.proxyDepositBase, chainDecimals), @@ -84,6 +85,9 @@ export function useCentrifugeConsts() { }, poolSystem: { poolDeposit: new CurrencyBalance(consts.poolSystem.poolDeposit, chainDecimals), + minUpdateDelay: Number(api.consts.poolSystem.minUpdateDelay.toPrimitive()), + maxTranches: Number(api.consts.poolSystem.maxTranches.toPrimitive()), + challengeTime: Number(api.consts.poolSystem.challengeTime.toPrimitive()), }, keystore: { keyDeposit: CurrencyBalance.fromFloat(100, chainDecimals), diff --git a/centrifuge-react/src/components/WalletProvider/WalletProvider.tsx b/centrifuge-react/src/components/WalletProvider/WalletProvider.tsx index 4097aa261c..4a59873f08 100644 --- a/centrifuge-react/src/components/WalletProvider/WalletProvider.tsx +++ b/centrifuge-react/src/components/WalletProvider/WalletProvider.tsx @@ -8,6 +8,7 @@ import * as React from 'react' import { useQuery } from 'react-query' import { firstValueFrom, map, switchMap } from 'rxjs' import { ReplacedError, useAsyncCallback } from '../../hooks/useAsyncCallback' +import { useCentrifugeQuery } from '../../hooks/useCentrifugeQuery' import { useCentrifuge, useCentrifugeApi, useCentrifugeConsts } from '../CentrifugeProvider' import { EvmChains, getAddChainParameters, getEvmUrls } from './evm/chains' import { EvmConnectorMeta, getEvmConnectors } from './evm/connectors' @@ -191,19 +192,6 @@ export function WalletProvider({ }, [] ) - - /* - ['allProxies'], - () => - firstValueFrom(cent.proxies.getAllProxies()).then((proxies) => { - return Object.fromEntries( - Object.entries(proxies).map(([delegatee, ps]) => [ - utils.formatAddress(delegatee), - ps.map((p) => ({ ...p, delegator: utils.formatAddress(p.delegator) })), - ]) - ) - }), - */ const evmSubstrateAccounts = isEvmOnSubstrate ? state.evm.accounts?.map((addr) => ({ address: evmToSubstrateAddress(addr, centEvmChainId!), @@ -211,36 +199,39 @@ export function WalletProvider({ wallet: state.evm.selectedWallet as any, })) : null - const { data: proxies, isLoading: proxiesAreLoading } = useQuery( - [ - 'proxies', - state.substrate.accounts?.map((acc) => acc.address), - state.substrate.multisigs.map((m) => m.address), - evmSubstrateAccounts?.map((acc) => acc.address), - ], - () => - firstValueFrom( - cent.proxies.getMultiUserProxies([ - (state.substrate.accounts || []) - .map((acc) => acc.address) - .concat(state.substrate.multisigs.map((m) => m.address)) - .concat(evmSubstrateAccounts?.map((acc) => acc.address) || []), - ]) - ), - { - staleTime: Infinity, - } - ) - const delegatees = [...new Set(Object.values(proxies ?? {})?.flatMap((p) => p.map((d) => d.delegator)))] - const { data: nestedProxies, isLoading: nestedProxiesAreLoading } = useQuery( - ['nestedProxies', delegatees], - () => firstValueFrom(cent.proxies.getMultiUserProxies([delegatees])), - { - enabled: !!Object.keys(proxies ?? {})?.length, - staleTime: Infinity, - } - ) + // const { data: proxies, isLoading: proxiesAreLoading } = useQuery( + // [ + // 'proxies', + // state.substrate.accounts?.map((acc) => acc.address), + // state.substrate.multisigs.map((m) => m.address), + // evmSubstrateAccounts?.map((acc) => acc.address), + // ], + // () => + // firstValueFrom( + // cent.proxies.getMultiUserProxies([ + // (state.substrate.accounts || []) + // .map((acc) => acc.address) + // .concat(state.substrate.multisigs.map((m) => m.address)) + // .concat(evmSubstrateAccounts?.map((acc) => acc.address) || []), + // ]) + // ), + // { + // staleTime: Infinity, + // } + // ) + + // const delegatees = [...new Set(Object.values(proxies ?? {})?.flatMap((p) => p.map((d) => d.delegator)))] + // const { data: nestedProxies, isLoading: nestedProxiesAreLoading } = useQuery( + // ['nestedProxies', delegatees], + // () => firstValueFrom(cent.proxies.getMultiUserProxies([delegatees])), + // { + // enabled: !!Object.keys(proxies ?? {})?.length, + // staleTime: Infinity, + // } + // ) + + const [proxies] = useCentrifugeQuery(['allProxies'], (cent) => cent.proxies.getAllProxies()) function setFilteredAccounts(accounts: SubstrateAccount[]) { const mappedAccounts = accounts @@ -369,7 +360,9 @@ export function WalletProvider({ const [scopedNetworks, setScopedNetworks] = React.useState(null) const ctx: WalletContextType = React.useMemo(() => { - const combinedProxies = { ...proxies, ...nestedProxies } + const combinedProxies = { + ...proxies, + } const combinedSubstrateAccounts = (evmSubstrateAccounts || state.substrate.accounts)?.flatMap((account) => { const { address } = account @@ -455,7 +448,7 @@ export function WalletProvider({ selectedProxies: selectedCombinedAccount?.proxies || null, selectedMultisig: selectedCombinedAccount?.multisig || null, proxies: combinedProxies, - proxiesAreLoading: nestedProxiesAreLoading || proxiesAreLoading, + proxiesAreLoading: !proxies, subscanUrl, }, evm: { @@ -465,7 +458,7 @@ export function WalletProvider({ chains: evmChains, }, } - }, [connect, disconnect, selectAccount, proxies, nestedProxies, state, isConnectError, isConnecting]) + }, [connect, disconnect, selectAccount, proxies, state, isConnectError, isConnecting]) return (