From b493c5af8c4b1688fdba5c49de18f0df06b1f0b8 Mon Sep 17 00:00:00 2001 From: Sophia Date: Wed, 18 Oct 2023 16:08:28 -0400 Subject: [PATCH 1/3] Create LICENSE (#1646) --- LICENSE | 165 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..e554c4cf4e --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2019 Centrifuge GmbH + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. From e2e35258add86dded2216f329b719206b186643f Mon Sep 17 00:00:00 2001 From: Sophia Date: Thu, 19 Oct 2023 09:48:36 -0400 Subject: [PATCH 2/3] Update support email (#1645) --- onboarding-api/src/emails/sendDocumentsMessage.ts | 2 +- onboarding-api/src/emails/sendVerifiedBusinessMessage.ts | 2 +- onboarding-api/src/emails/sendVerifyEmailMessage.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/onboarding-api/src/emails/sendDocumentsMessage.ts b/onboarding-api/src/emails/sendDocumentsMessage.ts index e34ac1d192..e9d73fd3be 100644 --- a/onboarding-api/src/emails/sendDocumentsMessage.ts +++ b/onboarding-api/src/emails/sendDocumentsMessage.ts @@ -69,7 +69,7 @@ export const sendDocumentsMessage = async ( template_id: templateIds.updateInvestorStatus, from: { name: 'Centrifuge', - email: 'hello@centrifuge.io', + email: 'support@centrifuge.io', }, attachments, } diff --git a/onboarding-api/src/emails/sendVerifiedBusinessMessage.ts b/onboarding-api/src/emails/sendVerifiedBusinessMessage.ts index c9328ca4e4..b231d8f830 100644 --- a/onboarding-api/src/emails/sendVerifiedBusinessMessage.ts +++ b/onboarding-api/src/emails/sendVerifiedBusinessMessage.ts @@ -59,7 +59,7 @@ export const sendVerifiedBusinessMessage = async ( from: { name: 'Centrifuge', email: isGloballyOnboarding - ? 'hello@centrifuge.io' + ? 'support@centrifuge.io' : `issuer+${metadata.pool.name?.replaceAll(' ', '')}@centrifuge.io`, }, } diff --git a/onboarding-api/src/emails/sendVerifyEmailMessage.ts b/onboarding-api/src/emails/sendVerifyEmailMessage.ts index 01744b2741..8dcb06aa38 100644 --- a/onboarding-api/src/emails/sendVerifyEmailMessage.ts +++ b/onboarding-api/src/emails/sendVerifyEmailMessage.ts @@ -34,7 +34,7 @@ export const sendVerifyEmailMessage = async (user: OnboardingUser, wallet: Reque template_id: templateIds.verifyEmail, from: { name: 'Centrifuge', - email: 'hello@centrifuge.io', + email: 'support@centrifuge.io', }, } await sendEmail(message) From 772aca2d56ba86a0992fd535b776c97003c8c13d Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Thu, 19 Oct 2023 16:33:08 +0200 Subject: [PATCH 3/3] Centrifuge App: Pending write-off and epoch changes (#1628) --- .../src/components/PoolChangesBanner.tsx | 41 ++++++++++ .../Configuration/EpochAndTranches.tsx | 54 +++++++++---- .../Configuration/PendingLoanChanges.tsx | 49 ------------ .../Configuration/WriteOffGroups.tsx | 50 +++++++++--- .../pages/IssuerPool/Configuration/index.tsx | 6 +- centrifuge-app/src/pages/IssuerPool/index.tsx | 31 ++++--- centrifuge-app/src/utils/usePools.ts | 80 +++++++++++++------ centrifuge-js/src/modules/pools.ts | 14 ++-- .../CentrifugeProvider/CentrifugeProvider.tsx | 4 + 9 files changed, 205 insertions(+), 124 deletions(-) create mode 100644 centrifuge-app/src/components/PoolChangesBanner.tsx delete mode 100644 centrifuge-app/src/pages/IssuerPool/Configuration/PendingLoanChanges.tsx diff --git a/centrifuge-app/src/components/PoolChangesBanner.tsx b/centrifuge-app/src/components/PoolChangesBanner.tsx new file mode 100644 index 0000000000..9607820f49 --- /dev/null +++ b/centrifuge-app/src/components/PoolChangesBanner.tsx @@ -0,0 +1,41 @@ +import { Banner, Text } from '@centrifuge/fabric' +import * as React from 'react' +import { useLoanChanges, usePoolChanges } from '../utils/usePools' +import { RouterTextLink } from './TextLink' + +export type PoolChangesBannerProps = { + poolId: string +} +const STORAGE_KEY = 'poolChangesBannerDismissed' + +export function PoolChangesBanner({ poolId }: PoolChangesBannerProps) { + const poolChanges = usePoolChanges(poolId) + const { policyChanges } = useLoanChanges(poolId) + const [isOpen, setIsOpen] = React.useState(false) + + React.useEffect(() => { + const dismissed = !!sessionStorage.getItem(STORAGE_KEY) + const hasReady = policyChanges?.some((change) => change.status === 'ready') || poolChanges?.status === 'ready' + if (!dismissed && hasReady) { + setIsOpen(true) + } + }, [poolChanges, policyChanges]) + + function onClose() { + sessionStorage.setItem(STORAGE_KEY, '1') + setIsOpen(false) + } + + return ( + + There are pending pool changes that can now be enabled{' '} + here + + } + /> + ) +} diff --git a/centrifuge-app/src/pages/IssuerPool/Configuration/EpochAndTranches.tsx b/centrifuge-app/src/pages/IssuerPool/Configuration/EpochAndTranches.tsx index 8cf75fc5aa..1ecfa17f0d 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, usePoolMetadata } from '../../../utils/usePools' +import { usePool, usePoolChanges, usePoolMetadata } from '../../../utils/usePools' import { TrancheInput } from '../../IssuerCreatePool/TrancheInput' import { validate } from '../../IssuerCreatePool/validate' @@ -25,7 +25,13 @@ export function EpochAndTranches() { const [isEditing, setIsEditing] = React.useState(false) const pool = usePool(poolId) const { data: metadata } = usePoolMetadata(pool) - const [account] = useSuitableAccounts({ poolId, poolRole: ['PoolAdmin'] }) + 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[] = [ { @@ -85,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) @@ -177,7 +183,7 @@ export function EpochAndTranches() { ] execute( [poolId, newPoolMetadata, { minEpochTime: epochSeconds, tranches: hasTrancheChanges ? tranches : undefined }], - { account } + { account, forceProxyType: 'Borrow' } ) actions.setSubmitting(false) }, @@ -198,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() @@ -206,14 +212,15 @@ export function EpochAndTranches() {
+ Epoch and tranches{' '} + {changes && changes.status !== 'ready' && Pending changes} + } + subtitle={`Changes require ${ + delay < 0.5 ? `${Math.ceil(delay / 24)} hour(s)` : `${Math.round(delay)} day(s)` + } and no oustanding redeem orders before they can be enabled`} headerRight={ isEditing ? ( @@ -232,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 70692053b6..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 [account] = useSuitableAccounts({ poolId, poolRole: ['LoanAdmin'] }) + 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={ <> @@ -186,15 +199,29 @@ export function WriteOffGroups() { small loading={isLoading || form.isSubmitting} loadingMessage={isLoading || form.isSubmitting ? 'Pending...' : undefined} + disabled={!account} key="done" > Done ) : ( - + + {latestPolicyChange?.status === 'ready' && ( + + )} + + )} } @@ -208,7 +235,6 @@ export function WriteOffGroups() { ) : ( )} - diff --git a/centrifuge-app/src/pages/IssuerPool/Configuration/index.tsx b/centrifuge-app/src/pages/IssuerPool/Configuration/index.tsx index baad4393f9..79985e7cf4 100644 --- a/centrifuge-app/src/pages/IssuerPool/Configuration/index.tsx +++ b/centrifuge-app/src/pages/IssuerPool/Configuration/index.tsx @@ -4,7 +4,7 @@ import { useDebugFlags } from '../../../components/DebugFlags' import { LoadBoundary } from '../../../components/LoadBoundary' import { PageWithSideBar } from '../../../components/PageWithSideBar' import { PendingMultisigs } from '../../../components/PendingMultisigs' -import { usePoolAdmin } from '../../../utils/usePermissions' +import { useCanBorrow, usePoolAdmin } from '../../../utils/usePermissions' import { IssuerPoolHeader } from '../Header' import { Details } from './Details' import { EpochAndTranches } from './EpochAndTranches' @@ -28,10 +28,12 @@ export function IssuerPoolConfigurationPage() { function IssuerPoolConfiguration() { const { pid: poolId } = useParams<{ pid: string }>() const { editPoolConfig } = useDebugFlags() + const isPoolAdmin = !!usePoolAdmin(poolId) + const isBorrower = useCanBorrow(poolId) return ( - {!!usePoolAdmin(poolId) && ( + {(isPoolAdmin || isBorrower) && ( <>
diff --git a/centrifuge-app/src/pages/IssuerPool/index.tsx b/centrifuge-app/src/pages/IssuerPool/index.tsx index 19db03f840..9678d3f3b6 100644 --- a/centrifuge-app/src/pages/IssuerPool/index.tsx +++ b/centrifuge-app/src/pages/IssuerPool/index.tsx @@ -1,5 +1,6 @@ import * as React from 'react' -import { Route, Switch, useRouteMatch } from 'react-router' +import { Route, Switch, useParams, useRouteMatch } from 'react-router' +import { PoolChangesBanner } from '../../components/PoolChangesBanner' import { IssuerPoolAccessPage } from './Access' import { IssuerPoolAssetPage } from './Assets' import { IssuerPoolConfigurationPage } from './Configuration' @@ -10,20 +11,24 @@ import { IssuerPoolLiquidityPage } from './Liquidity' import { IssuerPoolOverviewPage } from './Overview' import { IssuerPoolReportingPage } from './Reporting' -export const IssuerPoolPage: React.FC = () => { +export function IssuerPoolPage() { const { path } = useRouteMatch() + const { pid: poolId } = useParams<{ pid: string }>() return ( - - - - - - - - - - - + <> + + + + + + + + + + + + + ) } diff --git a/centrifuge-app/src/utils/usePools.ts b/centrifuge-app/src/utils/usePools.ts index 116ec7bfb9..cfa343ab62 100644 --- a/centrifuge-app/src/utils/usePools.ts +++ b/centrifuge-app/src/utils/usePools.ts @@ -1,7 +1,7 @@ import Centrifuge, { BorrowerTransaction, Loan, 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/pools.ts b/centrifuge-js/src/modules/pools.ts index 08c96dc096..1a705cd6a1 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -2899,15 +2899,17 @@ export function getPoolsModule(inst: Centrifuge) { switchMap((api) => api.query.poolSystem.scheduledUpdate(poolId)), map((updateData) => { const update = updateData.toPrimitive() as any - if (!update) return null + if (!update?.changes) return null + const { changes, submittedAt } = update + 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, + tranches: changes.tranches.noChange === null ? null : changes.tranches.newValue, + trancheMetadata: changes.trancheMetadata.noChange === null ? null : changes.trancheMetadata.newValue, + minEpochTime: changes.minEpochTime.noChange === null ? null : changes.minEpochTime.newValue, + maxNavAge: changes.maxNavAge.noChange === null ? null : changes.maxNavAge.newValue, }, - submittedAt: new Date(update.submittedAt * 1000).toISOString(), + submittedAt: new Date(submittedAt * 1000).toISOString(), } }) ) 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),