diff --git a/centrifuge-app/src/components/Menu-deprecated/index.tsx b/centrifuge-app/src/components/Menu-deprecated/index.tsx index 83e5d3d7e1..6723461169 100644 --- a/centrifuge-app/src/components/Menu-deprecated/index.tsx +++ b/centrifuge-app/src/components/Menu-deprecated/index.tsx @@ -2,7 +2,7 @@ import { Box, IconInvestments, IconNft, Menu as Panel, MenuItemGroup, Shelf, Sta import { config } from '../../config' import { useAddress } from '../../utils/useAddress' import { useIsAboveBreakpoint } from '../../utils/useIsAboveBreakpoint' -import { usePools } from '../../utils/usePools' +import { usePoolsThatAnyConnectedAddressHasPermissionsFor } from '../../utils/usePermissions' import { RouterLinkButton } from '../RouterLinkButton' import { GovernanceMenu } from './GovernanceMenu' import { IssuerMenu } from './IssuerMenu' @@ -10,8 +10,7 @@ import { PageLink } from './PageLink' import { PoolLink } from './PoolLink' export function Menu() { - // const pools = usePoolsThatAnyConnectedAddressHasPermissionsFor() || [] - const pools = usePools() || [] + const pools = usePoolsThatAnyConnectedAddressHasPermissionsFor() || [] const isXLarge = useIsAboveBreakpoint('XL') const address = useAddress('substrate') @@ -38,7 +37,7 @@ export function Menu() { {(pools.length > 0 || config.poolCreationType === 'immediate') && ( - id)}> + {isXLarge ? ( {!!pools.length && diff --git a/centrifuge-app/src/components/Menu/IssuerMenu.tsx b/centrifuge-app/src/components/Menu/IssuerMenu.tsx index 58ed994907..85cc76b7bc 100644 --- a/centrifuge-app/src/components/Menu/IssuerMenu.tsx +++ b/centrifuge-app/src/components/Menu/IssuerMenu.tsx @@ -6,14 +6,13 @@ import { Toggle } from './Toggle' type IssuerMenuProps = { defaultOpen?: boolean - poolIds?: string[] stacked?: boolean children?: React.ReactNode } -export function IssuerMenu({ defaultOpen = false, poolIds = [], stacked, children }: IssuerMenuProps) { +export function IssuerMenu({ defaultOpen = false, stacked, children }: IssuerMenuProps) { const match = useRouteMatch<{ pid: string }>('/issuer/:pid') - const isActive = match && poolIds.includes(match.params.pid) + const isActive = !!match const [open, setOpen] = React.useState(defaultOpen) const { space } = useTheme() const fullWidth = `calc(100vw - 2 * ${space[1]}px)` diff --git a/centrifuge-app/src/components/Menu/index.tsx b/centrifuge-app/src/components/Menu/index.tsx index e8479b6928..cf9e1e4d79 100644 --- a/centrifuge-app/src/components/Menu/index.tsx +++ b/centrifuge-app/src/components/Menu/index.tsx @@ -2,7 +2,7 @@ import { Box, IconInvestments, IconNft, Menu as Panel, MenuItemGroup, Shelf, Sta import { config } from '../../config' import { useAddress } from '../../utils/useAddress' import { useIsAboveBreakpoint } from '../../utils/useIsAboveBreakpoint' -import { usePools } from '../../utils/usePools' +import { usePoolsThatAnyConnectedAddressHasPermissionsFor } from '../../utils/usePermissions' import { RouterLinkButton } from '../RouterLinkButton' import { GovernanceMenu } from './GovernanceMenu' import { IssuerMenu } from './IssuerMenu' @@ -10,8 +10,7 @@ import { PageLink } from './PageLink' import { PoolLink } from './PoolLink' export function Menu() { - // const pools = usePoolsThatAnyConnectedAddressHasPermissionsFor() || [] - const pools = usePools() || [] + const pools = usePoolsThatAnyConnectedAddressHasPermissionsFor() || [] const isLarge = useIsAboveBreakpoint('L') const address = useAddress('substrate') @@ -38,15 +37,14 @@ export function Menu() { {(pools.length > 0 || config.poolCreationType === 'immediate') && ( - id)}> + {isLarge ? ( - {!!pools.length && - pools.map((pool) => ( - - - - ))} + {pools.map((pool) => ( + + + + ))} {address && config.poolCreationType === 'immediate' && ( diff --git a/centrifuge-app/src/utils/usePermissions.ts b/centrifuge-app/src/utils/usePermissions.ts index 9783859ef2..2fbf758210 100644 --- a/centrifuge-app/src/utils/usePermissions.ts +++ b/centrifuge-app/src/utils/usePermissions.ts @@ -12,7 +12,7 @@ import { combineLatest, filter, map, repeatWhen, switchMap } from 'rxjs' import { diffPermissions } from '../pages/IssuerPool/Configuration/Admins' import { useCollections } from './useCollections' import { useLoan } from './useLoans' -import { usePool, usePoolMetadata } from './usePools' +import { usePool, usePoolMetadata, usePools } from './usePools' export function usePermissions(address?: string) { const [result] = useCentrifugeQuery(['permissions', address], (cent) => cent.pools.getUserPermissions([address!]), { @@ -29,38 +29,38 @@ export function usePoolPermissions(poolId?: string) { return result } -// export function useUserPermissionsMulti(addresses: string[]) { -// const [results] = useCentrifugeQueries( -// addresses.map((address) => ({ -// queryKey: ['permissions', address], -// queryCallback: (cent) => cent.pools.getUserPermissions([address!]), -// })) -// ) - -// return results -// } +export function useUserPermissionsMulti(addresses: string[], options?: { enabled?: boolean }) { + const [result] = useCentrifugeQuery( + ['permissions', ...addresses], + (cent) => cent.pools.getUserPermissions([addresses]), + { + enabled: !!addresses.length && options?.enabled !== false, + } + ) + return result +} -// // Better name welcomed lol -// export function usePoolsThatAnyConnectedAddressHasPermissionsFor() { -// const { -// substrate: { combinedAccounts }, -// } = useWallet() -// const actingAddresses = [...new Set(combinedAccounts?.map((acc) => acc.actingAddress))] -// const permissionResults = useUserPermissionsMulti(actingAddresses) +// Better name welcomed lol +export function usePoolsThatAnyConnectedAddressHasPermissionsFor() { + const { + substrate: { combinedAccounts, proxiesAreLoading }, + } = useWallet() + const actingAddresses = [...new Set(combinedAccounts?.map((acc) => acc.actingAddress))] + const permissionsResult = useUserPermissionsMulti(actingAddresses, { enabled: !proxiesAreLoading }) -// const poolIds = new Set( -// permissionResults -// .map((permissions) => -// Object.entries(permissions?.pools || {}).map(([poolId, roles]) => (roles.roles.length ? poolId : [])) -// ) -// .flat(2) -// ) + const poolIds = new Set( + permissionsResult + ?.map((permissions) => + Object.entries(permissions?.pools || {}).map(([poolId, roles]) => (roles.roles.length ? poolId : [])) + ) + .flat(2) + ) -// const pools = usePools(false) -// const filtered = pools?.filter((p) => poolIds.has(p.id)) + const pools = usePools(false) + const filtered = pools?.filter((p) => poolIds.has(p.id)) -// return filtered -// } + return filtered +} // Returns whether the connected address can borrow from a pool in principle export function useCanBorrow(poolId: string) { diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index 5b2bb11352..661086e9a7 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -681,6 +681,18 @@ type BorrowerTransaction = { amount: CurrencyBalance | undefined } +export type Permissions = { + pools: { + [poolId: string]: PoolRoles + } + currencies: { + [currency: string]: { + roles: CurrencyRole[] + holder: boolean + } + } +} + const formatPoolKey = (keys: StorageKey<[u32]>) => (keys.toHuman() as string[])[0].replace(/\D/g, '') const formatLoanKey = (keys: StorageKey<[u32, u32]>) => (keys.toHuman() as string[])[1].replace(/\D/g, '') @@ -1124,8 +1136,14 @@ export function getPoolsModule(inst: Centrifuge) { ) } - function getUserPermissions(args: [address: Account]) { - const [address] = args + function getUserPermissions( + args: [address: T] + ): T extends Array ? Observable : Observable { + const [maybeArray] = args + const addresses = (Array.isArray(maybeArray) ? (maybeArray as Account[]) : [maybeArray as Account]).map( + (addr) => addressToHex(addr) as string + ) + const addressSet = new Set(addresses) const $api = inst.getApi() const $events = inst.getEvents().pipe( @@ -1135,59 +1153,76 @@ export function getPoolsModule(inst: Centrifuge) { ) if (!event) return false - const [accountId] = (event.toJSON() as any).event.data - return isSameAddress(address, accountId) + const [accountId] = (event.toHuman() as any).event.data + return addressSet.has(addressToHex(accountId)) }) ) return $api.pipe( - switchMap((api) => api.query.permissions.permission.entries(address)), - map((permissionsData) => { - const roles: { - pools: { - [poolId: string]: PoolRoles - } - currencies: { - [currency: string]: { - roles: CurrencyRole[] - holder: boolean - } - } - } = { - pools: {}, - currencies: {}, - } - - permissionsData.forEach(([keys, value]) => { - const key = (keys.toHuman() as any)[1] as { Pool: string } | { Currency: any } - if ('Pool' in key) { - const poolId = key.Pool.replace(/\D/g, '') - const permissions = value.toJSON() as any - roles.pools[poolId] = { - roles: ( - [ - 'PoolAdmin', - 'Borrower', - 'PricingAdmin', - 'LiquidityAdmin', - 'InvestorAdmin', - 'LoanAdmin', - 'PODReadAccess', + switchMap((api) => + api.query.permissions.permission.keys().pipe( + switchMap((keys) => { + const userKeys = keys + .map((key) => { + const [account, scope] = key.toHuman() as any as [string, { Pool: string } | { Currency: any }] + return [ + addressToHex(account), + 'Pool' in scope ? { Pool: scope.Pool.replace(/\D/g, '') } : scope, ] as const - ).filter((role) => AdminRoleBits[role] & permissions.poolAdmin.bits), - tranches: {}, - } - permissions.trancheInvestor.info - .filter((info: any) => info.permissionedTill * 1000 > Date.now()) - .forEach((info: any) => { - roles.pools[poolId].tranches[info.trancheId] = new Date(info.permissionedTill * 1000).toISOString() }) - } - }) - return roles - }), + .filter(([account, scope]) => { + return 'Pool' in scope && addressSet.has(account) + }) + return api.query.permissions.permission.multi(userKeys).pipe( + map((permissionsData) => { + const permissionsByAddressIndex: Permissions[] = [] + + function setPoolRoles(user: string, poolId: string, roles: PoolRoles) { + const i = addresses.indexOf(user) + const obj = permissionsByAddressIndex[i] ?? { + pools: {}, + currencies: {}, + } + obj.pools[poolId] = roles + permissionsByAddressIndex[i] = obj + } + permissionsData.forEach((value, i) => { + const [account, scope] = userKeys[i] + if ('Pool' in scope) { + const poolId = scope.Pool.replace(/\D/g, '') + const permissions = value.toJSON() as any + const roles: PoolRoles = { + roles: ( + [ + 'PoolAdmin', + 'Borrower', + 'PricingAdmin', + 'LiquidityAdmin', + 'InvestorAdmin', + 'LoanAdmin', + 'PODReadAccess', + ] as const + ).filter((role) => AdminRoleBits[role] & permissions.poolAdmin.bits), + tranches: {}, + } + permissions.trancheInvestor.info + .filter((info: any) => info.permissionedTill * 1000 > Date.now()) + .forEach((info: any) => { + roles.tranches[info.trancheId] = new Date(info.permissionedTill * 1000).toISOString() + }) + + setPoolRoles(account, poolId, roles) + } + }) + return Array.isArray(maybeArray) ? permissionsByAddressIndex : permissionsByAddressIndex[0] + }) + ) + }) + ) + ), + repeatWhen(() => $events) - ) + ) as any } function getPoolPermissions(args: [poolId: string]) { diff --git a/centrifuge-js/src/utils/index.ts b/centrifuge-js/src/utils/index.ts index 8b5f3d027c..2d826349a2 100644 --- a/centrifuge-js/src/utils/index.ts +++ b/centrifuge-js/src/utils/index.ts @@ -114,7 +114,7 @@ export function getDateMonthsFromNow(month: number) { return new Date(date.setMonth(date.getMonth() + month)) } -export function addressToHex(addr: string) { +export function addressToHex(addr: string | Uint8Array) { return u8aToHex(decodeAddress(addr)) } diff --git a/centrifuge-react/src/components/WalletProvider/WalletProvider.tsx b/centrifuge-react/src/components/WalletProvider/WalletProvider.tsx index b6c8376f51..4097aa261c 100644 --- a/centrifuge-react/src/components/WalletProvider/WalletProvider.tsx +++ b/centrifuge-react/src/components/WalletProvider/WalletProvider.tsx @@ -41,6 +41,7 @@ export type WalletContextType = { evmChainId?: number accounts: SubstrateAccount[] | null proxies: Record | undefined + proxiesAreLoading: boolean multisigs: ComputedMultisig[] combinedAccounts: CombinedSubstrateAccount[] | null selectedAccount: SubstrateAccount | null @@ -210,7 +211,7 @@ export function WalletProvider({ wallet: state.evm.selectedWallet as any, })) : null - const { data: proxies } = useQuery( + const { data: proxies, isLoading: proxiesAreLoading } = useQuery( [ 'proxies', state.substrate.accounts?.map((acc) => acc.address), @@ -232,7 +233,7 @@ export function WalletProvider({ ) const delegatees = [...new Set(Object.values(proxies ?? {})?.flatMap((p) => p.map((d) => d.delegator)))] - const { data: nestedProxies } = useQuery( + const { data: nestedProxies, isLoading: nestedProxiesAreLoading } = useQuery( ['nestedProxies', delegatees], () => firstValueFrom(cent.proxies.getMultiUserProxies([delegatees])), { @@ -454,6 +455,7 @@ export function WalletProvider({ selectedProxies: selectedCombinedAccount?.proxies || null, selectedMultisig: selectedCombinedAccount?.multisig || null, proxies: combinedProxies, + proxiesAreLoading: nestedProxiesAreLoading || proxiesAreLoading, subscanUrl, }, evm: { diff --git a/centrifuge-react/src/hooks/useCentrifugeQueries.ts b/centrifuge-react/src/hooks/useCentrifugeQueries.ts deleted file mode 100644 index 59f5612e1d..0000000000 --- a/centrifuge-react/src/hooks/useCentrifugeQueries.ts +++ /dev/null @@ -1,75 +0,0 @@ -import Centrifuge from '@centrifuge/centrifuge-js' -import * as React from 'react' -import { useQueries, useQueryClient } from 'react-query' -import { firstValueFrom, Observable } from 'rxjs' -import { useCentrifuge } from '../components/CentrifugeProvider' -import { useCentrifugeKey } from '../components/CentrifugeProvider/CentrifugeProvider' -import { CentrifugeQueryOptions, getQuerySource } from './useCentrifugeQuery' - -type MultiQueryOptions = ({ - queryKey: readonly unknown[] - queryCallback: (cent: Centrifuge) => Observable -} & CentrifugeQueryOptions)[] - -// TODO: Fix infinite loop when receiving new data sometimes -export function useCentrifugeQueries( - queries: readonly [...MultiQueryOptions] -): readonly [(T | null | undefined)[], (Observable | undefined)[]] { - const cent = useCentrifuge() - const centKey = useCentrifugeKey() - const queryClient = useQueryClient() - - // Using react-query to cache the observables to ensure that all consumers subscribe to the same multicasted observable - const sourceResults = useQueries( - queries.map((query) => { - const { queryKey, queryCallback, ...options } = query - const { suspense, enabled = true } = options || {} - return { - queryKey: ['querySource', centKey, ...queryKey], - queryFn: () => getQuerySource(cent, queryKey, queryCallback, options), - suspense, - staleTime: Infinity, - enabled, - } - }) - ) - - const dataResults = useQueries( - queries.map((query, i) => { - const { queryKey, queryCallback, ...options } = query - const { suspense, enabled = true } = options || {} - const $source = sourceResults[i].data - return { - queryKey: ['queryData', centKey, ...queryKey, !!$source], - queryFn: () => ($source ? firstValueFrom($source) : null), - suspense, - // Infinite staleTime as useQueries here is only used to populate the cache initially and - // to handle suspending the component when the suspense option is enabled. - // Further data is subscribed to, and added to the cache, after the component has mounted. - staleTime: Infinity, - enabled: $source && enabled, - retry: false, - } - }) - ) - - React.useEffect(() => { - const subs = sourceResults.map((r, i) => - r.data?.subscribe({ - next: (data) => { - if (data) { - const cached = queryClient.getQueryData(['queryData', centKey, ...queries[i].queryKey, true]) - if (cached !== data) { - queryClient.setQueryData(['queryData', centKey, ...queries[i].queryKey, true], data) - } - } - }, - }) - ) - return () => { - subs.forEach((sub) => sub?.unsubscribe()) - } - }, [sourceResults]) - - return [dataResults.map((r) => r.data), sourceResults.map((r) => r.data)] as const -} diff --git a/centrifuge-react/src/index.ts b/centrifuge-react/src/index.ts index 0dc9c137b2..668d1a01ee 100644 --- a/centrifuge-react/src/index.ts +++ b/centrifuge-react/src/index.ts @@ -6,7 +6,6 @@ export * from './components/WalletMenu' export * from './components/WalletProvider' export { useAsyncCallback } from './hooks/useAsyncCallback' export { useBalances } from './hooks/useBalances' -export * from './hooks/useCentrifugeQueries' export * from './hooks/useCentrifugeQuery' export { useCentrifugeTransaction } from './hooks/useCentrifugeTransaction' export { useEns } from './hooks/useEns'