Skip to content

Commit

Permalink
Add create pool existing functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
kattylucy committed Nov 26, 2024
1 parent 230a03d commit 23cd486
Show file tree
Hide file tree
Showing 3 changed files with 315 additions and 3 deletions.
312 changes: 310 additions & 2 deletions centrifuge-app/src/pages/IssuerCreatePool/index.tsx
Original file line number Diff line number Diff line change
@@ -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)`
Expand All @@ -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<HTMLFormElement>(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 }>()

Check warning on line 85 in centrifuge-app/src/pages/IssuerCreatePool/index.tsx

View workflow job for this annotation

GitHub Actions / build-app

'multisigData' is assigned a value but never used

Check warning on line 85 in centrifuge-app/src/pages/IssuerCreatePool/index.tsx

View workflow job for this annotation

GitHub Actions / ff-prod / build-app

'multisigData' is assigned a value but never used
const [createdPoolId, setCreatedPoolId] = useState('')

Check warning on line 86 in centrifuge-app/src/pages/IssuerCreatePool/index.tsx

View workflow job for this annotation

GitHub Actions / build-app

'createdPoolId' is assigned a value but never used

Check warning on line 86 in centrifuge-app/src/pages/IssuerCreatePool/index.tsx

View workflow job for this annotation

GitHub Actions / ff-prod / build-app

'createdPoolId' is assigned a value but never used
const [isMultisigDialogOpen, setIsMultisigDialogOpen] = useState(false)

Check warning on line 87 in centrifuge-app/src/pages/IssuerCreatePool/index.tsx

View workflow job for this annotation

GitHub Actions / build-app

'isMultisigDialogOpen' is assigned a value but never used

Check warning on line 87 in centrifuge-app/src/pages/IssuerCreatePool/index.tsx

View workflow job for this annotation

GitHub Actions / ff-prod / build-app

'isMultisigDialogOpen' is assigned a value but never used

const { execute: createProxies, isLoading: createProxiesIsPending } = useCentrifugeTransaction(

Check warning on line 89 in centrifuge-app/src/pages/IssuerCreatePool/index.tsx

View workflow job for this annotation

GitHub Actions / build-app

'createProxiesIsPending' is assigned a value but never used

Check warning on line 89 in centrifuge-app/src/pages/IssuerCreatePool/index.tsx

View workflow job for this annotation

GitHub Actions / ff-prod / build-app

'createProxiesIsPending' is assigned a value but never used
`${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(

Check warning on line 116 in centrifuge-app/src/pages/IssuerCreatePool/index.tsx

View workflow job for this annotation

GitHub Actions / build-app

'transactionIsPending' is assigned a value but never used

Check warning on line 116 in centrifuge-app/src/pages/IssuerCreatePool/index.tsx

View workflow job for this annotation

GitHub Actions / ff-prod / build-app

'transactionIsPending' is assigned a value but never used
`${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<FileType> => {
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())

Check warning on line 354 in centrifuge-app/src/pages/IssuerCreatePool/index.tsx

View workflow job for this annotation

GitHub Actions / build-app

'deposit' is assigned a value but never used

Check warning on line 354 in centrifuge-app/src/pages/IssuerCreatePool/index.tsx

View workflow job for this annotation

GitHub Actions / ff-prod / build-app

'deposit' is assigned a value but never used

const { values, errors } = form

const checkStepCompletion = (stepNumber: number) => {
Expand Down
3 changes: 3 additions & 0 deletions centrifuge-app/src/pages/IssuerCreatePool/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion centrifuge-app/src/pages/IssuerCreatePool/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
positiveNumber,
required,
} from '../../utils/validation'
import { CreatePoolValues } from './types'

export const MB = 1024 ** 2

Expand Down Expand Up @@ -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<any> = {}

const tokenNames = new Set<string>()
Expand Down

0 comments on commit 23cd486

Please sign in to comment.