From f8b251db4721c9086b300fdd277709e9826b27f4 Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Mon, 12 Jun 2023 17:11:54 +0200 Subject: [PATCH] Centrifuge App: Fix pool role and IPFS URIs (#1446) --- .../pages/IssuerCreatePool/useStoredIssuer.ts | 2 +- .../pages/IssuerPool/Access/PoolManagers.tsx | 4 ++-- .../pages/IssuerPool/Configuration/Admins.tsx | 6 ++--- .../Configuration/ViewLoanTemplate.tsx | 2 +- .../IssuerPool/Investors/InvestorStatus.tsx | 2 +- .../Investors/OnboardingSettings.tsx | 8 +++---- .../src/pages/IssuerPool/Investors/index.tsx | 2 +- centrifuge-app/src/pages/Loan/index.tsx | 2 +- centrifuge-app/src/utils/parseMetadataUrl.ts | 4 +++- centrifuge-app/src/utils/usePermissions.ts | 4 ++-- centrifuge-js/src/modules/metadata.ts | 11 +++++----- centrifuge-js/src/modules/pools.ts | 22 ++++++++++--------- .../Transactions/TransactionToasts.tsx | 7 +++--- onboarding-api/README.md | 2 +- onboarding-api/src/utils/centrifuge.ts | 6 ++--- 15 files changed, 44 insertions(+), 40 deletions(-) diff --git a/centrifuge-app/src/pages/IssuerCreatePool/useStoredIssuer.ts b/centrifuge-app/src/pages/IssuerCreatePool/useStoredIssuer.ts index e227208301..2a22860739 100644 --- a/centrifuge-app/src/pages/IssuerCreatePool/useStoredIssuer.ts +++ b/centrifuge-app/src/pages/IssuerCreatePool/useStoredIssuer.ts @@ -16,7 +16,7 @@ export function useStoredIssuer() { if (!allPools || !permissions) { return [] } - return allPools.filter(({ id, metadata }) => permissions?.pools[id]?.roles.includes('PoolAdmin') && metadata) + return allPools.filter(({ id, metadata }) => permissions?.pools[id]?.roles.includes('InvestorAdmin') && metadata) }, [allPools, permissions]) const { data, isLoading } = usePoolMetadata(pools[0]) diff --git a/centrifuge-app/src/pages/IssuerPool/Access/PoolManagers.tsx b/centrifuge-app/src/pages/IssuerPool/Access/PoolManagers.tsx index be478809f2..80ebac3ffd 100644 --- a/centrifuge-app/src/pages/IssuerPool/Access/PoolManagers.tsx +++ b/centrifuge-app/src/pages/IssuerPool/Access/PoolManagers.tsx @@ -106,9 +106,9 @@ export function PoolManagers({ poolId }: { poolId: string }) { storedManagerPermissions, values.adminMultisig.signers.map((address) => ({ address, - roles: { MemberListAdmin: true, LiquidityAdmin: true }, + roles: { InvestorAdmin: true, LiquidityAdmin: true }, })), - ['LiquidityAdmin', 'MemberListAdmin'] + ['LiquidityAdmin', 'InvestorAdmin'] ), newPoolMetadata, ], diff --git a/centrifuge-app/src/pages/IssuerPool/Configuration/Admins.tsx b/centrifuge-app/src/pages/IssuerPool/Configuration/Admins.tsx index d933ceef00..ef6a72dc4a 100644 --- a/centrifuge-app/src/pages/IssuerPool/Configuration/Admins.tsx +++ b/centrifuge-app/src/pages/IssuerPool/Configuration/Admins.tsx @@ -12,7 +12,7 @@ import { Tooltips } from '../../../components/Tooltips' import { usePoolPermissions, useSuitableAccounts } from '../../../utils/usePermissions' import { AddAddressInput } from './AddAddressInput' -type AdminRole = 'PoolAdmin' | 'Borrower' | 'PricingAdmin' | 'LiquidityAdmin' | 'MemberListAdmin' | 'LoanAdmin' +type AdminRole = 'PoolAdmin' | 'Borrower' | 'PricingAdmin' | 'LiquidityAdmin' | 'InvestorAdmin' | 'LoanAdmin' type Admin = { address: string @@ -157,7 +157,7 @@ export function Admins({ poolId }: { poolId: string }) { header: , cell: (row: Row) => ( [admin.address, admin.roles])) diff --git a/centrifuge-app/src/pages/IssuerPool/Configuration/ViewLoanTemplate.tsx b/centrifuge-app/src/pages/IssuerPool/Configuration/ViewLoanTemplate.tsx index e1a5c49e85..7b2d587ad1 100644 --- a/centrifuge-app/src/pages/IssuerPool/Configuration/ViewLoanTemplate.tsx +++ b/centrifuge-app/src/pages/IssuerPool/Configuration/ViewLoanTemplate.tsx @@ -18,7 +18,7 @@ export const ViewLoanTemplate: React.FC = () => { const { pid: poolId, sid: templateId } = useParams<{ pid: string; sid: string }>() const pool = usePool(poolId) const { data: poolMetadata } = usePoolMetadata(pool) - const { data: templateData } = useMetadata(`ipfs://ipfs/${templateId}`) + const { data: templateData } = useMetadata(`ipfs://${templateId}`) return ( <> diff --git a/centrifuge-app/src/pages/IssuerPool/Investors/InvestorStatus.tsx b/centrifuge-app/src/pages/IssuerPool/Investors/InvestorStatus.tsx index 112b1e2bb9..b0adecf3d6 100644 --- a/centrifuge-app/src/pages/IssuerPool/Investors/InvestorStatus.tsx +++ b/centrifuge-app/src/pages/IssuerPool/Investors/InvestorStatus.tsx @@ -31,7 +31,7 @@ export const InvestorStatus: React.FC = () => { const permissions = usePermissions(validAddress) const [pendingTrancheId, setPendingTrancheId] = React.useState('') - const [account] = useSuitableAccounts({ poolId, poolRole: ['MemberListAdmin'] }) + const [account] = useSuitableAccounts({ poolId, poolRole: ['InvestorAdmin'] }) const { execute, isLoading: isTransactionPending } = useCentrifugeTransaction( 'Update investor', diff --git a/centrifuge-app/src/pages/IssuerPool/Investors/OnboardingSettings.tsx b/centrifuge-app/src/pages/IssuerPool/Investors/OnboardingSettings.tsx index 1016235f3c..6140e8ddc0 100644 --- a/centrifuge-app/src/pages/IssuerPool/Investors/OnboardingSettings.tsx +++ b/centrifuge-app/src/pages/IssuerPool/Investors/OnboardingSettings.tsx @@ -205,15 +205,15 @@ export const OnboardingSettings = () => { }, } - const memberlistAdmin = import.meta.env.REACT_APP_MEMBERLIST_ADMIN_PURE_PROXY - const hasMemberlistPermissions = permissions?.[addressToHex(memberlistAdmin)]?.roles.includes('MemberListAdmin') + const investorAdmin = import.meta.env.REACT_APP_MEMBERLIST_ADMIN_PURE_PROXY + const hasMemberlistPermissions = permissions?.[addressToHex(investorAdmin)]?.roles.includes('InvestorAdmin') const isAnyTrancheOpen = Object.values(values.openForOnboarding).includes(true) if (!useExternalUrl && isAnyTrancheOpen && !hasMemberlistPermissions) { // pool is open for onboarding and onboarding-api proxy is not in pool permissions - updatePermissionAndConfigTx([[[memberlistAdmin, 'MemberListAdmin']], [], amendedMetadata]) + updatePermissionAndConfigTx([[[investorAdmin, 'InvestorAdmin']], [], amendedMetadata]) } else if (hasMemberlistPermissions && (useExternalUrl || !isAnyTrancheOpen)) { // remove onboarding-api proxy from pool permissions - updatePermissionAndConfigTx([[], [[memberlistAdmin, 'MemberListAdmin']], amendedMetadata]) + updatePermissionAndConfigTx([[], [[investorAdmin, 'InvestorAdmin']], amendedMetadata]) } else { updateConfigTx([poolId, amendedMetadata], { account }) } diff --git a/centrifuge-app/src/pages/IssuerPool/Investors/index.tsx b/centrifuge-app/src/pages/IssuerPool/Investors/index.tsx index b9536fcb10..0573531a1f 100644 --- a/centrifuge-app/src/pages/IssuerPool/Investors/index.tsx +++ b/centrifuge-app/src/pages/IssuerPool/Investors/index.tsx @@ -22,7 +22,7 @@ export function IssuerPoolInvestorsPage() { function IssuerPoolInvestors() { const { pid: poolId } = useParams<{ pid: string }>() - const canEditInvestors = useSuitableAccounts({ poolId, poolRole: ['MemberListAdmin'] }).length > 0 + const canEditInvestors = useSuitableAccounts({ poolId, poolRole: ['InvestorAdmin'] }).length > 0 const isPoolAdmin = useSuitableAccounts({ poolId, poolRole: ['PoolAdmin'] }).length > 0 return ( diff --git a/centrifuge-app/src/pages/Loan/index.tsx b/centrifuge-app/src/pages/Loan/index.tsx index adda7d348a..d46db1d9fa 100644 --- a/centrifuge-app/src/pages/Loan/index.tsx +++ b/centrifuge-app/src/pages/Loan/index.tsx @@ -74,7 +74,7 @@ const Loan: React.FC = () => { const imageUrl = nftMetadata?.image ? cent.metadata.parseMetadataUrl(nftMetadata.image) : '' const { data: templateData } = useMetadata( - nftMetadata?.properties?._template && `ipfs://ipfs/${nftMetadata?.properties?._template}` + nftMetadata?.properties?._template && `ipfs://${nftMetadata?.properties?._template}` ) const documentId = useNftDocumentId(nft?.collectionId, nft?.id) diff --git a/centrifuge-app/src/utils/parseMetadataUrl.ts b/centrifuge-app/src/utils/parseMetadataUrl.ts index d8d8e8c9f1..dcf9c117bd 100644 --- a/centrifuge-app/src/utils/parseMetadataUrl.ts +++ b/centrifuge-app/src/utils/parseMetadataUrl.ts @@ -16,8 +16,10 @@ export function parseMetadataUrl(url: string) { if (!url.includes(':')) { // string without protocol is assumed to be an IPFS hash newUrl = new URL(`ipfs/${url}`, IFPS_GATEWAY) + } else if (url.startsWith('ipfs://ipfs/')) { + newUrl = new URL(url.slice(12), IFPS_GATEWAY) } else if (url.startsWith('ipfs://')) { - newUrl = new URL(url.substr(7), IFPS_GATEWAY) + newUrl = new URL(url.slice(7), IFPS_GATEWAY) } else { newUrl = new URL(url) } diff --git a/centrifuge-app/src/utils/usePermissions.ts b/centrifuge-app/src/utils/usePermissions.ts index aaade41b6b..0fb5226fd3 100644 --- a/centrifuge-app/src/utils/usePermissions.ts +++ b/centrifuge-app/src/utils/usePermissions.ts @@ -225,13 +225,13 @@ export function usePoolAccess(poolId: string) { : [] const missingAdminPermissions = diffPermissions( [storedAdminRoles], - [{ address: storedAdminRoles.address, roles: { MemberListAdmin: true } }] + [{ address: storedAdminRoles.address, roles: { InvestorAdmin: true } }] ).add const missingManagerPermissions = diffPermissions( storedManagerPermissions, (multisig?.signers || adminDelegates?.map((p) => p.delegatee))?.map((address) => ({ address, - roles: { MemberListAdmin: true, LiquidityAdmin: true }, + roles: { InvestorAdmin: true, LiquidityAdmin: true }, })) || [] ).add diff --git a/centrifuge-js/src/modules/metadata.ts b/centrifuge-js/src/modules/metadata.ts index 4e87e1b5ec..9c8b01f1b4 100644 --- a/centrifuge-js/src/modules/metadata.ts +++ b/centrifuge-js/src/modules/metadata.ts @@ -38,8 +38,10 @@ export function getMetadataModule(inst: Centrifuge) { if (!url.includes(':')) { // string without protocol is assumed to be an IPFS hash newUrl = new URL(`ipfs/${url.replace(/\/?(ipfs\/)/, '')}`, inst.config.metadataHost) + } else if (url.startsWith('ipfs://ipfs/')) { + newUrl = new URL(`ipfs/${url.slice(12)}`, inst.config.metadataHost) } else if (url.startsWith('ipfs://')) { - newUrl = new URL(`ipfs/${url.substr(7)}`, inst.config.metadataHost) + newUrl = new URL(`ipfs/${url.slice(7)}`, inst.config.metadataHost) } else { newUrl = new URL(url) } @@ -56,11 +58,8 @@ export function getMetadataModule(inst: Centrifuge) { const IPFS_HASH_LENGTH = 46 function parseIPFSHash(uri: string) { - if (uri.includes('ipfs://')) { - const hash = uri - .split(/ipfs:\/\/ipfs\//) - .filter(Boolean) - .join() + if (uri.startsWith('ipfs://')) { + const hash = uri.slice(7) return { uri, ipfsHash: hash } } else if (!uri.includes('/') && uri.length === IPFS_HASH_LENGTH) { return { uri: `ipfs://${uri}`, ipfsHash: uri } diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index b418932329..5766f2c335 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -19,7 +19,7 @@ import { Dec } from '../utils/Decimal' const PerquintillBN = new BN(10).pow(new BN(18)) const PriceBN = new BN(10).pow(new BN(27)) -type AdminRole = 'PoolAdmin' | 'Borrower' | 'PricingAdmin' | 'LiquidityAdmin' | 'MemberListAdmin' | 'LoanAdmin' +type AdminRole = 'PoolAdmin' | 'Borrower' | 'PricingAdmin' | 'LiquidityAdmin' | 'InvestorAdmin' | 'LoanAdmin' type CurrencyRole = 'PermissionedAssetManager' | 'PermissionedAssetIssuer' @@ -41,7 +41,7 @@ const AdminRoleBits = { Borrower: 0b00000010, PricingAdmin: 0b00000100, LiquidityAdmin: 0b00001000, - MemberListAdmin: 0b00010000, + InvestorAdmin: 0b00010000, LoanAdmin: 0b00100000, } @@ -596,7 +596,7 @@ export function getPoolsModule(inst: Centrifuge) { ) { if (options?.paymentInfo) { const hash = '0'.repeat(46) - return of({ uri: `ipfs://ipfs/${hash}`, ipfsHash: hash }) + return of({ uri: `ipfs://${hash}`, ipfsHash: hash }) } const tranchesById: PoolMetadata['tranches'] = {} @@ -689,7 +689,7 @@ export function getPoolsModule(inst: Centrifuge) { const submittable = api.tx.utility.batchAll([ ...add.map(([addr, role]) => api.tx.permissions.add( - { PoolRole: typeof role === 'string' ? 'PoolAdmin' : 'MemberListAdmin' }, + { PoolRole: typeof role === 'string' ? 'PoolAdmin' : 'InvestorAdmin' }, addr, { Pool: poolId }, { PoolRole: role } @@ -697,7 +697,7 @@ export function getPoolsModule(inst: Centrifuge) { ), ...sortedRemove.map(([addr, role]) => api.tx.permissions.remove( - { PoolRole: typeof role === 'string' ? 'PoolAdmin' : 'MemberListAdmin' }, + { PoolRole: typeof role === 'string' ? 'PoolAdmin' : 'InvestorAdmin' }, addr, { Pool: poolId }, { PoolRole: role } @@ -973,7 +973,7 @@ export function getPoolsModule(inst: Centrifuge) { const permissions = value.toJSON() as any roles.pools[poolId] = { roles: ( - ['PoolAdmin', 'Borrower', 'PricingAdmin', 'LiquidityAdmin', 'MemberListAdmin', 'LoanAdmin'] as const + ['PoolAdmin', 'Borrower', 'PricingAdmin', 'LiquidityAdmin', 'InvestorAdmin', 'LoanAdmin'] as const ).filter((role) => AdminRoleBits[role] & permissions.poolAdmin.bits), tranches: {}, } @@ -1028,7 +1028,7 @@ export function getPoolsModule(inst: Centrifuge) { const permissions = value.toJSON() as any roles[account] = { roles: ( - ['PoolAdmin', 'Borrower', 'PricingAdmin', 'LiquidityAdmin', 'MemberListAdmin', 'LoanAdmin'] as const + ['PoolAdmin', 'Borrower', 'PricingAdmin', 'LiquidityAdmin', 'InvestorAdmin', 'LoanAdmin'] as const ).filter((role) => AdminRoleBits[role] & permissions.poolAdmin.bits), tranches: {}, } @@ -2298,10 +2298,12 @@ export function findBalance>( } function parseCurrencyKey(key: CurrencyKey): CurrencyKey { - if (typeof key === 'string' || 'ForeignAsset' in key) return key - return { - Tranche: [key.Tranche[0].replace(/\D/g, ''), key.Tranche[1]], + if (typeof key !== 'string' && 'Tranche' in key) { + return { + Tranche: [key.Tranche[0].replace(/\D/g, ''), key.Tranche[1]], + } } + return key } function looksLike(a: any, b: any): boolean { diff --git a/centrifuge-react/src/components/Transactions/TransactionToasts.tsx b/centrifuge-react/src/components/Transactions/TransactionToasts.tsx index 11778bd3f8..2f2e7be3d8 100644 --- a/centrifuge-react/src/components/Transactions/TransactionToasts.tsx +++ b/centrifuge-react/src/components/Transactions/TransactionToasts.tsx @@ -20,6 +20,7 @@ const toastSublabel = { } const TOAST_DURATION = 10000 +const ERROR_TOAST_DURATION = 60000 export type TransactionToastsProps = { positionProps?: { @@ -44,7 +45,7 @@ export function TransactionToasts({ const explorer = useGetExplorerUrl() return ( - + {transactions .filter((tx) => !tx.dismissed && !['creating', 'unconfirmed'].includes(tx.status)) .map((tx) => { @@ -56,8 +57,8 @@ export function TransactionToasts({ status={toastStatus[tx.status]} onDismiss={dismiss(tx.id)} onStatusChange={(newStatus) => { - if (['ok'].includes(newStatus)) { - setTimeout(dismiss(tx.id), TOAST_DURATION) + if (['ok', 'critical'].includes(newStatus)) { + setTimeout(dismiss(tx.id), newStatus === 'ok' ? TOAST_DURATION : ERROR_TOAST_DURATION) } }} action={ diff --git a/onboarding-api/README.md b/onboarding-api/README.md index f3d9b7b706..68f0777508 100644 --- a/onboarding-api/README.md +++ b/onboarding-api/README.md @@ -351,7 +351,7 @@ Sets the ultimate beneficial owners for the entity. Once onboarding is complete a final tx will be signed by the server which will whtielist investors. For this, a pure proxy must be created and sufficiently funded for each chain environment. The pure proxy only has to be created once and can be used for all pools. -After creating the pure proxy, it must then be given `MemberListAdmin` permissions for each pool by the address with `PoolAdmin` permissions. +After creating the pure proxy, it must then be given `InvestorAdmin` permissions for each pool by the address with `PoolAdmin` permissions. ## Endpoints diff --git a/onboarding-api/src/utils/centrifuge.ts b/onboarding-api/src/utils/centrifuge.ts index 1ea30957c1..0c98e14437 100644 --- a/onboarding-api/src/utils/centrifuge.ts +++ b/onboarding-api/src/utils/centrifuge.ts @@ -23,7 +23,7 @@ export const getSigner = async () => { await cryptoWaitReady() const keyring = new Keyring({ type: 'sr25519', ss58Format: 2 }) // the pure proxy controller (PURE_PROXY_CONTROLLER_SEED) is the wallet that controls the pure proxy being used to sign the transaction - // the pure proxy address (MEMBERLIST_ADMIN_PURE_PROXY) has to be given MemberListAdmin permissions on each pool before being able to whitelist investors + // the pure proxy address (MEMBERLIST_ADMIN_PURE_PROXY) has to be given InvestorAdmin permissions on each pool before being able to whitelist investors return keyring.addFromMnemonic(process.env.PURE_PROXY_CONTROLLER_SEED) } @@ -48,7 +48,7 @@ export const addCentInvestorToMemberList = async (walletAddress: string, poolId: api.pipe( switchMap((api) => { const submittable = api.tx.permissions.add( - { PoolRole: 'MemberListAdmin' }, + { PoolRole: 'InvestorAdmin' }, walletAddress, { Pool: poolId }, { PoolRole: { TrancheInvestor: [trancheId, OneHundredYearsFromNow] } } @@ -56,7 +56,7 @@ export const addCentInvestorToMemberList = async (walletAddress: string, poolId: if (metadata?.onboarding?.podReadAccess) { const address = cent.utils.formatAddress(walletAddress) const podSubmittable = api.tx.permissions.add( - { PoolRole: 'MemberListAdmin' }, + { PoolRole: 'InvestorAdmin' }, address, { Pool: poolId }, { PoolRole: 'PODReadAccess' }