diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 826bdc80..56a86e91 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -13,6 +13,7 @@ import MainLayout from './components/layout/Main' import { WatchedAddressesContextProvider } from './contexts/WatchedAddressesContext' import { WalletConnectContextProvider } from './contexts/WalletConnectContext' import { ModalsContextProvider } from './contexts/ModalsContext' +import { PplApiContextProvider } from './contexts/PeopleChainApiContext' const App = () => { const queryClient = new QueryClient() @@ -24,19 +25,21 @@ const App = () => { - - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/packages/ui/src/components/EasySetup/SetIdentity.tsx b/packages/ui/src/components/EasySetup/SetIdentity.tsx index 12f7b728..b1992879 100644 --- a/packages/ui/src/components/EasySetup/SetIdentity.tsx +++ b/packages/ui/src/components/EasySetup/SetIdentity.tsx @@ -3,13 +3,13 @@ import { styled } from '@mui/material/styles' import { SubmittableExtrinsic } from '@polkadot/api/types' import { ISubmittableResult } from '@polkadot/types/types' import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react' -import { useApi } from '../../contexts/ApiContext' import { TextField } from '../library' import { useIdentity } from '../../hooks/useIdentity' import { useCheckBalance } from '../../hooks/useCheckBalance' import { getErrorMessageReservedFunds } from '../../utils' import { formatBnBalance } from '../../utils/formatBnBalance' import { useSetIdentityReservedFunds } from '../../hooks/useSetIdentityReservedFunds' +import { useIdenityApi } from '../../hooks/useIdentityApi' interface Props { className?: string @@ -103,7 +103,7 @@ const fieldNameAndPlaceholder = (fieldName: keyof IdentityFields) => { const MAX_ALLOWED_VAL_LENGTH = 32 const SetIdentity = ({ className, onSetExtrinsic, from, onSetErrorMessage }: Props) => { - const { api, chainInfo } = useApi() + const { api, chainInfo } = useIdenityApi() const [identityFields, setIdentityFields] = useState() const chainIdentity = useIdentity(from) const [hasChangedAtLeastAField, setHasChangedAtLeastAField] = useState(false) diff --git a/packages/ui/src/components/modals/Send.tsx b/packages/ui/src/components/modals/Send.tsx index bd088eab..6136542b 100644 --- a/packages/ui/src/components/modals/Send.tsx +++ b/packages/ui/src/components/modals/Send.tsx @@ -30,7 +30,7 @@ import { formatBnBalance } from '../../utils/formatBnBalance' import { useGetMultisigTx } from '../../hooks/useGetMultisigTx' import SetIdentity from '../EasySetup/SetIdentity' import { getErrorMessageReservedFunds } from '../../utils/getErrorMessageReservedFunds' -import { useHasIdentityPallet } from '../../hooks/useHasIdentityPallet' +import { useHasIdentityFeature } from '../../hooks/useHasIdentityFeature' export enum EasyTransferTitle { SendTokens = 'Send tokens', @@ -76,7 +76,7 @@ const Send = ({ onClose, className, onSuccess, onFinalized, preselected }: Props (a) => !!a.address ) as AccountBaseInfo[] }, [getMultisigAsAccountBaseInfo, selectedMultiProxy]) - const hasIdentityPallet = useHasIdentityPallet() + const { hasIdentityPallet, hasPplChain } = useHasIdentityFeature() const [selectedOrigin, setSelectedOrigin] = useState(possibleOrigin[0]) const isProxySelected = useMemo(() => selectedOrigin.meta?.isProxy, [selectedOrigin]) const [selectedMultisig, setSelectedMultisig] = useState(selectedMultiProxy?.multisigs[0]) @@ -170,7 +170,7 @@ const Send = ({ onClose, className, onSuccess, onFinalized, preselected }: Props [EasyTransferTitle.FromCallData]: } as Partial> - if (hasIdentityPallet) { + if (hasIdentityPallet && !hasPplChain) { res[EasyTransferTitle.SetIdentity] = ( = { polkadot: { chainId: 'polkadot', @@ -49,6 +53,7 @@ export const networkList: Record = { kusama: { chainId: 'kusama', explorerNetworkName: 'kusama', + pplChainRpcUrl: kusamaPplChain, rpcUrl: 'wss://rpc.ibp.network/kusama', httpGraphqlUrl: HTTP_GRAPHQL_URL, logo: chainsKusamaSVG @@ -64,6 +69,7 @@ export const networkList: Record = { chainId: 'asset-hub-kusama', explorerNetworkName: 'asset-hub-kusama', rpcUrl: 'wss://sys.ibp.network/statemine', + pplChainRpcUrl: kusamaPplChain, httpGraphqlUrl: HTTP_GRAPHQL_URL, logo: nodesAssetHubSVG }, @@ -147,6 +153,7 @@ export const networkList: Record = { westend: { chainId: 'westend', explorerNetworkName: 'westend', + pplChainRpcUrl: westendPplChain, rpcUrl: 'wss://westend-rpc.polkadot.io', httpGraphqlUrl: HTTP_GRAPHQL_URL, logo: nodesWestendColourSVG diff --git a/packages/ui/src/contexts/PeopleChainApiContext.tsx b/packages/ui/src/contexts/PeopleChainApiContext.tsx new file mode 100644 index 00000000..3a4c220a --- /dev/null +++ b/packages/ui/src/contexts/PeopleChainApiContext.tsx @@ -0,0 +1,99 @@ +import React, { useMemo } from 'react' +import { ApiPromise, WsProvider } from '@polkadot/api' +import { useState, useEffect, createContext, useContext } from 'react' +import { useNetwork } from './NetworkContext' +import '@polkadot/api-augment' + +type ApiContextProps = { + children: React.ReactNode | React.ReactNode[] +} + +export interface IApiContext { + pplApi?: false | ApiPromise + pplChainInfo?: ChainInfoHuman +} + +export interface ChainInfoHuman { + ss58Format: number + tokenDecimals: number + tokenSymbol: string +} + +interface RawChainInfoHuman { + ss58Format: string + tokenDecimals: string[] + tokenSymbol: string[] +} + +const PplApiContext = createContext(undefined) + +const PplApiContextProvider = ({ children }: ApiContextProps) => { + const { selectedNetworkInfo } = useNetwork() + const [chainInfo, setChainInfo] = useState() + const [pplApiPromise, setPplApiPromise] = useState() + const [isPplApiReady, setIsPplApiReady] = useState(false) + const provider = useMemo( + () => + !!selectedNetworkInfo?.pplChainRpcUrl && new WsProvider(selectedNetworkInfo?.pplChainRpcUrl), + [selectedNetworkInfo] + ) + + useEffect(() => { + if (!provider) return + + // console.log('---> connecting to', provider.endpoint) + setIsPplApiReady(false) + const pplApi = new ApiPromise({ provider }) + pplApi.isReady.then((newApi) => setPplApiPromise(newApi)).catch(console.error) + + return () => { + // console.log('<---disconnecting') + setIsPplApiReady(false) + !!pplApi && pplApi.disconnect() + setPplApiPromise(undefined) + } + + // prevent an infinite loop + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [provider]) + + useEffect(() => { + if (!pplApiPromise) return + + pplApiPromise.isReady + .then((pplApi) => { + setIsPplApiReady(true) + + const info = pplApi.registry.getChainProperties() + const raw = info?.toHuman() as unknown as RawChainInfoHuman + setChainInfo({ + // some parachains such as interlay have a comma in the format, e.g: "2,042" + ss58Format: Number(raw?.ss58Format.replace(',', '')) || 0, + tokenDecimals: Number(raw?.tokenDecimals[0]) || 0, + tokenSymbol: raw?.tokenSymbol[0] || '' + }) + }) + .catch(console.error) + }, [pplApiPromise]) + + return ( + + {children} + + ) +} + +const usePplApi = () => { + const context = useContext(PplApiContext) + if (context === undefined) { + throw new Error('usePplApi must be used within a PplApiContextProvider') + } + return context +} + +export { PplApiContextProvider, usePplApi } diff --git a/packages/ui/src/hooks/useGetIdentity.tsx b/packages/ui/src/hooks/useGetIdentity.tsx index de0d589e..a35404dc 100644 --- a/packages/ui/src/hooks/useGetIdentity.tsx +++ b/packages/ui/src/hooks/useGetIdentity.tsx @@ -1,8 +1,8 @@ import { useCallback } from 'react' -import { useApi } from '../contexts/ApiContext' +import { useIdenityApi } from './useIdentityApi' export const useGetIdentity = () => { - const { api } = useApi() + const { api } = useIdenityApi() const getIdentity = useCallback( async (address: string) => { diff --git a/packages/ui/src/hooks/useHasIdentityFeature.tsx b/packages/ui/src/hooks/useHasIdentityFeature.tsx new file mode 100644 index 00000000..d7c151df --- /dev/null +++ b/packages/ui/src/hooks/useHasIdentityFeature.tsx @@ -0,0 +1,14 @@ +import { useMemo } from 'react' +import { useNetwork } from '../contexts/NetworkContext' +import { useIdenityApi } from './useIdentityApi' + +export const useHasIdentityFeature = () => { + const { api } = useIdenityApi() + const { selectedNetworkInfo } = useNetwork() + const hasIdentityPallet = useMemo(() => !!api && !!api.tx?.identity?.setIdentity, [api]) + const hasPplChain = useMemo(() => !!selectedNetworkInfo?.pplChainRpcUrl, [selectedNetworkInfo]) + return { + hasPplChain, + hasIdentityPallet + } +} diff --git a/packages/ui/src/hooks/useHasIdentityPallet.tsx b/packages/ui/src/hooks/useHasIdentityPallet.tsx deleted file mode 100644 index bfe83cb9..00000000 --- a/packages/ui/src/hooks/useHasIdentityPallet.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { useMemo } from 'react' -import { useApi } from '../contexts/ApiContext' - -export const useHasIdentityPallet = () => { - const { api } = useApi() - const hasIdentityPallet = useMemo(() => !!api && !!api.tx?.identity?.setIdentity, [api]) - - return hasIdentityPallet -} diff --git a/packages/ui/src/hooks/useIdentity.tsx b/packages/ui/src/hooks/useIdentity.tsx index 67d1075d..d3bd82e6 100644 --- a/packages/ui/src/hooks/useIdentity.tsx +++ b/packages/ui/src/hooks/useIdentity.tsx @@ -1,9 +1,9 @@ import { useEffect, useState } from 'react' -import { useApi } from '../contexts/ApiContext' import { DeriveAccountInfo, DeriveAccountRegistration } from '@polkadot/api-derive/types' +import { useIdenityApi } from './useIdentityApi' export const useIdentity = (address?: string) => { - const { api } = useApi() + const { api } = useIdenityApi() const [identity, setIdentity] = useState(null) useEffect(() => { diff --git a/packages/ui/src/hooks/useIdentityApi.tsx b/packages/ui/src/hooks/useIdentityApi.tsx new file mode 100644 index 00000000..15a7d1fd --- /dev/null +++ b/packages/ui/src/hooks/useIdentityApi.tsx @@ -0,0 +1,39 @@ +import { ApiPromise } from '@polkadot/api' +import { useState, useEffect } from 'react' +import { useApi } from '../contexts/ApiContext' +import { ChainInfoHuman, usePplApi } from '../contexts/PeopleChainApiContext' + +export const useIdenityApi = () => { + const { api, chainInfo } = useApi() + const { pplApi, pplChainInfo } = usePplApi() + const [apiToUse, setApiToUse] = useState(null) + const [chainInfoToUse, setChainInfoToUse] = useState(undefined) + + useEffect(() => { + if (!pplApi && !api) { + return + } + + if (pplApi) { + setApiToUse(pplApi) + setChainInfoToUse(pplChainInfo) + } else if (api) { + setApiToUse(api) + setChainInfoToUse(chainInfo) + } + }, [api, chainInfo, pplApi, pplChainInfo]) + + useEffect(() => { + if (!pplApi && !api) { + return + } + + if (pplApi) { + setApiToUse(pplApi) + } else if (api) { + setApiToUse(api) + } + }, [api, pplApi]) + + return { api: apiToUse, chainInfo: chainInfoToUse } +} diff --git a/packages/ui/src/pages/Home/MultisigActionMenu.tsx b/packages/ui/src/pages/Home/MultisigActionMenu.tsx index 09034dd6..18b67ba5 100644 --- a/packages/ui/src/pages/Home/MultisigActionMenu.tsx +++ b/packages/ui/src/pages/Home/MultisigActionMenu.tsx @@ -11,7 +11,7 @@ import { } from 'react-icons/hi2' import { useGetSubscanLinks } from '../../hooks/useSubscanLink' import { EasyTransferTitle } from '../../components/modals/Send' -import { useHasIdentityPallet } from '../../hooks/useHasIdentityPallet' +import { useHasIdentityFeature } from '../../hooks/useHasIdentityFeature' interface MultisigActionMenuProps { withNewTransactionButton?: boolean @@ -25,7 +25,7 @@ const MultisigActionMenu = ({ const { selectedHasProxy, selectedIsWatched, selectedMultiProxy } = useMultiProxy() const { setIsEditModalOpen, setIsChangeMultiModalOpen, onOpenSendModal } = useModals() const { getSubscanAccountLink } = useGetSubscanLinks() - const hasIdentityPallet = useHasIdentityPallet() + const { hasIdentityPallet, hasPplChain } = useHasIdentityFeature() const options: MenuOption[] = useMemo(() => { const opts = [ @@ -48,6 +48,7 @@ const MultisigActionMenu = ({ !selectedIsWatched && hasIdentityPallet && + hasPplChain && opts.push({ text: 'Set identity', icon: ,