From aecd231c6ef8c6cd5042829670d644cfa28d4da8 Mon Sep 17 00:00:00 2001 From: Katty Barroso <51223655+kattylucy@users.noreply.github.com> Date: Wed, 4 Dec 2024 12:44:30 +0100 Subject: [PATCH] Create pool bugs and fixes (#2550) * Bug fixes and add proposal link * Fix ratings creating empty value --- .../IssuerCreatePool/PoolDetailsSection.tsx | 21 +- .../IssuerCreatePool/PoolSetupSection.tsx | 23 +- .../IssuerCreatePool/PoolStructureSection.tsx | 12 +- .../src/pages/IssuerCreatePool/index.tsx | 47 +- .../src/pages/IssuerCreatePool/oldindex.tsx | 839 ------------------ .../src/pages/IssuerCreatePool/types.ts | 6 +- .../src/pages/IssuerCreatePool/validate.ts | 6 +- centrifuge-js/src/modules/pools.ts | 10 + 8 files changed, 68 insertions(+), 896 deletions(-) delete mode 100644 centrifuge-app/src/pages/IssuerCreatePool/oldindex.tsx diff --git a/centrifuge-app/src/pages/IssuerCreatePool/PoolDetailsSection.tsx b/centrifuge-app/src/pages/IssuerCreatePool/PoolDetailsSection.tsx index a026f9170..8b9f678cb 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/PoolDetailsSection.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/PoolDetailsSection.tsx @@ -138,7 +138,7 @@ export const PoolDetailsSection = () => { form.setFieldValue('issuerLogo', file)} - accept="image/*" + accept="image/png, image/jpeg, image/jpg" fileTypeText="SVG, PNG, or JPG (max. 1MB; 480x480px)" label="Issuer logo" /> @@ -164,7 +164,7 @@ export const PoolDetailsSection = () => { /> )} - + {({ field, meta, form }: FieldProps) => ( { - + {({ field, meta, form }: FieldProps) => ( { label="Website URL" placeholder="www.example.com" isUrl + validate={validate.websiteNotRequired()} /> { label="Governance forum" placeholder="www.example.com" isUrl + validate={validate.websiteNotRequired()} /> { Service analysis - + { }} label="Reviewer avatar" placeholder="Choose file" - accept="image/*" + accept="image/png, image/jpeg, image/jpg" small /> )} diff --git a/centrifuge-app/src/pages/IssuerCreatePool/PoolSetupSection.tsx b/centrifuge-app/src/pages/IssuerCreatePool/PoolSetupSection.tsx index 971334ca0..c20df3aba 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/PoolSetupSection.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/PoolSetupSection.tsx @@ -57,6 +57,8 @@ export const PoolSetupSection = () => { const form = useFormikContext() const { values } = form + console.log(values) + return ( @@ -410,26 +412,7 @@ export const PoolSetupSection = () => { )} {values.onboardingExperience === 'external' && ( - {values.tranches.map((tranche, index) => ( - - {({ field, meta }: FieldProps) => ( - - Onboarding URL {tranche.tokenName}} - isUrl - placeholder="www.example.com" - onChange={(e: React.ChangeEvent) => - form.setFieldValue(`onboarding.tranches.${tranche.tokenName}`, e.target.value) - } - /> - - )} - - ))} + )} diff --git a/centrifuge-app/src/pages/IssuerCreatePool/PoolStructureSection.tsx b/centrifuge-app/src/pages/IssuerCreatePool/PoolStructureSection.tsx index 2aba393a6..b54be7710 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/PoolStructureSection.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/PoolStructureSection.tsx @@ -297,7 +297,7 @@ export const PoolStructureSection = () => { placeholder={getTrancheName(index)} maxLength={30} name={`tranches.${index}.tokenName`} - disabled={values.tranches.length === 1} + disabled value={getTrancheName(index)} onChange={(e: React.ChangeEvent) => handleTrancheNameChange(e, index, form)} /> @@ -312,7 +312,7 @@ export const PoolStructureSection = () => { Min. investment*} /> } placeholder="0.00" - currency={values.currency} + currency={values.assetDenomination} errorMessage={meta.touched ? meta.error : undefined} onChange={(value) => form.setFieldValue(field.name, value)} onBlur={() => form.setFieldTouched(field.name, true)} @@ -358,8 +358,8 @@ export const PoolStructureSection = () => { as={NumberInput} placeholder="0.00" symbol="%" - name={`tranches.${index}.interestRate`} - validate={validate.interestRate} + name={`tranches.${index}.apyPercentage`} + validate={validate.apyPercentage} /> @@ -392,7 +392,7 @@ export const PoolStructureSection = () => { /> } placeholder="0.00" - currency={values.currency} + currency={values.assetDenomination} errorMessage={meta.touched ? meta.error : undefined} onChange={(value) => form.setFieldValue(field.name, value)} onBlur={() => form.setFieldTouched(field.name, true)} @@ -402,7 +402,7 @@ export const PoolStructureSection = () => { - {({ field, form }: FieldProps) => ( + {({ field }: FieldProps) => ( theme.breakpoints.S}) { @@ -61,7 +62,7 @@ const stepFields: { [key: number]: string[] } = { 'issuerShortDescription', 'issuerDescription', ], - 3: ['investmentDetails', 'liquidityDetails'], + 3: ['assetOriginators', 'adminMultisig'], } const txMessage = { @@ -79,7 +80,6 @@ const IssuerCreatePoolPage = () => { const currencies = usePoolCurrencies() const centrifuge = useCentrifuge() const api = useCentrifugeApi() - const pools = usePools() const { poolCreationType } = useDebugFlags() const consts = useCentrifugeConsts() const { chainDecimals } = useCentrifugeConsts() @@ -95,7 +95,7 @@ const IssuerCreatePoolPage = () => { const [createdModal, setCreatedModal] = useState(false) const [preimageHash, setPreimageHash] = useState('') const [isPreimageDialogOpen, setIsPreimageDialogOpen] = useState(false) - const [createdPoolId, setCreatedPoolId] = useState('') + const [proposalId, setProposalId] = useState(null) useEffect(() => { if (createType === 'notePreimage') { @@ -106,6 +106,7 @@ const IssuerCreatePoolPage = () => { const event = events.find(({ event }) => api.events.preimage.Noted.is(event)) const parsedEvent = event?.toJSON() as any if (!parsedEvent) return false + console.info('Preimage hash: ', parsedEvent.event.data[0]) setPreimageHash(parsedEvent.event.data[0]) setIsPreimageDialogOpen(true) }) @@ -115,16 +116,6 @@ const IssuerCreatePoolPage = () => { } }, [centrifuge, createType]) - useEffect(() => { - if (createdPoolId && pools?.find((p) => p.id === createdPoolId)) { - // Redirecting only when we find the newly created pool in the data from usePools - // Otherwise the Issue Overview page will throw an error when it can't find the pool - // It can take a second for the new data to come in after creating the pool - navigate(`/issuer/${createdPoolId}`) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pools, createdPoolId]) - const { execute: createProxies, isLoading: createProxiesIsPending } = useCentrifugeTransaction( `${txMessage[createType]} 1/2`, (cent) => { @@ -236,11 +227,16 @@ const IssuerCreatePoolPage = () => { setIsMultisigDialogOpen(true) } const [, , , , poolId] = args - console.log(poolId, result) if (createType === 'immediate') { - setCreatedPoolId(poolId) + navigate(`/pools/${poolId}`) } else { - setCreatedModal(true) + const event = result.events.find(({ event }) => api.events.democracy.Proposed.is(event)) + if (event) { + const eventData = event.toHuman() as any + const proposalId = eventData.event.data.proposalIndex.replace(/\D/g, '') + setCreatedModal(true) + setProposalId(proposalId) + } } }, } @@ -286,7 +282,9 @@ const IssuerCreatePoolPage = () => { } // Pool ratings - if (values.poolRatings) { + if (values.poolRatings[0].agency === '') { + metadataValues.poolRatings = [] + } else { const newRatingReports = await Promise.all( values.poolRatings.map((rating) => pinFileIfExists(centrifuge, rating.reportFile ?? null)) ) @@ -300,8 +298,6 @@ const IssuerCreatePoolPage = () => { reportFile: pinnedReport ? { uri: pinnedReport.uri, mime: rating.reportFile?.type ?? '' } : null, } }) - - metadataValues.poolRatings = ratings } // Tranches @@ -433,6 +429,12 @@ const IssuerCreatePoolPage = () => { })) }, [values, step]) + const isCreatePoolEnabled = + values.assetOriginators.length > 0 && + values.assetOriginators[0] !== '' && + values.adminMultisig.signers.length > 0 && + values.adminMultisig.signers[0] !== '' + return ( <> { small onClick={handleNextStep} loading={createProxiesIsPending || transactionIsPending || form.isSubmitting} + disabled={step === 3 && !isCreatePoolEnabled} // Disable the button if on step 3 and conditions aren't met > {step === 3 ? 'Create pool' : 'Next'} @@ -514,7 +517,9 @@ const IssuerCreatePoolPage = () => { - + diff --git a/centrifuge-app/src/pages/IssuerCreatePool/oldindex.tsx b/centrifuge-app/src/pages/IssuerCreatePool/oldindex.tsx deleted file mode 100644 index 79609964c..000000000 --- a/centrifuge-app/src/pages/IssuerCreatePool/oldindex.tsx +++ /dev/null @@ -1,839 +0,0 @@ -import { CurrencyBalance, isSameAddress, Perquintill, Rate, TransactionOptions } from '@centrifuge/centrifuge-js' -import { - AddFee, - CurrencyKey, - FeeTypes, - FileType, - PoolMetadataInput, - TrancheInput, -} from '@centrifuge/centrifuge-js/dist/modules/pools' -import { - useBalances, - useCentrifuge, - useCentrifugeConsts, - useCentrifugeTransaction, - useWallet, -} from '@centrifuge/centrifuge-react' -import { - Box, - Button, - CurrencyInput, - FileUpload, - Grid, - Select, - Shelf, - Step, - Stepper, - Text, - TextInput, - Thumbnail, -} from '@centrifuge/fabric' -import { createKeyMulti, sortAddresses } from '@polkadot/util-crypto' -import BN from 'bn.js' -import { Field, FieldProps, Form, FormikErrors, FormikProvider, setIn, useFormik } from 'formik' -import * as React from 'react' -import { useNavigate } from 'react-router' -import { combineLatest, firstValueFrom, lastValueFrom, switchMap, tap } from 'rxjs' -import { useTheme } from 'styled-components' -import { useDebugFlags } from '../../components/DebugFlags' -import { PreimageHashDialog } from '../../components/Dialogs/PreimageHashDialog' -import { ShareMultisigDialog } from '../../components/Dialogs/ShareMultisigDialog' -import { FieldWithErrorMessage } from '../../components/FieldWithErrorMessage' -import { PageSection } from '../../components/PageSection' -import { Tooltips } from '../../components/Tooltips' -import { config, isTestEnv } from '../../config' -import { isSubstrateAddress } from '../../utils/address' -import { Dec } from '../../utils/Decimal' -import { formatBalance } from '../../utils/formatting' -import { getFileDataURI } from '../../utils/getFileDataURI' -import { useAddress } from '../../utils/useAddress' -import { useCreatePoolFee } from '../../utils/useCreatePoolFee' -import { usePoolCurrencies } from '../../utils/useCurrencies' -import { useFocusInvalidInput } from '../../utils/useFocusInvalidInput' -import { usePools } from '../../utils/usePools' -import { AdminMultisigSection } from './AdminMultisig' -import { IssuerInput } from './IssuerInput' -import { PoolFeeSection } from './PoolFeeInput' -import { PoolRatingInput } from './PoolRatingInput' -import { PoolReportsInput } from './PoolReportsInput' -import { TrancheSection } from './TrancheInput' -import { useStoredIssuer } from './useStoredIssuer' -import { validate } from './validate' - -const ASSET_CLASSES = Object.keys(config.assetClasses).map((key) => ({ - label: key, - value: key, -})) - -export default function IssuerCreatePoolPage() { - return -} - -export interface Tranche { - tokenName: string - symbolName: string - interestRate: number | '' - minRiskBuffer: number | '' - minInvestment: number | '' -} -export interface WriteOffGroupInput { - days: number | '' - writeOff: number | '' - penaltyInterest: number | '' -} - -export const createEmptyTranche = (trancheName: string): Tranche => ({ - tokenName: trancheName, - symbolName: '', - interestRate: trancheName === 'Junior' ? '' : 0, - minRiskBuffer: trancheName === 'Junior' ? '' : 0, - minInvestment: 1000, -}) - -export type CreatePoolValues = Omit< - PoolMetadataInput, - 'poolIcon' | 'issuerLogo' | 'executiveSummary' | 'adminMultisig' | 'poolFees' | 'poolReport' | 'poolRatings' -> & { - poolIcon: File | null - issuerLogo: File | null - executiveSummary: File | null - reportAuthorName: string - reportAuthorTitle: string - reportAuthorAvatar: File | null - reportUrl: string - adminMultisigEnabled: boolean - adminMultisig: Exclude - poolFees: { - id?: number - name: string - feeType: FeeTypes - percentOfNav: number | '' - walletAddress: string - feePosition: 'Top of waterfall' - category: string - }[] - poolType: 'open' | 'closed' - investorType: string - issuerShortDescription: string - issuerCategories: { type: string; value: string }[] - poolRatings: { - agency?: string - value?: string - reportUrl?: string - reportFile?: File | null - }[] - - poolStructure: string - trancheStucture: 1 | 2 | 3 -} - -const initialValues: CreatePoolValues = { - poolStructure: '', - trancheStucture: 0, - - poolIcon: null, - poolName: '', - assetClass: 'Private credit', - subAssetClass: '', - currency: isTestEnv ? 'USDC' : 'Native USDC', - maxReserve: 1000000, - epochHours: 23, // in hours - epochMinutes: 50, // in minutes - listed: !import.meta.env.REACT_APP_DEFAULT_UNLIST_POOLS, - investorType: '', - issuerName: '', - issuerRepName: '', - issuerLogo: null, - issuerDescription: '', - issuerShortDescription: '', - issuerCategories: [], - - executiveSummary: null, - website: '', - forum: '', - email: '', - details: [], - reportAuthorName: '', - reportAuthorTitle: '', - reportAuthorAvatar: null, - reportUrl: '', - - poolRatings: [], - - tranches: [createEmptyTranche('')], - adminMultisig: { - signers: [], - threshold: 1, - }, - adminMultisigEnabled: false, - poolFees: [], - poolType: 'open', -} - -function PoolIcon({ icon, children }: { icon?: File | null; children: string }) { - const [uri, setUri] = React.useState('') - React.useEffect(() => { - ;(async () => { - if (!icon) return - const uri = await getFileDataURI(icon) - setUri(uri) - })() - }, [icon]) - return uri ? : -} - -function CreatePoolForm() { - const theme = useTheme() - const [activeStep, setActiveStep] = React.useState(1) - - const address = useAddress('substrate') - const { - substrate: { addMultisig }, - } = useWallet() - const centrifuge = useCentrifuge() - const currencies = usePoolCurrencies() - const { chainDecimals } = useCentrifugeConsts() - const pools = usePools() - const navigate = useNavigate() - const balances = useBalances(address) - const { data: storedIssuer, isLoading: isStoredIssuerLoading } = useStoredIssuer() - const [waitingForStoredIssuer, setWaitingForStoredIssuer] = React.useState(true) - const [isPreimageDialogOpen, setIsPreimageDialogOpen] = React.useState(false) - const [isMultisigDialogOpen, setIsMultisigDialogOpen] = React.useState(false) - const [preimageHash, setPreimageHash] = React.useState('') - const [createdPoolId, setCreatedPoolId] = React.useState('') - const [multisigData, setMultisigData] = React.useState<{ hash: string; callData: string }>() - const { poolCreationType } = useDebugFlags() - const consts = useCentrifugeConsts() - const createType = (poolCreationType as TransactionOptions['createType']) || config.poolCreationType || 'immediate' - - React.useEffect(() => { - // If the hash can't be found on Pinata the request can take a long time to time out - // During which the name/description can't be edited - // Set a deadline for how long we're willing to wait on a stored issuer - setTimeout(() => setWaitingForStoredIssuer(false), 10000) - }, []) - - React.useEffect(() => { - if (storedIssuer) setWaitingForStoredIssuer(false) - }, [storedIssuer]) - - React.useEffect(() => { - if (createdPoolId && pools?.find((p) => p.id === createdPoolId)) { - // Redirecting only when we find the newly created pool in the data from usePools - // Otherwise the Issue Overview page will throw an error when it can't find the pool - // It can take a second for the new data to come in after creating the pool - navigate(`/issuer/${createdPoolId}`) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pools, createdPoolId]) - - const txMessage = { - immediate: 'Create pool', - propose: 'Submit pool proposal', - notePreimage: 'Note preimage', - } - const { execute: createPoolTx, isLoading: transactionIsPending } = useCentrifugeTransaction( - `${txMessage[createType]} 2/2`, - (cent) => - ( - args: [ - values: CreatePoolValues, - transferToMultisig: BN, - aoProxy: string, - adminProxy: string, - poolId: string, - tranches: TrancheInput[], - currency: CurrencyKey, - maxReserve: BN, - metadata: PoolMetadataInput, - poolFees: AddFee['fee'][] - ], - options - ) => { - const [values, transferToMultisig, aoProxy, adminProxy, , , , , { adminMultisig }] = args - const multisigAddr = adminMultisig && createKeyMulti(adminMultisig.signers, adminMultisig.threshold) - const poolArgs = args.slice(3) as any - return combineLatest([ - cent.getApi(), - cent.pools.createPool(poolArgs, { createType: options?.createType, batch: true }), - ]).pipe( - switchMap(([api, poolSubmittable]) => { - // BATCH https://polkadot.js.org/docs/kusama/extrinsics/#batchcalls-veccall - api.tx.utlity - .batch - // create pool current functionality + pure proxy functionality goes here - () - const adminProxyDelegates = multisigAddr - ? [multisigAddr] - : (adminMultisig && values.adminMultisig?.signers?.filter((addr) => addr !== address)) ?? [] - const otherMultisigSigners = - multisigAddr && sortAddresses(adminMultisig.signers.filter((addr) => !isSameAddress(addr, address!))) - const proxiedPoolCreate = api.tx.proxy.proxy(adminProxy, undefined, poolSubmittable) - const submittable = api.tx.utility.batchAll( - [ - api.tx.balances.transferKeepAlive(adminProxy, consts.proxy.proxyDepositFactor.add(transferToMultisig)), - api.tx.balances.transferKeepAlive( - aoProxy, - consts.proxy.proxyDepositFactor.add(consts.uniques.collectionDeposit) - ), - adminProxyDelegates.length > 0 && - api.tx.proxy.proxy( - adminProxy, - undefined, - api.tx.utility.batchAll( - [ - ...adminProxyDelegates.map((addr) => api.tx.proxy.addProxy(addr, 'Any', 0)), - multisigAddr ? api.tx.proxy.removeProxy(address, 'Any', 0) : null, - ].filter(Boolean) - ) - ), - api.tx.proxy.proxy( - aoProxy, - undefined, - api.tx.utility.batchAll([ - api.tx.proxy.addProxy(adminProxy, 'Any', 0), - api.tx.proxy.removeProxy(address, 'Any', 0), - ]) - ), - multisigAddr - ? api.tx.multisig.asMulti(adminMultisig.threshold, otherMultisigSigners, null, proxiedPoolCreate, 0) - : proxiedPoolCreate, - ].filter(Boolean) - ) - setMultisigData({ callData: proxiedPoolCreate.method.toHex(), hash: proxiedPoolCreate.method.hash.toHex() }) - return cent.wrapSignAndSend(api, submittable, { ...options, multisig: undefined, proxies: undefined }) - }) - ) - }, - { - onSuccess: (args) => { - if (form.values.adminMultisigEnabled && form.values.adminMultisig.threshold > 1) setIsMultisigDialogOpen(true) - const [, , , , poolId] = args - if (createType === 'immediate') { - setCreatedPoolId(poolId) - } - }, - } - ) - - const { execute: createProxies, isLoading: createProxiesIsPending } = useCentrifugeTransaction( - `${txMessage[createType]} 1/2`, - (cent) => { - return (_: [nextTx: (adminProxy: string, aoProxy: string) => void], options) => - cent.getApi().pipe( - switchMap((api) => { - const submittable = api.tx.utility.batchAll([ - api.tx.proxy.createPure('Any', 0, 0), - api.tx.proxy.createPure('Any', 0, 1), - ]) - return cent.wrapSignAndSend(api, submittable, options) - }) - ) - }, - { - onSuccess: async ([nextTx], result) => { - const api = await centrifuge.getApiPromise() - const events = result.events.filter(({ event }) => api.events.proxy.PureCreated.is(event)) - if (!events) return - const { pure } = (events[0].toHuman() as any).event.data - const { pure: pure2 } = (events[1].toHuman() as any).event.data - - nextTx(pure, pure2) - }, - } - ) - - const form = useFormik({ - initialValues, - validate: (values) => { - let errors: FormikErrors = {} - - const tokenNames = new Set() - const commonTokenSymbolStart = values.tranches[0].symbolName.slice(0, 3) - const tokenSymbols = new Set() - let prevInterest = Infinity - let prevRiskBuffer = 0 - - const juniorInterestRate = parseFloat(values.tranches[0].interestRate as string) - - values.poolFees.forEach((fee, i) => { - if (fee.name === '') { - errors = setIn(errors, `poolFees.${i}.name`, 'Name is required') - } - if (fee.percentOfNav === '' || fee.percentOfNav < 0.0001 || fee.percentOfNav > 10) { - errors = setIn(errors, `poolFees.${i}.percentOfNav`, 'Percentage between 0.0001 and 10 is required') - } - if (fee.walletAddress === '') { - errors = setIn(errors, `poolFees.${i}.walletAddress`, 'Wallet address is required') - } - if (!isSubstrateAddress(fee?.walletAddress)) { - errors = setIn(errors, `poolFees.${i}.walletAddress`, 'Invalid address') - } - }) - - values.tranches.forEach((t, i) => { - if (tokenNames.has(t.tokenName)) { - errors = setIn(errors, `tranches.${i}.tokenName`, 'Tranche names must be unique') - } - tokenNames.add(t.tokenName) - - // matches any character thats not alphanumeric or - - if (/[^a-z^A-Z^0-9^-]+/.test(t.symbolName)) { - errors = setIn(errors, `tranches.${i}.symbolName`, 'Invalid character detected') - } - - if (tokenSymbols.has(t.symbolName)) { - errors = setIn(errors, `tranches.${i}.symbolName`, 'Token symbols must be unique') - } - tokenSymbols.add(t.symbolName) - - if (t.symbolName.slice(0, 3) !== commonTokenSymbolStart) { - errors = setIn(errors, `tranches.${i}.symbolName`, 'Token symbols must all start with the same 3 characters') - } - - if (i > 0 && t.interestRate !== '') { - if (t.interestRate > juniorInterestRate) { - errors = setIn( - errors, - `tranches.${i}.interestRate`, - "Interest rate can't be higher than the junior tranche's target APY" - ) - } - if (t.interestRate > prevInterest) { - errors = setIn(errors, `tranches.${i}.interestRate`, "Can't be higher than a more junior tranche") - } - prevInterest = t.interestRate - } - - if (t.minRiskBuffer !== '') { - if (t.minRiskBuffer < prevRiskBuffer) { - errors = setIn(errors, `tranches.${i}.minRiskBuffer`, "Can't be lower than a more junior tranche") - } - prevRiskBuffer = t.minRiskBuffer - } - }) - - return errors - }, - validateOnMount: true, - onSubmit: async (values, { setSubmitting }) => { - if (!currencies || !address) return - - const metadataValues: PoolMetadataInput = { ...values } as any - - // Handle admin multisig - metadataValues.adminMultisig = - values.adminMultisigEnabled && values.adminMultisig.threshold > 1 - ? { - ...values.adminMultisig, - signers: sortAddresses(values.adminMultisig.signers), - } - : undefined - - // Get the currency for the pool - const currency = currencies.find((c) => c.symbol === values.currency)! - - // Pool ID and required assets - const poolId = await centrifuge.pools.getAvailablePoolId() - if (!values.poolIcon || (!isTestEnv && !values.executiveSummary)) { - return - } - - const pinFile = async (file: File): Promise => { - const pinned = await lastValueFrom(centrifuge.metadata.pinFile(await getFileDataURI(file))) - return { uri: pinned.uri, mime: file.type } - } - - // Handle pinning files (pool icon, issuer logo, and executive summary) - const promises = [pinFile(values.poolIcon)] - - if (values.issuerLogo) { - promises.push(pinFile(values.issuerLogo)) - } - - if (!isTestEnv && values.executiveSummary) { - promises.push(pinFile(values.executiveSummary)) - } - - const [pinnedPoolIcon, pinnedIssuerLogo, pinnedExecSummary] = await Promise.all(promises) - - metadataValues.issuerLogo = pinnedIssuerLogo?.uri - ? { uri: pinnedIssuerLogo.uri, mime: values?.issuerLogo?.type || '' } - : null - - metadataValues.executiveSummary = - !isTestEnv && values.executiveSummary - ? { uri: pinnedExecSummary.uri, mime: values.executiveSummary.type } - : null - - metadataValues.poolIcon = { uri: pinnedPoolIcon.uri, mime: values.poolIcon.type } - - // Handle pool report if available - if (values.reportUrl) { - let avatar = null - if (values.reportAuthorAvatar) { - const pinned = await pinFile(values.reportAuthorAvatar) - avatar = { uri: pinned.uri, mime: values.reportAuthorAvatar.type } - } - metadataValues.poolReport = { - authorAvatar: avatar, - authorName: values.reportAuthorName, - authorTitle: values.reportAuthorTitle, - url: values.reportUrl, - } - } - if (values.poolRatings) { - const newRatingReportPromise = await Promise.all( - values.poolRatings.map((rating) => (rating.reportFile ? pinFile(rating.reportFile) : null)) - ) - const ratings = values.poolRatings.map((rating, index) => { - let reportFile: FileType | null = rating.reportFile - ? { uri: rating.reportFile.name, mime: rating.reportFile.type } - : null - if (rating.reportFile && newRatingReportPromise[index]?.uri) { - reportFile = newRatingReportPromise[index] ?? null - } - return { - agency: rating.agency ?? '', - value: rating.value ?? '', - reportUrl: rating.reportUrl ?? '', - reportFile: reportFile ?? null, - } - }) - metadataValues.poolRatings = ratings - } - - const nonJuniorTranches = metadataValues.tranches.slice(1) - const tranches = [ - {}, - ...nonJuniorTranches.map((tranche) => ({ - interestRatePerSec: Rate.fromAprPercent(tranche.interestRate), - minRiskBuffer: Perquintill.fromPercent(tranche.minRiskBuffer), - })), - ] - - const feeId = await firstValueFrom(centrifuge.pools.getNextPoolFeeId()) - const poolFees: AddFee['fee'][] = values.poolFees.map((fee, i) => { - return { - name: fee.name, - destination: fee.walletAddress, - amount: Rate.fromPercent(fee.percentOfNav), - feeType: fee.feeType, - limit: 'ShareOfPortfolioValuation', - account: fee.feeType === 'chargedUpTo' ? fee.walletAddress : undefined, - feePosition: fee.feePosition, - } - }) - metadataValues.poolFees = poolFees.map((fee, i) => ({ - name: fee.name, - id: feeId + i, - feePosition: fee.feePosition, - feeType: fee.feeType, - })) - - if (metadataValues.adminMultisig && metadataValues.adminMultisig.threshold > 1) { - addMultisig(metadataValues.adminMultisig) - } - - createProxies([ - (aoProxy, adminProxy) => { - createPoolTx( - [ - values, - CurrencyBalance.fromFloat(createDeposit, chainDecimals), - aoProxy, - adminProxy, - poolId, - tranches, - currency.key, - CurrencyBalance.fromFloat(values.maxReserve, currency.decimals), - metadataValues, - poolFees, - ], - { createType } - ) - }, - ]) - - setSubmitting(false) - }, - }) - - React.useEffect(() => { - if (!isStoredIssuerLoading && storedIssuer && waitingForStoredIssuer) { - if (storedIssuer.name) { - form.setFieldValue('issuerName', storedIssuer.name, false) - } - if (storedIssuer.repName) { - form.setFieldValue('issuerRepName', storedIssuer.repName, false) - } - if (storedIssuer.description) { - form.setFieldValue('issuerDescription', storedIssuer.description, false) - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isStoredIssuerLoading]) - - React.useEffect(() => { - if (createType === 'notePreimage') { - const $events = centrifuge - .getEvents() - .pipe( - tap(({ api, events }) => { - const event = events.find(({ event }) => api.events.preimage.Noted.is(event)) - const parsedEvent = event?.toJSON() as any - if (!parsedEvent) return false - console.info('Preimage hash: ', parsedEvent.event.data[0]) - setPreimageHash(parsedEvent.event.data[0]) - setIsPreimageDialogOpen(true) - }) - ) - .subscribe() - return () => $events.unsubscribe() - } - }, [centrifuge, createType]) - - const formRef = React.useRef(null) - useFocusInvalidInput(form, formRef) - - const { proposeFee, poolDeposit, proxyDeposit, collectionDeposit } = useCreatePoolFee(form?.values) - const createDeposit = (proposeFee?.toDecimal() ?? Dec(0)) - .add(poolDeposit.toDecimal()) - .add(collectionDeposit.toDecimal()) - const deposit = createDeposit.add(proxyDeposit.toDecimal()) - - const subAssetClasses = - config.assetClasses[form.values.assetClass]?.map((label) => ({ - label, - value: label, - })) ?? [] - - // Use useEffect to update tranche name when poolName changes - React.useEffect(() => { - if (form.values.poolName) { - form.setFieldValue('tranches', [createEmptyTranche(form.values.poolName)]) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [form.values.poolName]) - - return ( - <> - setIsPreimageDialogOpen(false)} - /> - {multisigData && ( - setIsMultisigDialogOpen(false)} - /> - )} - -
- - New pool setup - - - - - - - - - - - - - - - - {({ field, form, meta }: FieldProps) => ( - } - onChange={(event) => { - form.setFieldValue('assetClass', event.target.value) - form.setFieldValue('subAssetClass', '', false) - }} - onBlur={field.onBlur} - errorMessage={meta.touched && meta.error ? meta.error : undefined} - value={field.value} - options={ASSET_CLASSES} - placeholder="Select..." - /> - )} - - - - - {({ field, meta, form }: FieldProps) => ( - } - onChange={(event: any) => form.setFieldValue('investorType', event.target.value)} - onBlur={field.onBlur} - errorMessage={meta.touched && meta.error ? meta.error : undefined} - value={field.value} - as={TextInput} - /> - )} - - - - - {({ field, meta, form }: FieldProps) => ( - } - onChange={(event) => form.setFieldValue('currency', event.target.value)} - onBlur={field.onBlur} - errorMessage={meta.touched && meta.error ? meta.error : undefined} - value={field.value} - options={currencies?.map((c) => ({ value: c.symbol, label: c.name })) ?? []} - placeholder="Select..." - /> - ) - }} - - - - - {({ field, form }: FieldProps) => ( - form.setFieldValue('maxReserve', value)} - /> - )} - - - - - {({ field, meta, form }: FieldProps) => ( - form.setFieldValue('poolStructure', event.target.value)} - onBlur={field.onBlur} - errorMessage={meta.touched && meta.error ? meta.error : undefined} - value={field.value} - as={TextInput} - placeholder="Revolving" - /> - )} - - - - - - - - - - - - - - - - - - - - - - - Deposit required: {formatBalance(deposit, balances?.native.currency.symbol, 1)} - - - - - - - -
- - ) -} diff --git a/centrifuge-app/src/pages/IssuerCreatePool/types.ts b/centrifuge-app/src/pages/IssuerCreatePool/types.ts index 12d64725d..c266c0970 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/types.ts +++ b/centrifuge-app/src/pages/IssuerCreatePool/types.ts @@ -8,13 +8,14 @@ export interface Tranche { minInvestment: number | '' apy: string interestRate: number | '' + apyPercentage: number | null } export interface PoolFee { id: number name: string feeType: FeeTypes - percentOfNav: string + percentOfNav: number walletAddress: string feePosition: 'Top of waterfall' category: string @@ -32,6 +33,7 @@ export const createEmptyTranche = (trancheName: string): Tranche => ({ minRiskBuffer: trancheName === 'Junior' ? '' : 0, minInvestment: 1000, apy: '90d', + apyPercentage: null, }) export const createPoolFee = (): PoolFee => ({ @@ -40,7 +42,7 @@ export const createPoolFee = (): PoolFee => ({ category: '', feePosition: 'Top of waterfall', feeType: '' as FeeTypes, - percentOfNav: '', + percentOfNav: 0, walletAddress: '', }) diff --git a/centrifuge-app/src/pages/IssuerCreatePool/validate.ts b/centrifuge-app/src/pages/IssuerCreatePool/validate.ts index 7c618bec8..52c78d436 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/validate.ts +++ b/centrifuge-app/src/pages/IssuerCreatePool/validate.ts @@ -43,6 +43,7 @@ export const validate = { maxPriceVariation: combine(required(), min(0), max(10000)), maturityDate: combine(required(), maturityDate()), apy: required(), + apyPercentage: required(), // pool details poolName: combine(required(), maxLength(100)), @@ -93,13 +94,14 @@ export const validateValues = (values: CreatePoolValues) => { let prevInterest = Infinity let prevRiskBuffer = 0 - const juniorInterestRate = parseFloat(values.tranches[0].interestRate as string) + const juniorInterestRate = + values.tranches[0].apyPercentage !== null ? parseFloat(values.tranches[0].apyPercentage.toString()) : 0 values.poolFees.forEach((fee, i) => { if (fee.name === '') { errors = setIn(errors, `poolFees.${i}.name`, 'Name is required') } - if (fee.percentOfNav === '' || fee.percentOfNav < 0.0001 || fee.percentOfNav > 10) { + if (fee.percentOfNav === 0 || fee.percentOfNav < 0.0001 || fee.percentOfNav > 10) { errors = setIn(errors, `poolFees.${i}.percentOfNav`, 'Percentage between 0.0001 and 10 is required') } if (fee.walletAddress === '') { diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index 575b0c612..db3730707 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -662,10 +662,12 @@ interface TrancheFormValues { minRiskBuffer: number | '' minInvestment: number | '' apy: string | '' + apyPercentage: number | null } export interface PoolMetadataInput { // structure + assetDenomination: string poolStructure: 'revolving' assetClass: 'Public credit' | 'Private credit' subAssetClass: string @@ -713,6 +715,7 @@ export interface PoolMetadataInput { onboarding?: { tranches: { [trancheId: string]: { agreement: FileType | undefined; openForOnboarding: boolean } } taxInfoRequired?: boolean + externalOnboardingUrl?: string } listed?: boolean @@ -780,6 +783,7 @@ export type PoolMetadata = { icon?: FileType | null minInitialInvestment?: string apy: string + apyPercentage: number | null } > loanTemplates?: { @@ -1129,6 +1133,7 @@ export function getPoolsModule(inst: Centrifuge) { tranchesById[computeTrancheId(index, poolId)] = { minInitialInvestment: CurrencyBalance.fromFloat(tranche.minInvestment, currencyDecimals).toString(), apy: tranche.apy, + apyPercentage: tranche.apyPercentage, } }) @@ -1178,6 +1183,11 @@ export function getPoolsModule(inst: Centrifuge) { pod: {}, tranches: tranchesById, adminMultisig: metadata.adminMultisig, + onboarding: { + tranches: metadata.onboarding?.tranches || {}, + taxInfoRequired: metadata.onboarding?.taxInfoRequired, + externalOnboardingUrl: metadata.onboarding?.externalOnboardingUrl, + }, } return inst.metadata.pinJson(formattedMetadata)