From 655041658aefd1b315a3f442c71d53e8f4c55325 Mon Sep 17 00:00:00 2001 From: katty barroso Date: Tue, 26 Nov 2024 10:26:27 +0100 Subject: [PATCH 01/17] Fix ts error and change logic for onboarding values --- .../IssuerCreatePool/PoolSetupSection.tsx | 54 ++++++++++--------- centrifuge-js/src/modules/pools.ts | 5 +- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/centrifuge-app/src/pages/IssuerCreatePool/PoolSetupSection.tsx b/centrifuge-app/src/pages/IssuerCreatePool/PoolSetupSection.tsx index b3bfede00..b16e97b23 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/PoolSetupSection.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/PoolSetupSection.tsx @@ -325,33 +325,35 @@ export const PoolSetupSection = () => { icon={} /> - - - {({ field, meta, form }: FieldProps) => ( - - { - form.setFieldTouched('poolIcon', true, false) - form.setFieldValue('poolIcon', file) - }} - label="Click to upload" - errorMessage={meta.touched && meta.error ? meta.error : undefined} - accept="application/pdf" - small - /> - - )} - - - Tax document requirement - + {values.onboardingExperience === 'centrifuge' && ( + + + {({ field, meta, form }: FieldProps) => ( + + { + form.setFieldTouched('poolIcon', true, false) + form.setFieldValue('poolIcon', file) + }} + label="Click to upload" + errorMessage={meta.touched && meta.error ? meta.error : undefined} + accept="application/pdf" + small + /> + + )} + + + Tax document requirement + + - + )} diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index 553d5cb39..2dddcee50 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -713,12 +713,11 @@ export interface PoolMetadataInput { signers: string[] threshold: number } - poolFees: { id: number; name: string; feePosition: 'Top of waterfall'; category?: string; feeType: FeeTypes }[] - poolType: 'open' | 'closed' - adminMultisigEnabled: boolean + onboardingExperience: string + assetOriginators: string[] } export type WithdrawAddress = { From 71089b13baa134607ed0cd3b5dcb7a8b29d3d748 Mon Sep 17 00:00:00 2001 From: katty barroso Date: Tue, 26 Nov 2024 11:34:05 +0100 Subject: [PATCH 02/17] Add create pool existing functionality --- .../src/pages/IssuerCreatePool/index.tsx | 312 +++++++++++++++++- .../src/pages/IssuerCreatePool/types.ts | 3 + .../src/pages/IssuerCreatePool/validate.ts | 3 +- 3 files changed, 315 insertions(+), 3 deletions(-) diff --git a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx index 018cde820..f286c969d 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx @@ -1,12 +1,40 @@ +import { + AddFee, + CurrencyBalance, + CurrencyKey, + FileType, + isSameAddress, + Perquintill, + PoolMetadataInput, + Rate, + TrancheInput, + TransactionOptions, +} from '@centrifuge/centrifuge-js' import { Box, Button, Step, Stepper, Text } from '@centrifuge/fabric' +import { createKeyMulti, sortAddresses } from '@polkadot/util-crypto' +import BN from 'bn.js' import { Form, FormikProvider, useFormik } from 'formik' import { useEffect, useRef, useState } from 'react' +import { combineLatest, firstValueFrom, lastValueFrom, switchMap } from 'rxjs' import styled, { useTheme } from 'styled-components' +import { + useAddress, + useCentrifuge, + useCentrifugeConsts, + useCentrifugeTransaction, + useWallet, +} from '../../../../centrifuge-react' +import { useDebugFlags } from '../../../src/components/DebugFlags' +import { Dec } from '../../../src/utils/Decimal' +import { getFileDataURI } from '../../../src/utils/getFileDataURI' +import { useCreatePoolFee } from '../../../src/utils/useCreatePoolFee' +import { usePoolCurrencies } from '../../../src/utils/useCurrencies' import { useIsAboveBreakpoint } from '../../../src/utils/useIsAboveBreakpoint' +import { config } from '../../config' import { PoolDetailsSection } from './PoolDetailsSection' import { PoolSetupSection } from './PoolSetupSection' import { Line, PoolStructureSection } from './PoolStructureSection' -import { initialValues } from './types' +import { CreatePoolValues, initialValues } from './types' import { validateValues } from './validate' const StyledBox = styled(Box)` @@ -31,20 +59,300 @@ const stepFields: { [key: number]: string[] } = { 3: ['investmentDetails', 'liquidityDetails'], } +const txMessage = { + immediate: 'Create pool', + propose: 'Submit pool proposal', + notePreimage: 'Note preimage', +} + const IssuerCreatePoolPage = () => { const theme = useTheme() const formRef = useRef(null) const isSmall = useIsAboveBreakpoint('S') + const address = useAddress('substrate') + const currencies = usePoolCurrencies() + const centrifuge = useCentrifuge() + const { poolCreationType } = useDebugFlags() + const consts = useCentrifugeConsts() + const { chainDecimals } = useCentrifugeConsts() + const createType = (poolCreationType as TransactionOptions['createType']) || config.poolCreationType || 'immediate' + const { + substrate: { addMultisig }, + } = useWallet() + const [step, setStep] = useState(1) const [stepCompleted, setStepCompleted] = useState({ 1: false, 2: false, 3: false }) + const [multisigData, setMultisigData] = useState<{ hash: string; callData: string }>() + const [createdPoolId, setCreatedPoolId] = useState('') + const [isMultisigDialogOpen, setIsMultisigDialogOpen] = useState(false) + + 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 { 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 form = useFormik({ initialValues, validate: (values) => validateValues(values), validateOnMount: true, - onSubmit: () => console.log('a'), + 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) { + 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 (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 = 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, + } + } + + // Handle pool ratings + 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 + } + + // Organize tranches + const nonJuniorTranches = metadataValues.tranches.slice(1) + const tranches = [ + {}, + ...nonJuniorTranches.map((tranche) => ({ + interestRatePerSec: Rate.fromAprPercent(tranche.interestRate), + minRiskBuffer: Perquintill.fromPercent(tranche.minRiskBuffer), + })), + ] + + // Pool fees + 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 } + ) + }, + ]) + }, }) + 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 { values, errors } = form const checkStepCompletion = (stepNumber: number) => { diff --git a/centrifuge-app/src/pages/IssuerCreatePool/types.ts b/centrifuge-app/src/pages/IssuerCreatePool/types.ts index b6f9c8eac..474bdb215 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/types.ts +++ b/centrifuge-app/src/pages/IssuerCreatePool/types.ts @@ -44,6 +44,9 @@ export type CreatePoolValues = Omit< // pool details poolType: 'open' | 'closed' issuerCategories: { type: string; value: string }[] + poolIcon: File + issuerLogo: File + executiveSummary: File reportAuthorName: string reportAuthorTitle: string diff --git a/centrifuge-app/src/pages/IssuerCreatePool/validate.ts b/centrifuge-app/src/pages/IssuerCreatePool/validate.ts index 18052c55e..7c618bec8 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/validate.ts +++ b/centrifuge-app/src/pages/IssuerCreatePool/validate.ts @@ -20,6 +20,7 @@ import { positiveNumber, required, } from '../../utils/validation' +import { CreatePoolValues } from './types' export const MB = 1024 ** 2 @@ -83,7 +84,7 @@ export const validate = { penaltyInterest: combine(required(), nonNegativeNumber(), max(100)), } -export const validateValues = (values) => { +export const validateValues = (values: CreatePoolValues) => { let errors: FormikErrors = {} const tokenNames = new Set() From 4d59d2af696d4d0427235f9b4d34a4685b03d790 Mon Sep 17 00:00:00 2001 From: katty barroso Date: Wed, 27 Nov 2024 15:48:54 +0100 Subject: [PATCH 03/17] Cleanup types --- .../IssuerCreatePool/IssuerCategories.tsx | 36 ++- .../IssuerCreatePool/PoolSetupSection.tsx | 38 ++- .../IssuerCreatePool/PoolStructureSection.tsx | 100 ++++--- .../src/pages/IssuerCreatePool/index.tsx | 269 +++++++++--------- .../src/pages/IssuerCreatePool/types.ts | 42 +-- centrifuge-js/src/modules/pools.ts | 46 ++- 6 files changed, 264 insertions(+), 267 deletions(-) diff --git a/centrifuge-app/src/pages/IssuerCreatePool/IssuerCategories.tsx b/centrifuge-app/src/pages/IssuerCreatePool/IssuerCategories.tsx index 4d70a2f34..9ada8a501 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/IssuerCategories.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/IssuerCategories.tsx @@ -1,5 +1,5 @@ import { PoolMetadataInput } from '@centrifuge/centrifuge-js' -import { Box, IconButton, IconTrash, Select, Text, TextInput } from '@centrifuge/fabric' +import { Box, Grid, IconButton, IconTrash, Select, Text, TextInput } from '@centrifuge/fabric' import { Field, FieldArray, FieldProps, useFormikContext } from 'formik' import { AddButton } from './PoolDetailsSection' import { StyledGrid } from './PoolStructureSection' @@ -40,20 +40,28 @@ export const IssuerCategoriesSection = () => { <> {form.values.issuerCategories.map((_, index) => ( <> - - {({ field, meta }: FieldProps) => ( - form.setFieldValue(field.name, event.target.value)} + onBlur={field.onBlur} + value={field.value} + options={PROVIDERS} + placeholder="Please select..." + /> + )} + + {_.type === 'other' && ( + + {({ field, meta }: FieldProps) => ( + + )} + )} - - + {({ field, meta }: FieldProps) => ( { {values.onboardingExperience === 'centrifuge' && ( - - {({ field, meta, form }: FieldProps) => ( + + {({ form }) => ( - { - form.setFieldTouched('poolIcon', true, false) - form.setFieldValue('poolIcon', file) - }} - label="Click to upload" - errorMessage={meta.touched && meta.error ? meta.error : undefined} - accept="application/pdf" - small - /> + {form.values.tranches.map((tranche, index) => ( + + {({ field, meta }: FieldProps) => ( + + { + form.setFieldTouched(`subscriptionDocuments[${index}]`, true, false) + form.setFieldValue(`subscriptionDocuments[${index}]`, file) + }} + label={`Subscription document for ${tranche.trancheName}`} + errorMessage={meta.touched && meta.error ? meta.error : undefined} + accept="application/pdf" + small + /> + + )} + + ))} )} - + Tax document requirement { const theme = useTheme() return @@ -58,16 +65,20 @@ export const CheckboxOption = ({ id, height, styles, + onChange, + isChecked, }: { name: string label: string sublabel?: string - value: string | number | boolean + value?: string | number | boolean disabled?: boolean icon?: React.ReactNode id?: keyof typeof tooltipText height?: number styles?: React.CSSProperties + onChange?: () => void + isChecked?: boolean }) => { const theme = useTheme() @@ -85,20 +96,24 @@ export const CheckboxOption = ({ alignItems={icon ? 'center' : 'flex-start'} {...styles} > - - {({ field, form, meta }: FieldProps) => ( - form.setFieldValue(name, val.target.checked ? value : null)} - onBlur={field.onBlur} - checked={form.values[name] === value} - /> - )} - + {onChange ? ( + + ) : ( + + {({ field, form, meta }: FieldProps) => ( + form.setFieldValue(name, val.target.checked ? value : null)} + onBlur={field.onBlur} + checked={form.values[name] === value} + /> + )} + + )} {icon && {icon}} />} {sublabel && ( @@ -125,7 +140,7 @@ export const PoolStructureSection = () => { case 0: return 'Junior' case 1: - return values.trancheStructure === 2 ? 'Senior' : 'Mezzanine' + return values.tranches.length === 2 ? 'Senior' : 'Mezzanine' case 2: return 'Senior' default: @@ -140,6 +155,23 @@ export const PoolStructureSection = () => { form.setFieldValue(`tranches.${index}.tokenName`, `${poolName} ${suffix}`) } + const handleTrancheCheckboxChange = (selectedValue: number) => { + if (selectedValue === 0) { + // Set to single tranche: Junior + form.setFieldValue('tranches', [createEmptyTranche('Junior')]) + } else if (selectedValue === 1) { + // Set to two tranches: Junior, Senior + form.setFieldValue('tranches', [createEmptyTranche('Junior'), createEmptyTranche('Senior')]) + } else if (selectedValue === 2) { + // Set to three tranches: Junior, Mezzanine, Senior + form.setFieldValue('tranches', [ + createEmptyTranche('Junior'), + createEmptyTranche('Mezzanine'), + createEmptyTranche('Senior'), + ]) + } + } + return ( @@ -166,27 +198,19 @@ export const PoolStructureSection = () => { Define tranche structure * - } - /> - } - /> - } - /> + + {Array.from({ length: 3 }).map((_, index) => { + return ( + } + onChange={() => handleTrancheCheckboxChange(index)} + isChecked={values.tranches.length === tranches[index].length} + /> + ) + })} @@ -259,7 +283,7 @@ export const PoolStructureSection = () => { Tranches - {Array.from({ length: values.trancheStructure }).map((_, index) => ( + {values.tranches.map((_, index) => ( Tranche {index + 1} diff --git a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx index f286c969d..99f6d99c7 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx @@ -1,12 +1,8 @@ import { AddFee, - CurrencyBalance, CurrencyKey, - FileType, isSameAddress, - Perquintill, PoolMetadataInput, - Rate, TrancheInput, TransactionOptions, } from '@centrifuge/centrifuge-js' @@ -15,7 +11,7 @@ import { createKeyMulti, sortAddresses } from '@polkadot/util-crypto' import BN from 'bn.js' import { Form, FormikProvider, useFormik } from 'formik' import { useEffect, useRef, useState } from 'react' -import { combineLatest, firstValueFrom, lastValueFrom, switchMap } from 'rxjs' +import { combineLatest, switchMap } from 'rxjs' import styled, { useTheme } from 'styled-components' import { useAddress, @@ -26,7 +22,6 @@ import { } from '../../../../centrifuge-react' import { useDebugFlags } from '../../../src/components/DebugFlags' import { Dec } from '../../../src/utils/Decimal' -import { getFileDataURI } from '../../../src/utils/getFileDataURI' import { useCreatePoolFee } from '../../../src/utils/useCreatePoolFee' import { usePoolCurrencies } from '../../../src/utils/useCurrencies' import { useIsAboveBreakpoint } from '../../../src/utils/useIsAboveBreakpoint' @@ -140,10 +135,8 @@ const IssuerCreatePoolPage = () => { ]).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 - () + api.tx.utlity.batch() + const adminProxyDelegates = multisigAddr ? [multisigAddr] : (adminMultisig && values.adminMultisig?.signers?.filter((addr) => addr !== address)) ?? [] @@ -202,10 +195,14 @@ const IssuerCreatePoolPage = () => { validate: (values) => validateValues(values), validateOnMount: true, onSubmit: async (values, { setSubmitting }) => { + const poolId = await centrifuge.pools.getAvailablePoolId() if (!currencies || !address) return const metadataValues: PoolMetadataInput = { ...values } as any + // find the currency (asset denomination in UI) + const currency = currencies.find((c) => c.symbol === values.currency)! + // Handle admin multisig metadataValues.adminMultisig = values.adminMultisigEnabled && values.adminMultisig.threshold > 1 @@ -215,133 +212,131 @@ const IssuerCreatePoolPage = () => { } : 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) { - 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 (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 = 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, - } - } - - // Handle pool ratings - 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 - } - - // Organize tranches - const nonJuniorTranches = metadataValues.tranches.slice(1) - const tranches = [ - {}, - ...nonJuniorTranches.map((tranche) => ({ - interestRatePerSec: Rate.fromAprPercent(tranche.interestRate), - minRiskBuffer: Perquintill.fromPercent(tranche.minRiskBuffer), - })), - ] - - // Pool fees - 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 } - ) - }, - ]) + // // Get the currency for the pool + + // // Pool ID and required assets + // if (!values.poolIcon) { + // 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 (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 = 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, + // } + // } + + // // Handle pool ratings + // 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 + // } + + // // Organize tranches + // const nonJuniorTranches = metadataValues.tranches.slice(1) + // const tranches = [ + // {}, + // ...nonJuniorTranches.map((tranche) => ({ + // interestRatePerSec: Rate.fromAprPercent(tranche.interestRate), + // minRiskBuffer: Perquintill.fromPercent(tranche.minRiskBuffer), + // })), + // ] + + // // Pool fees + // 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 } + // ) + // }, + // ]) }, }) diff --git a/centrifuge-app/src/pages/IssuerCreatePool/types.ts b/centrifuge-app/src/pages/IssuerCreatePool/types.ts index 474bdb215..f331e8590 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/types.ts +++ b/centrifuge-app/src/pages/IssuerCreatePool/types.ts @@ -1,4 +1,4 @@ -import { FeeTypes, PoolMetadataInput } from '@centrifuge/centrifuge-js' +import { PoolMetadataInput } from '@centrifuge/centrifuge-js' import { isTestEnv } from '../../config' export interface Tranche { @@ -19,7 +19,7 @@ export const createEmptyTranche = (trancheName: string): Tranche => ({ tokenName: trancheName, symbolName: '', interestRate: 0, - minRiskBuffer: trancheName === 'Junior' ? '' : 0, + minRiskBuffer: trancheName === 'Junio' ? '' : 0, minInvestment: 1000, apy: '90d', }) @@ -33,54 +33,20 @@ export const createPoolFee = () => ({ walletAddress: '', }) -export type CreatePoolValues = Omit< - PoolMetadataInput, - 'poolIcon' | 'issuerLogo' | 'executiveSummary' | 'adminMultisig' | 'poolFees' | 'poolReport' | 'poolRatings' -> & { +export type CreatePoolValues = Omit & { // pool structure assetDenomination: string - trancheStructure: 1 | 2 | 3 - - // pool details - poolType: 'open' | 'closed' - issuerCategories: { type: string; value: string }[] - poolIcon: File - issuerLogo: File - executiveSummary: File - - 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 - }[] - poolRatings: { - agency?: string - value?: string - reportUrl?: string - reportFile?: File | null - }[] } export const initialValues: CreatePoolValues = { // pool structure poolStructure: 'revolving', - trancheStructure: 1, assetClass: 'Private credit', assetDenomination: '', subAssetClass: '', - - // pool structure -> tranches - tranches: [createEmptyTranche('')], + tranches: [createEmptyTranche('Junior')], // pool details section poolName: '', diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index 2dddcee50..34816d7e3 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -665,59 +665,55 @@ interface TrancheFormValues { } export interface PoolMetadataInput { - // pool structure - poolStructure: string + // structure + poolStructure: 'revolving' assetClass: 'Public credit' | 'Private credit' subAssetClass: string - - // pool structure -> tranches tranches: TrancheFormValues[] // details poolName: string - assetDenomination: string investorType: string poolIcon: FileType | null + poolType: 'open' | 'closed' maxReserve: number | '' issuerName: string + issuerRepName?: string issuerLogo?: FileType | null - issuerRepName: string issuerShortDescription: string issuerDescription: string - issuerCategories: { type: string; value: string; customType?: string }[] - website: string - forum: string - email: string - executiveSummary: FileType | null + website?: string | '' + forum?: string | '' + email?: string | '' + executiveSummary?: FileType | null details?: IssuerDetail[] - - currency: string - epochHours: number | '' - epochMinutes: number | '' - listed?: boolean - - poolReport?: { - authorName: string - authorTitle: string - authorAvatar: FileType | null - url: string - } + issuerCategories: { type: string; value: string; description?: string }[] poolRatings: { agency?: string value?: string reportUrl?: string reportFile?: FileType | null }[] + poolReport?: { + authorName: string + authorTitle: string + authorAvatar: FileType | null + url: string + } + // setup adminMultisig?: { signers: string[] threshold: number } poolFees: { id: number; name: string; feePosition: 'Top of waterfall'; category?: string; feeType: FeeTypes }[] - poolType: 'open' | 'closed' adminMultisigEnabled: boolean - onboardingExperience: string assetOriginators: string[] + onboardingExperience: string + onboarding?: { + tranches: { [trancheId: string]: { agreement: FileType | undefined; openForOnboarding: boolean } } + taxInfoRequired?: boolean + } } export type WithdrawAddress = { From 5980f2b120c282bfe4d2a9fad1bc70c9691b8965 Mon Sep 17 00:00:00 2001 From: katty barroso Date: Wed, 27 Nov 2024 15:53:08 +0100 Subject: [PATCH 04/17] cleanup --- .../IssuerCreatePool/PoolSetupSection.tsx | 18 +++++----- .../src/pages/IssuerCreatePool/types.ts | 33 +++++++++++++++++-- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/centrifuge-app/src/pages/IssuerCreatePool/PoolSetupSection.tsx b/centrifuge-app/src/pages/IssuerCreatePool/PoolSetupSection.tsx index 54165a357..e3fa42588 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/PoolSetupSection.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/PoolSetupSection.tsx @@ -33,6 +33,8 @@ export const PoolSetupSection = () => { const form = useFormikContext() const { values } = form + console.log(values) + return ( @@ -88,7 +90,7 @@ export const PoolSetupSection = () => { { - if (form.values.adminMultisig && form.values.adminMultisig.signers?.length <= 10) { + if (values.adminMultisig && values.adminMultisig.signers?.length <= 10) { push('') } }} @@ -115,7 +117,7 @@ export const PoolSetupSection = () => { onBlur={field.onBlur} errorMessage={meta.touched && meta.error ? meta.error : undefined} value={field.value} - options={form.values.adminMultisig.signers.map((_: string, i: number) => ({ + options={values.adminMultisig.signers.map((_: string, i: number) => ({ label: i + 1, value: i + 1, }))} @@ -142,7 +144,7 @@ export const PoolSetupSection = () => { Add or remove addresses that can: Originate assets and invest in the pool* - {form.values.assetOriginators?.map((_: string, index: number) => ( + {values.assetOriginators?.map((_: string, index: number) => ( {({ field }: FieldProps) => } @@ -154,7 +156,7 @@ export const PoolSetupSection = () => { { - if (form.values.adminMultisig && form.values.adminMultisig.signers?.length <= 10) { + if (values.adminMultisig && values.adminMultisig.signers?.length <= 10) { push('') } }} @@ -207,12 +209,12 @@ export const PoolSetupSection = () => { {({ push, remove }) => ( <> - {form.values.poolFees.map((_, index) => ( + {values.poolFees.map((_, index) => ( Pool fees {index + 1} - {form.values.poolFees.length > 1 && ( + {values.poolFees.length > 1 && ( remove(index)}> @@ -330,7 +332,7 @@ export const PoolSetupSection = () => { {({ form }) => ( - {form.values.tranches.map((tranche, index) => ( + {values.tranches.map((tranche, index) => ( {({ field, meta }: FieldProps) => ( @@ -341,7 +343,7 @@ export const PoolSetupSection = () => { form.setFieldTouched(`subscriptionDocuments[${index}]`, true, false) form.setFieldValue(`subscriptionDocuments[${index}]`, file) }} - label={`Subscription document for ${tranche.trancheName}`} + label={`Subscription document for ${tranche.tokenName}`} errorMessage={meta.touched && meta.error ? meta.error : undefined} accept="application/pdf" small diff --git a/centrifuge-app/src/pages/IssuerCreatePool/types.ts b/centrifuge-app/src/pages/IssuerCreatePool/types.ts index f331e8590..9f1cfccdd 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/types.ts +++ b/centrifuge-app/src/pages/IssuerCreatePool/types.ts @@ -1,4 +1,4 @@ -import { PoolMetadataInput } from '@centrifuge/centrifuge-js' +import { FeeTypes, PoolMetadataInput } from '@centrifuge/centrifuge-js' import { isTestEnv } from '../../config' export interface Tranche { @@ -33,11 +33,40 @@ export const createPoolFee = () => ({ walletAddress: '', }) -export type CreatePoolValues = Omit & { +export type CreatePoolValues = Omit< + PoolMetadataInput, + 'poolIcon' | 'issuerLogo' | 'executiveSummary' | 'adminMultisig' | 'poolFees' | 'poolReport' | 'poolRatings' +> & { // pool structure assetDenomination: string + + // pool details + issuerCategories: { type: string; value: string }[] + poolIcon: File + issuerLogo: File + executiveSummary: File + + 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 + }[] + poolRatings: { + agency?: string + value?: string + reportUrl?: string + reportFile?: File | null + }[] } export const initialValues: CreatePoolValues = { From 3410bebc1253ac0335c1e2c58386d3004a6b499c Mon Sep 17 00:00:00 2001 From: katty barroso Date: Wed, 27 Nov 2024 16:17:35 +0100 Subject: [PATCH 05/17] Add deposit banner --- centrifuge-app/src/pages/IssuerCreatePool/index.tsx | 10 +++++++++- centrifuge-js/src/modules/pools.ts | 12 ++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx index 99f6d99c7..6660bce91 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx @@ -201,7 +201,7 @@ const IssuerCreatePoolPage = () => { const metadataValues: PoolMetadataInput = { ...values } as any // find the currency (asset denomination in UI) - const currency = currencies.find((c) => c.symbol === values.currency)! + const currency = currencies.find((c) => c.symbol === values.assetDenomination)! // Handle admin multisig metadataValues.adminMultisig = @@ -390,6 +390,14 @@ const IssuerCreatePoolPage = () => { + {step === 1 && ( + + + A deposit of 1100 CFG is required to create this pool. Please make sure you have sufficient funds + in your wallet. + + + )} {step === 1 && } {step === 2 && } diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index 34816d7e3..a70f61d1b 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -674,17 +674,17 @@ export interface PoolMetadataInput { // details poolName: string investorType: string - poolIcon: FileType | null + poolIcon: FileType poolType: 'open' | 'closed' maxReserve: number | '' issuerName: string - issuerRepName?: string - issuerLogo?: FileType | null + issuerRepName: string + issuerLogo: FileType | null issuerShortDescription: string issuerDescription: string - website?: string | '' - forum?: string | '' - email?: string | '' + website: string + forum: string + email: string executiveSummary?: FileType | null details?: IssuerDetail[] issuerCategories: { type: string; value: string; description?: string }[] From 36cd71cdc40fab5efabe2aefe8593941a9c2f1cc Mon Sep 17 00:00:00 2001 From: katty barroso Date: Wed, 27 Nov 2024 16:32:24 +0100 Subject: [PATCH 06/17] Fix linter errors --- centrifuge-app/src/pages/IssuerCreatePool/types.ts | 2 +- centrifuge-js/src/modules/pools.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/centrifuge-app/src/pages/IssuerCreatePool/types.ts b/centrifuge-app/src/pages/IssuerCreatePool/types.ts index 9f1cfccdd..6e33df302 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/types.ts +++ b/centrifuge-app/src/pages/IssuerCreatePool/types.ts @@ -19,7 +19,7 @@ export const createEmptyTranche = (trancheName: string): Tranche => ({ tokenName: trancheName, symbolName: '', interestRate: 0, - minRiskBuffer: trancheName === 'Junio' ? '' : 0, + minRiskBuffer: trancheName === 'Junior' ? '' : 0, minInvestment: 1000, apy: '90d', }) diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index a70f61d1b..abc343da9 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -679,13 +679,13 @@ export interface PoolMetadataInput { maxReserve: number | '' issuerName: string issuerRepName: string - issuerLogo: FileType | null + issuerLogo: FileType issuerShortDescription: string issuerDescription: string website: string forum: string email: string - executiveSummary?: FileType | null + executiveSummary: FileType | null details?: IssuerDetail[] issuerCategories: { type: string; value: string; description?: string }[] poolRatings: { @@ -714,6 +714,10 @@ export interface PoolMetadataInput { tranches: { [trancheId: string]: { agreement: FileType | undefined; openForOnboarding: boolean } } taxInfoRequired?: boolean } + + listed?: boolean + epochHours: number | '' + epochMinutes: number | '' } export type WithdrawAddress = { From 945bf45f1c633172dffea07fd014f39cff46cdd3 Mon Sep 17 00:00:00 2001 From: katty barroso Date: Thu, 28 Nov 2024 10:45:29 +0100 Subject: [PATCH 07/17] Add metadata values --- .../IssuerCreatePool/PoolDetailsSection.tsx | 4 +- .../IssuerCreatePool/PoolSetupSection.tsx | 2 - .../src/pages/IssuerCreatePool/index.tsx | 202 ++++++++---------- .../src/pages/IssuerCreatePool/types.ts | 6 +- .../src/pages/IssuerCreatePool/utils.ts | 33 +++ 5 files changed, 129 insertions(+), 118 deletions(-) create mode 100644 centrifuge-app/src/pages/IssuerCreatePool/utils.ts diff --git a/centrifuge-app/src/pages/IssuerCreatePool/PoolDetailsSection.tsx b/centrifuge-app/src/pages/IssuerCreatePool/PoolDetailsSection.tsx index 97178d4d7..e2ae9f2c9 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/PoolDetailsSection.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/PoolDetailsSection.tsx @@ -29,8 +29,6 @@ export const PoolDetailsSection = () => { const form = useFormikContext() const createLabel = (label: string) => `${label}${isTestEnv ? '' : '*'}` - console.log(form.values) - return ( @@ -299,9 +297,9 @@ export const PoolDetailsSection = () => { onFileChange={(file) => { form.setFieldValue('reportAuthorAvatar', file) }} - accept="application/pdf" label="Reviewer avatar" placeholder="Choose file" + accept="image/*" small /> )} diff --git a/centrifuge-app/src/pages/IssuerCreatePool/PoolSetupSection.tsx b/centrifuge-app/src/pages/IssuerCreatePool/PoolSetupSection.tsx index e3fa42588..27678a12c 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/PoolSetupSection.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/PoolSetupSection.tsx @@ -33,8 +33,6 @@ export const PoolSetupSection = () => { const form = useFormikContext() const { values } = form - console.log(values) - return ( diff --git a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx index 6660bce91..b28c90466 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx @@ -1,8 +1,11 @@ import { AddFee, CurrencyKey, + FileType, isSameAddress, + Perquintill, PoolMetadataInput, + Rate, TrancheInput, TransactionOptions, } from '@centrifuge/centrifuge-js' @@ -11,7 +14,7 @@ import { createKeyMulti, sortAddresses } from '@polkadot/util-crypto' import BN from 'bn.js' import { Form, FormikProvider, useFormik } from 'formik' import { useEffect, useRef, useState } from 'react' -import { combineLatest, switchMap } from 'rxjs' +import { combineLatest, firstValueFrom, switchMap } from 'rxjs' import styled, { useTheme } from 'styled-components' import { useAddress, @@ -30,6 +33,7 @@ import { PoolDetailsSection } from './PoolDetailsSection' import { PoolSetupSection } from './PoolSetupSection' import { Line, PoolStructureSection } from './PoolStructureSection' import { CreatePoolValues, initialValues } from './types' +import { pinFileIfExists, pinFiles } from './utils' import { validateValues } from './validate' const StyledBox = styled(Box)` @@ -196,14 +200,90 @@ const IssuerCreatePoolPage = () => { validateOnMount: true, onSubmit: async (values, { setSubmitting }) => { const poolId = await centrifuge.pools.getAvailablePoolId() + if (!currencies || !address) return const metadataValues: PoolMetadataInput = { ...values } as any - // find the currency (asset denomination in UI) - const currency = currencies.find((c) => c.symbol === values.assetDenomination)! + // Find the currency (asset denomination in UI) + const currency = currencies.find((c) => c.symbol.toLowerCase() === values.assetDenomination)! + + // Handle pining files for ipfs + if (!values.poolIcon) return + + const filesToPin = { + poolIcon: values.poolIcon, + issuerLogo: values.issuerLogo, + executiveSummary: values.executiveSummary, + authorAvatar: values.reportAuthorAvatar, + } + + const pinnedFiles = await pinFiles(centrifuge, filesToPin) + if (pinnedFiles.poolIcon) metadataValues.poolIcon = pinnedFiles.poolIcon as FileType + if (pinnedFiles.issuerLogo) metadataValues.issuerLogo = pinnedFiles.issuerLogo as FileType + if (pinnedFiles.executiveSummary) metadataValues.executiveSummary = pinnedFiles.executiveSummary + + // Pool report + if (values.reportUrl) { + metadataValues.poolReport = { + authorAvatar: pinnedFiles.authorAvatar, + authorName: values.reportAuthorName, + authorTitle: values.reportAuthorTitle, + url: values.reportUrl, + } + } + + // Pool ratings + if (values.poolRatings) { + const newRatingReports = await Promise.all( + values.poolRatings.map((rating) => pinFileIfExists(centrifuge, rating.reportFile ?? null)) + ) + + const ratings = values.poolRatings.map((rating, index) => { + const pinnedReport = newRatingReports[index] + return { + agency: rating.agency ?? '', + value: rating.value ?? '', + reportUrl: rating.reportUrl ?? '', + reportFile: pinnedReport ? { uri: pinnedReport.uri, mime: rating.reportFile?.type ?? '' } : null, + } + }) + + metadataValues.poolRatings = ratings + } + + // Organize tranches + const nonJuniorTranches = metadataValues.tranches.slice(1) + const tranches = [ + {}, + ...nonJuniorTranches.map((tranche) => ({ + interestRatePerSec: Rate.fromAprPercent(tranche.interestRate), + minRiskBuffer: Perquintill.fromPercent(tranche.minRiskBuffer), + })), + ] + + // Pool fees + 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, + })) - // Handle admin multisig + // Multisign metadataValues.adminMultisig = values.adminMultisigEnabled && values.adminMultisig.threshold > 1 ? { @@ -212,111 +292,9 @@ const IssuerCreatePoolPage = () => { } : undefined - // // Get the currency for the pool - - // // Pool ID and required assets - // if (!values.poolIcon) { - // 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 (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 = 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, - // } - // } - - // // Handle pool ratings - // 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 - // } - - // // Organize tranches - // const nonJuniorTranches = metadataValues.tranches.slice(1) - // const tranches = [ - // {}, - // ...nonJuniorTranches.map((tranche) => ({ - // interestRatePerSec: Rate.fromAprPercent(tranche.interestRate), - // minRiskBuffer: Perquintill.fromPercent(tranche.minRiskBuffer), - // })), - // ] - - // // Pool fees - // 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) - // } + if (metadataValues.adminMultisig && metadataValues.adminMultisig.threshold > 1) { + addMultisig(metadataValues.adminMultisig) + } // createProxies([ // (aoProxy, adminProxy) => { @@ -361,7 +339,11 @@ const IssuerCreatePoolPage = () => { } const handleNextStep = () => { - setStep((prevStep) => prevStep + 1) + if (step === 3) { + form.handleSubmit() + } else { + setStep((prevStep) => prevStep + 1) + } } useEffect(() => { diff --git a/centrifuge-app/src/pages/IssuerCreatePool/types.ts b/centrifuge-app/src/pages/IssuerCreatePool/types.ts index 6e33df302..160d2ebdc 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/types.ts +++ b/centrifuge-app/src/pages/IssuerCreatePool/types.ts @@ -42,9 +42,9 @@ export type CreatePoolValues = Omit< // pool details issuerCategories: { type: string; value: string }[] - poolIcon: File - issuerLogo: File - executiveSummary: File + poolIcon: File | null + issuerLogo: File | null + executiveSummary: File | null reportAuthorName: string reportAuthorTitle: string diff --git a/centrifuge-app/src/pages/IssuerCreatePool/utils.ts b/centrifuge-app/src/pages/IssuerCreatePool/utils.ts new file mode 100644 index 000000000..dfd5d64d7 --- /dev/null +++ b/centrifuge-app/src/pages/IssuerCreatePool/utils.ts @@ -0,0 +1,33 @@ +import { FileType } from '@centrifuge/centrifuge-js' +import { useCentrifuge } from '@centrifuge/centrifuge-react' +import { lastValueFrom } from 'rxjs' +import { getFileDataURI } from '../../../src/utils/getFileDataURI' + +const pinFile = async (centrifuge: ReturnType, file: File): Promise => { + const pinned = await lastValueFrom(centrifuge.metadata.pinFile(await getFileDataURI(file))) + return { uri: pinned.uri, mime: file.type } +} + +export const pinFileIfExists = async (centrifuge: ReturnType, file: File | null) => + file ? pinFile(centrifuge, file) : Promise.resolve(null) + +export const pinFiles = async (centrifuge: ReturnType, files: { [key: string]: File | null }) => { + const promises = Object.entries(files).map(async ([key, file]) => { + const pinnedFile = await pinFileIfExists(centrifuge, file) + return { key, pinnedFile } + }) + + const results = await Promise.all(promises) + + return results.reduce((acc, { key, pinnedFile }) => { + if (pinnedFile) { + acc[key] = { + uri: pinnedFile.uri, + mime: files[key]?.type || '', + } + } else { + acc[key] = null + } + return acc + }, {} as { [key: string]: { uri: string; mime: string } | null }) +} From 86cf627f7837c551f9d315ae08c913606785b71f Mon Sep 17 00:00:00 2001 From: katty barroso Date: Thu, 28 Nov 2024 13:07:38 +0100 Subject: [PATCH 08/17] Cleanup types --- .../IssuerCreatePool/PoolDetailsSection.tsx | 1 - .../src/pages/IssuerCreatePool/index.tsx | 65 ++++++++++++------- .../src/pages/IssuerCreatePool/types.ts | 30 +++++---- centrifuge-js/src/modules/pools.ts | 29 +-------- 4 files changed, 59 insertions(+), 66 deletions(-) diff --git a/centrifuge-app/src/pages/IssuerCreatePool/PoolDetailsSection.tsx b/centrifuge-app/src/pages/IssuerCreatePool/PoolDetailsSection.tsx index e2ae9f2c9..a026f9170 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/PoolDetailsSection.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/PoolDetailsSection.tsx @@ -191,7 +191,6 @@ export const PoolDetailsSection = () => { placeholder="Type here..." maxLength={1000} errorMessage={meta.touched && meta.error ? meta.error : undefined} - // disabled={waitingForStoredIssuer} /> )} diff --git a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx index b28c90466..a375e0813 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx @@ -32,7 +32,7 @@ import { config } from '../../config' import { PoolDetailsSection } from './PoolDetailsSection' import { PoolSetupSection } from './PoolSetupSection' import { Line, PoolStructureSection } from './PoolStructureSection' -import { CreatePoolValues, initialValues } from './types' +import { CreatePoolValues, initialValues, PoolFee } from './types' import { pinFileIfExists, pinFiles } from './utils' import { validateValues } from './validate' @@ -252,36 +252,53 @@ const IssuerCreatePoolPage = () => { metadataValues.poolRatings = ratings } - // Organize tranches - const nonJuniorTranches = metadataValues.tranches.slice(1) - const tranches = [ - {}, - ...nonJuniorTranches.map((tranche) => ({ - interestRatePerSec: Rate.fromAprPercent(tranche.interestRate), - minRiskBuffer: Perquintill.fromPercent(tranche.minRiskBuffer), - })), - ] + // Tranches + const tranches = metadataValues.tranches.map((tranche, index) => { + const trancheType = + index === 0 + ? 'Residual' + : { + NonResidual: { + interestRatePerSec: Rate.fromAprPercent(tranche.interestRate).toString(), + minRiskBuffer: Perquintill.fromPercent(tranche.minRiskBuffer).toString(), + }, + } + + return { + trancheType, + metadata: { + tokenName: tranche.tokenName, + tokenSymbol: tranche.symbolName, + }, + } + }) // Pool fees const feeId = await firstValueFrom(centrifuge.pools.getNextPoolFeeId()) - const poolFees: AddFee['fee'][] = values.poolFees.map((fee, i) => { - return { + const metadataPoolFees: Pick[] = [] + const feeInput: Array<[string, { destination: string; editor: any; feeType: any }]> = [] + + values.poolFees.forEach((fee, index) => { + metadataPoolFees.push({ name: fee.name, - destination: fee.walletAddress, - amount: Rate.fromPercent(fee.percentOfNav), - feeType: fee.feeType, - limit: 'ShareOfPortfolioValuation', - account: fee.feeType === 'chargedUpTo' ? fee.walletAddress : undefined, + id: feeId ? feeId + index : 0, feePosition: fee.feePosition, - } + feeType: fee.feeType, + }) + + feeInput.push([ + 'Top', + { + destination: fee.walletAddress, + editor: fee.feeType === 'chargedUpTo' ? { account: fee.walletAddress } : 'Root', + feeType: { + [fee.feeType]: { limit: { ['ShareOfPortfolioValuation']: Rate.fromPercent(fee.percentOfNav) } }, + }, + }, + ]) }) - metadataValues.poolFees = poolFees.map((fee, i) => ({ - name: fee.name, - id: feeId + i, - feePosition: fee.feePosition, - feeType: fee.feeType, - })) + metadataValues.poolFees = metadataPoolFees // Multisign metadataValues.adminMultisig = diff --git a/centrifuge-app/src/pages/IssuerCreatePool/types.ts b/centrifuge-app/src/pages/IssuerCreatePool/types.ts index 160d2ebdc..67a5b1f14 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/types.ts +++ b/centrifuge-app/src/pages/IssuerCreatePool/types.ts @@ -9,6 +9,16 @@ export interface Tranche { apy: string interestRate: number | '' } + +export interface PoolFee { + id: number + name: string + feeType: FeeTypes + percentOfNav: string + walletAddress: string + feePosition: 'Top of waterfall' + category: string +} export interface WriteOffGroupInput { days: number | '' writeOff: number | '' @@ -24,11 +34,12 @@ export const createEmptyTranche = (trancheName: string): Tranche => ({ apy: '90d', }) -export const createPoolFee = () => ({ +export const createPoolFee = (): PoolFee => ({ + id: 0, name: '', category: '', - feePosition: '', - feeType: '', + feePosition: 'Top of waterfall', + feeType: '' as FeeTypes, percentOfNav: '', walletAddress: '', }) @@ -52,15 +63,7 @@ export type CreatePoolValues = Omit< reportUrl: string adminMultisigEnabled: boolean adminMultisig: Exclude - poolFees: { - id?: number - name: string - feeType: FeeTypes - percentOfNav: number | '' - walletAddress: string - feePosition: 'Top of waterfall' - category: string - }[] + poolFees: PoolFee[] poolRatings: { agency?: string value?: string @@ -73,14 +76,13 @@ export const initialValues: CreatePoolValues = { // pool structure poolStructure: 'revolving', assetClass: 'Private credit', - assetDenomination: '', + assetDenomination: isTestEnv ? 'USDC' : 'Native USDC', subAssetClass: '', tranches: [createEmptyTranche('Junior')], // pool details section poolName: '', poolIcon: null, - currency: isTestEnv ? 'USDC' : 'Native USDC', maxReserve: 1000000, investorType: '', issuerName: '', diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index abc343da9..f128749a4 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -1040,31 +1040,6 @@ export function getPoolsModule(inst: Centrifuge) { options?: TransactionOptions ) { const [admin, poolId, tranches, currency, maxReserve, metadata, fees] = args - const trancheInput = tranches.map((t, i) => ({ - trancheType: t.interestRatePerSec - ? { - NonResidual: { - interestRatePerSec: t.interestRatePerSec.toString(), - minRiskBuffer: t.minRiskBuffer?.toString(), - }, - } - : 'Residual', - metadata: { - tokenName: metadata.tranches[i].tokenName, - tokenSymbol: metadata.tranches[i].symbolName, - }, - })) - - const feeInput = fees.map((fee) => { - return [ - 'Top', - { - destination: fee.destination, - editor: fee?.account ? { account: fee.account } : 'Root', - feeType: { [fee.feeType]: { limit: { [fee.limit]: fee?.amount } } }, - }, - ] - }) return inst.getApi().pipe( switchMap((api) => @@ -1078,12 +1053,12 @@ export function getPoolsModule(inst: Centrifuge) { const tx = api.tx.poolRegistry.register( admin, poolId, - trancheInput, + tranches, currency, maxReserve.toString(), pinnedMetadata.ipfsHash, [], - feeInput + fees ) if (options?.createType === 'propose') { const proposalTx = api.tx.utility.batchAll([ From 4c087706dc867f31a0f83a826b0add602ffbcd60 Mon Sep 17 00:00:00 2001 From: katty barroso Date: Thu, 28 Nov 2024 16:39:39 +0100 Subject: [PATCH 09/17] Add onboarding functionality and UI fixes --- .../src/components/Menu/IssuerMenu.tsx | 4 +- centrifuge-app/src/components/Menu/Toggle.tsx | 2 +- centrifuge-app/src/components/Menu/index.tsx | 4 + .../IssuerCreatePool/PoolSetupSection.tsx | 100 ++++++++++++------ .../src/pages/IssuerCreatePool/index.tsx | 13 ++- fabric/src/icon-svg/icon-chevron-down.svg | 2 +- 6 files changed, 88 insertions(+), 37 deletions(-) diff --git a/centrifuge-app/src/components/Menu/IssuerMenu.tsx b/centrifuge-app/src/components/Menu/IssuerMenu.tsx index 24ad7b9a8..1e2385649 100644 --- a/centrifuge-app/src/components/Menu/IssuerMenu.tsx +++ b/centrifuge-app/src/components/Menu/IssuerMenu.tsx @@ -62,9 +62,9 @@ export function IssuerMenu({ defaultOpen = false, children }: IssuerMenuProps) { Issuer {isLarge && (open ? ( - + ) : ( - + ))} diff --git a/centrifuge-app/src/components/Menu/Toggle.tsx b/centrifuge-app/src/components/Menu/Toggle.tsx index 8737377fe..08838a5cf 100644 --- a/centrifuge-app/src/components/Menu/Toggle.tsx +++ b/centrifuge-app/src/components/Menu/Toggle.tsx @@ -10,7 +10,7 @@ export const Toggle = styled(Text)<{ isActive?: boolean; stacked?: boolean }>` width: 100%; grid-template-columns: ${({ stacked, theme }) => stacked ? '1fr' : `${theme.sizes.iconSmall}px 1fr ${theme.sizes.iconSmall}px`}; - color: ${({ isActive, theme }) => (isActive ? theme.colors.textGold : theme.colors.textInverted)}; + color: ${({ theme }) => theme.colors.textInverted}; border-radius: 4px; background-color: ${({ isActive }) => (isActive ? LIGHT_BACKGROUND : 'transparent')}; diff --git a/centrifuge-app/src/components/Menu/index.tsx b/centrifuge-app/src/components/Menu/index.tsx index 1c3e32191..5fe10a79d 100644 --- a/centrifuge-app/src/components/Menu/index.tsx +++ b/centrifuge-app/src/components/Menu/index.tsx @@ -40,6 +40,10 @@ const StyledRouterLinkButton = styled(RouterLinkButton)` background-color: ${COLOR}; color: white; } + + &:active { + border-color: transparent; + } } ` diff --git a/centrifuge-app/src/pages/IssuerCreatePool/PoolSetupSection.tsx b/centrifuge-app/src/pages/IssuerCreatePool/PoolSetupSection.tsx index 27678a12c..1968843ea 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/PoolSetupSection.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/PoolSetupSection.tsx @@ -28,6 +28,27 @@ const FEE_TYPES = [ const FEE_POSISTIONS = [{ label: 'Top of waterfall', value: 'Top of waterfall' }] +const TaxDocument = () => { + const form = useFormikContext() + + return ( + + Tax document requirement + + + {({ field }: FieldProps) => ( + form.setFieldValue('onboarding.taxInfoRequired', val.target.checked ? true : false)} + /> + )} + + + ) +} + export const PoolSetupSection = () => { const theme = useTheme() const form = useFormikContext() @@ -327,39 +348,54 @@ export const PoolSetupSection = () => { {values.onboardingExperience === 'centrifuge' && ( - - {({ form }) => ( - - {values.tranches.map((tranche, index) => ( - - {({ field, meta }: FieldProps) => ( - - { - form.setFieldTouched(`subscriptionDocuments[${index}]`, true, false) - form.setFieldValue(`subscriptionDocuments[${index}]`, file) - }} - label={`Subscription document for ${tranche.tokenName}`} - errorMessage={meta.touched && meta.error ? meta.error : undefined} - accept="application/pdf" - small - /> - - )} - - ))} - - )} - - - Tax document requirement - + + {values.tranches.map((tranche, index) => ( + + {({ field, meta }: FieldProps) => ( + + { + form.setFieldTouched(`onboarding.tranches.${tranche.tokenName}`, true, false) + form.setFieldValue(`onboarding.tranches.${tranche.tokenName}`, file) + }} + label={`Subscription document for ${tranche.tokenName}`} + errorMessage={meta.touched && meta.error ? meta.error : undefined} + accept="application/pdf" + small + /> + + )} + + ))} + + + )} + {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/index.tsx b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx index a375e0813..c8539b846 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx @@ -267,7 +267,8 @@ const IssuerCreatePoolPage = () => { return { trancheType, metadata: { - tokenName: tranche.tokenName, + tokenName: + metadataValues.tranches.length > 1 ? `${metadataValues.poolName} ${tranche.tokenName}` : 'Junior', tokenSymbol: tranche.symbolName, }, } @@ -313,6 +314,16 @@ const IssuerCreatePoolPage = () => { addMultisig(metadataValues.adminMultisig) } + // Onboarding + if (metadataValues.onboardingExperience === 'none') { + metadataValues.onboarding = { + taxInfoRequired: metadataValues.onboarding?.taxInfoRequired, + tranches: {}, + } + } + + console.log(metadataValues) + // createProxies([ // (aoProxy, adminProxy) => { // createPoolTx( diff --git a/fabric/src/icon-svg/icon-chevron-down.svg b/fabric/src/icon-svg/icon-chevron-down.svg index e70d2ea0d..493072f00 100644 --- a/fabric/src/icon-svg/icon-chevron-down.svg +++ b/fabric/src/icon-svg/icon-chevron-down.svg @@ -1,3 +1,3 @@ - + From 92af4ab58d271f2256ee0e98bf11850a80dc2943 Mon Sep 17 00:00:00 2001 From: katty barroso Date: Fri, 29 Nov 2024 17:28:58 +0100 Subject: [PATCH 10/17] Add proxies functionality --- .../pages/IssuerCreatePool/AdminMultisig.tsx | 2 +- .../IssuerCreatePool/PoolSetupSection.tsx | 61 ++++++++++++--- .../src/pages/IssuerCreatePool/index.tsx | 77 +++++++++++-------- .../src/pages/IssuerCreatePool/types.ts | 7 +- centrifuge-js/src/modules/pools.ts | 36 ++++++++- fabric/src/components/Toast/index.tsx | 2 +- 6 files changed, 135 insertions(+), 50 deletions(-) diff --git a/centrifuge-app/src/pages/IssuerCreatePool/AdminMultisig.tsx b/centrifuge-app/src/pages/IssuerCreatePool/AdminMultisig.tsx index 68476ce01..5ac4c2de4 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/AdminMultisig.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/AdminMultisig.tsx @@ -1,9 +1,9 @@ import { useWallet } from '@centrifuge/centrifuge-react' import { Button } from '@centrifuge/fabric' import { useFormikContext } from 'formik' -import { CreatePoolValues } from '.' import { PageSection } from '../../components/PageSection' import { MultisigForm } from '../IssuerPool/Access/MultisigForm' +import { CreatePoolValues } from './types' export function AdminMultisigSection() { const form = useFormikContext() diff --git a/centrifuge-app/src/pages/IssuerCreatePool/PoolSetupSection.tsx b/centrifuge-app/src/pages/IssuerCreatePool/PoolSetupSection.tsx index 1968843ea..2d8a0ed83 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/PoolSetupSection.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/PoolSetupSection.tsx @@ -1,4 +1,5 @@ -import { PoolMetadataInput } from '@centrifuge/centrifuge-js' +import { addressToHex, evmToSubstrateAddress, PoolMetadataInput } from '@centrifuge/centrifuge-js' +import { useCentEvmChainId } from '@centrifuge/centrifuge-react' import { Box, Checkbox, @@ -18,6 +19,7 @@ import { useTheme } from 'styled-components' import { FieldWithErrorMessage } from '../../../src/components/FieldWithErrorMessage' import { Tooltips } from '../../../src/components/Tooltips' import { feeCategories } from '../../../src/config' +import { isEvmAddress } from '../../../src/utils/address' import { AddButton } from './PoolDetailsSection' import { CheckboxOption, Line, StyledGrid } from './PoolStructureSection' @@ -51,9 +53,12 @@ const TaxDocument = () => { export const PoolSetupSection = () => { const theme = useTheme() + const chainId = useCentEvmChainId() const form = useFormikContext() const { values } = form + console.log(values) + return ( @@ -94,7 +99,24 @@ export const PoolSetupSection = () => { values.adminMultisig?.signers?.map((_, index) => ( - {({ field }: FieldProps) => } + {({ field, form }: FieldProps) => ( + { + form.setFieldValue(`adminMultisig.signers.${index}`, val.target.value) + }} + onBlur={() => { + const value = form.values.adminMultisig.signers[index] + if (value) { + const transformedValue = isEvmAddress(value) + ? evmToSubstrateAddress(value, chainId ?? 0) + : value + form.setFieldValue(`adminMultisig.signers.${index}`, transformedValue) + } + }} + /> + )} )) @@ -124,19 +146,19 @@ export const PoolSetupSection = () => { - + {({ field, meta, form }: FieldProps) => ( { /> )} - {_.type === 'other' && ( + {category.type === 'other' && ( {({ field, meta }: FieldProps) => ( @@ -63,7 +63,7 @@ export const IssuerCategoriesSection = () => { )} - {({ field, meta }: FieldProps) => ( + {({ field }: FieldProps) => ( Date: Tue, 3 Dec 2024 15:51:13 +0100 Subject: [PATCH 15/17] wip --- centrifuge-app/src/pages/IssuerCreatePool/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx index aeaae17e1..85e1a8493 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx @@ -220,8 +220,9 @@ const IssuerCreatePoolPage = () => { }, { onSuccess: (args, result) => { - const event = result.events.find(({ event }) => api.events.poolRegistry.Created.is(event)) - if (form.values.adminMultisigEnabled && form.values.adminMultisig.threshold > 1) setIsMultisigDialogOpen(true) + if (form.values.adminMultisigEnabled && form.values.adminMultisig.threshold > 1) { + setIsMultisigDialogOpen(true) + } const [, , , , poolId] = args console.log(poolId, result) if (createType === 'immediate') { From a8f299f259705e7640617b8d95d832f6926ff5d1 Mon Sep 17 00:00:00 2001 From: katty barroso Date: Tue, 3 Dec 2024 16:48:19 +0100 Subject: [PATCH 16/17] Add waiting before redirecting to avoid error --- .../src/pages/IssuerCreatePool/index.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx index 85e1a8493..502787c0c 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx +++ b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx @@ -33,6 +33,7 @@ import { Dec } from '../../../src/utils/Decimal' import { useCreatePoolFee } from '../../../src/utils/useCreatePoolFee' import { usePoolCurrencies } from '../../../src/utils/useCurrencies' import { useIsAboveBreakpoint } from '../../../src/utils/useIsAboveBreakpoint' +import { usePools } from '../../../src/utils/usePools' import { config } from '../../config' import { PoolDetailsSection } from './PoolDetailsSection' import { PoolSetupSection } from './PoolSetupSection' @@ -78,6 +79,7 @@ const IssuerCreatePoolPage = () => { const currencies = usePoolCurrencies() const centrifuge = useCentrifuge() const api = useCentrifugeApi() + const pools = usePools() const { poolCreationType } = useDebugFlags() const consts = useCentrifugeConsts() const { chainDecimals } = useCentrifugeConsts() @@ -93,6 +95,7 @@ const IssuerCreatePoolPage = () => { const [createdModal, setCreatedModal] = useState(false) const [preimageHash, setPreimageHash] = useState('') const [isPreimageDialogOpen, setIsPreimageDialogOpen] = useState(false) + const [createdPoolId, setCreatedPoolId] = useState('') useEffect(() => { if (createType === 'notePreimage') { @@ -103,7 +106,6 @@ 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) }) @@ -113,6 +115,16 @@ 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) => { @@ -226,7 +238,7 @@ const IssuerCreatePoolPage = () => { const [, , , , poolId] = args console.log(poolId, result) if (createType === 'immediate') { - navigate(`/pools/${poolId}`) + setCreatedPoolId(poolId) } else { setCreatedModal(true) } From b0f59828036a13b0561a7b405249cb7e4b66ceab Mon Sep 17 00:00:00 2001 From: katty barroso Date: Tue, 3 Dec 2024 16:48:38 +0100 Subject: [PATCH 17/17] Remove default empty pool fee --- centrifuge-app/src/pages/IssuerCreatePool/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/centrifuge-app/src/pages/IssuerCreatePool/types.ts b/centrifuge-app/src/pages/IssuerCreatePool/types.ts index 2de9c8b70..12d64725d 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/types.ts +++ b/centrifuge-app/src/pages/IssuerCreatePool/types.ts @@ -108,7 +108,7 @@ export const initialValues: CreatePoolValues = { threshold: 1, }, adminMultisigEnabled: false, - poolFees: [createPoolFee()], + poolFees: [], poolType: 'open', onboarding: {