From 438e2734cbcb131ec354656b11e60ac8d9085f20 Mon Sep 17 00:00:00 2001 From: Thibaut Sardan Date: Mon, 16 Oct 2023 17:31:20 +0100 Subject: [PATCH 1/7] add ability to add identity --- packages/ui/src/components/AccountDisplay.tsx | 45 ++----- .../components/EasySetup/BalancesTransfer.tsx | 2 +- .../EasySetup/IdentitySetIdentity.tsx | 127 ++++++++++++++++++ packages/ui/src/components/modals/Send.tsx | 51 ++++--- packages/ui/src/contexts/ModalsContext.tsx | 23 +++- packages/ui/src/hooks/useIdentity.tsx | 29 ++++ .../ui/src/pages/Home/MultisigActionMenu.tsx | 24 +++- 7 files changed, 243 insertions(+), 58 deletions(-) create mode 100644 packages/ui/src/components/EasySetup/IdentitySetIdentity.tsx create mode 100644 packages/ui/src/hooks/useIdentity.tsx diff --git a/packages/ui/src/components/AccountDisplay.tsx b/packages/ui/src/components/AccountDisplay.tsx index 598b4732..a426a769 100644 --- a/packages/ui/src/components/AccountDisplay.tsx +++ b/packages/ui/src/components/AccountDisplay.tsx @@ -6,10 +6,10 @@ import { AccountBadge, IconSizeVariant } from '../types' import { getDisplayAddress } from '../utils' import IdenticonBadge from './IdenticonBadge' import { useApi } from '../contexts/ApiContext' -import { DeriveAccountInfo, DeriveAccountRegistration } from '@polkadot/api-derive/types' import IdentityIcon from './IdentityIcon' import Balance from './library/Balance' import { useGetEncodedAddress } from '../hooks/useGetEncodedAddress' +import { useIdentity } from '../hooks/useIdentity' interface Props { address: string @@ -30,44 +30,27 @@ const AccountDisplay = ({ }: Props) => { const { getNamesWithExtension } = useAccountNames() const localName = useMemo(() => getNamesWithExtension(address), [address, getNamesWithExtension]) - const [identity, setIdentity] = useState(null) const { api } = useApi() const [mainDisplay, setMainDisplay] = useState('') const [sub, setSub] = useState(null) const getEncodedAddress = useGetEncodedAddress() const encodedAddress = useMemo(() => getEncodedAddress(address), [address, getEncodedAddress]) + const identity = useIdentity(address) useEffect(() => { - if (!api) { - return - } - - let unsubscribe: () => void - - api.derive.accounts - .info(address, (info: DeriveAccountInfo) => { - setIdentity(info.identity) + if (!identity) return - if (info.identity.displayParent && info.identity.display) { - // when an identity is a sub identity `displayParent` is set - // and `display` get the sub identity - setMainDisplay(info.identity.displayParent) - setSub(info.identity.display) - } else { - // There should not be a `displayParent` without a `display` - // but we can't be too sure. - setMainDisplay( - info.identity.displayParent || info.identity.display || info.nickname || '' - ) - } - }) - .then((unsub) => { - unsubscribe = unsub - }) - .catch((e) => console.error(e)) - - return () => unsubscribe && unsubscribe() - }, [address, api]) + if (identity.displayParent && identity.display) { + // when an identity is a sub identity `displayParent` is set + // and `display` get the sub identity + setMainDisplay(identity.displayParent) + setSub(identity.display) + } else { + // There should not be a `displayParent` without a `display` + // but we can't be too sure. + setMainDisplay(identity.displayParent || identity.display || '') + } + }, [address, api, identity]) return (
diff --git a/packages/ui/src/components/EasySetup/BalancesTransfer.tsx b/packages/ui/src/components/EasySetup/BalancesTransfer.tsx index ac799483..4685af81 100644 --- a/packages/ui/src/components/EasySetup/BalancesTransfer.tsx +++ b/packages/ui/src/components/EasySetup/BalancesTransfer.tsx @@ -3,7 +3,7 @@ import { styled } from '@mui/material/styles' import { SubmittableExtrinsic } from '@polkadot/api/types' import { ISubmittableResult } from '@polkadot/types/types' import GenericAccountSelection, { AccountBaseInfo } from '../select/GenericAccountSelection' -import React, { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { useApi } from '../../contexts/ApiContext' import { useCheckBalance } from '../../hooks/useCheckBalance' import BN from 'bn.js' diff --git a/packages/ui/src/components/EasySetup/IdentitySetIdentity.tsx b/packages/ui/src/components/EasySetup/IdentitySetIdentity.tsx new file mode 100644 index 00000000..43cb9554 --- /dev/null +++ b/packages/ui/src/components/EasySetup/IdentitySetIdentity.tsx @@ -0,0 +1,127 @@ +import { Grid } from '@mui/material' +import { styled } from '@mui/material/styles' +import { SubmittableExtrinsic } from '@polkadot/api/types' +import { ISubmittableResult } from '@polkadot/types/types' +import { useCallback, useEffect, useState } from 'react' +import { useApi } from '../../contexts/ApiContext' +import { TextFieldStyled } from '../library' +import { useIdentity } from '../../hooks/useIdentity' + +interface Props { + className?: string + from: string + onSetExtrinsic: (ext?: SubmittableExtrinsic<'promise', ISubmittableResult>) => void +} + +interface IdentityFields { + display: string | undefined + legal: string | undefined + web: string | undefined + riot: string | undefined + email: string | undefined + image: string | undefined + twitter: string | undefined +} + +const getRawOrNone = (val: string | undefined) => { + return val + ? { + Raw: val + } + : { none: null } +} +const getExtrinsicsArgs = (identity: IdentityFields) => { + const { legal, display, email, image, riot, twitter, web } = identity + + return { + additional: [], + display: getRawOrNone(display), + legal: getRawOrNone(legal), + web: getRawOrNone(web), + riot: getRawOrNone(riot), + email: getRawOrNone(email), + pgpFingerprint: null, + image: getRawOrNone(image), + twitter: getRawOrNone(twitter) + } +} + +const IdentitySetIdentity = ({ className, onSetExtrinsic, from }: Props) => { + const { api, chainInfo } = useApi() + const [identity, setIdentity] = useState() + const chainIdentity = useIdentity(from) + + useEffect(() => { + if (chainIdentity) { + const { display, email, legal, web, riot, twitter, image } = chainIdentity + setIdentity({ + display, + legal, + web, + riot, + email, + image, + twitter + }) + } else { + setIdentity({ + display: undefined, + legal: undefined, + web: undefined, + riot: undefined, + email: undefined, + image: undefined, + twitter: undefined + }) + } + }, [chainIdentity]) + + useEffect(() => { + if (!api) { + onSetExtrinsic(undefined) + return + } + + if (!identity) { + onSetExtrinsic(undefined) + return + } + + const extrinsicsArgs = getExtrinsicsArgs(identity) + onSetExtrinsic(api.tx.identity.setIdentity(extrinsicsArgs)) + }, [api, chainInfo, identity, onSetExtrinsic]) + + const onChangeField = useCallback((field: keyof IdentityFields, value: string) => { + setIdentity((prev) => (prev ? { ...prev, [field]: value } : undefined)) + }, []) + + return ( + + {identity && + Object.entries(identity).map(([fieldName, value]) => ( + + onChangeField(fieldName as keyof IdentityFields, val.target.value)} + value={value || ''} + /> + + ))} + + ) +} + +export default styled(IdentitySetIdentity)` + margin-top: 0.5rem; +` diff --git a/packages/ui/src/components/modals/Send.tsx b/packages/ui/src/components/modals/Send.tsx index 6a3bf7c5..9b3b3e5c 100644 --- a/packages/ui/src/components/modals/Send.tsx +++ b/packages/ui/src/components/modals/Send.tsx @@ -20,19 +20,27 @@ import FromCallData from '../EasySetup/FromCallData' import { ModalCloseButton } from '../library/ModalCloseButton' import { formatBnBalance } from '../../utils/formatBnBalance' import { useGetMultisigTx } from '../../hooks/useGetMultisigTx' +import IdentitySetIdentity from '../EasySetup/IdentitySetIdentity' -const SEND_TOKEN_MENU = 'Send tokens' -const FROM_CALL_DATA_MENU = 'From call data' -const MANUEL_EXTRINSIC_MENU = 'Manual extrinsic' +export const easyTransferTitle = [ + 'Send tokens', + 'From call data', + 'Manual extrinsic', + 'Set identity' +] as const + +export type EasyTransferTitle = (typeof easyTransferTitle)[number] +export const DEFAULT_EASY_SETUP_SELECTION: EasyTransferTitle = 'Send tokens' interface Props { + preselected: EasyTransferTitle onClose: () => void className?: string onSuccess?: () => void onFinalized?: () => void } -const Send = ({ onClose, className, onSuccess, onFinalized }: Props) => { +const Send = ({ onClose, className, onSuccess, onFinalized, preselected }: Props) => { const { getSubscanExtrinsicLink } = useGetSubscanLinks() const { api, chainInfo } = useApi() const [isSubmitting, setIsSubmitting] = useState(false) @@ -66,7 +74,9 @@ const Send = ({ onClose, className, onSuccess, onFinalized }: Props) => { const [extrinsicToCall, setExtrinsicToCall] = useState< SubmittableExtrinsic<'promise', ISubmittableResult> | undefined >() - const [selectedEasyOption, setSelectedEasyOption] = useState(SEND_TOKEN_MENU) + const [selectedEasyOption, setSelectedEasyOption] = useState( + preselected || DEFAULT_EASY_SETUP_SELECTION + ) const multisigTx = useGetMultisigTx({ selectedMultisig, extrinsicToCall, @@ -119,24 +129,30 @@ const Send = ({ onClose, className, onSuccess, onFinalized }: Props) => { [getMultisigByAddress, selectedMultiProxy] ) - const easySetupOptions: { [index: string]: ReactNode } = useMemo(() => { + const easySetupOptions: Record = useMemo(() => { return { - [SEND_TOKEN_MENU]: ( + 'Send tokens': ( ), - [MANUEL_EXTRINSIC_MENU]: ( + 'Set identity': ( + + ), + 'Manual extrinsic': ( setSelectedEasyOption(FROM_CALL_DATA_MENU)} + onSelectFromCallData={() => setSelectedEasyOption('From call data')} hasErrorMessage={!!easyOptionErrorMessage} /> ), - [FROM_CALL_DATA_MENU]: ( + 'From call data': ( { getSubscanExtrinsicLink ]) - const onChangeEasySetupOption = useCallback( - ({ target: { value } }: SelectChangeEvent) => { - if (typeof value !== 'string') { - console.error('Unexpected network value, expect string but received', value) + const onChangeEasySetupOption: (event: SelectChangeEvent) => void = useCallback( + ({ target: { value } }) => { + if (typeof value !== 'string' && !easyTransferTitle.includes(value as EasyTransferTitle)) { + console.error( + 'Unexpected selection, expect one of', + easyTransferTitle, + 'but received', + value + ) return } setErrorMessage('') setEasyOptionErrorMessage('') - setSelectedEasyOption(value) + setSelectedEasyOption(value as EasyTransferTitle) }, [] ) diff --git a/packages/ui/src/contexts/ModalsContext.tsx b/packages/ui/src/contexts/ModalsContext.tsx index 99a600a0..c0d49a38 100644 --- a/packages/ui/src/contexts/ModalsContext.tsx +++ b/packages/ui/src/contexts/ModalsContext.tsx @@ -1,7 +1,7 @@ import { useState, useContext, createContext, useCallback } from 'react' import ChangeMultisig from '../components/modals/ChangeMultisig' import EditNames from '../components/modals/EditNames' -import Send from '../components/modals/Send' +import Send, { DEFAULT_EASY_SETUP_SELECTION, EasyTransferTitle } from '../components/modals/Send' import { usePendingTx } from '../hooks/usePendingTx' import { SignClientTypes } from '@walletconnect/types' import WCSessionProposal from '../components/modals/WalletConnectSessionProposal' @@ -11,7 +11,8 @@ import WalletConnectSigning from '../components/modals/WalletConnectSigning' interface ModalsContextProps { setIsEditModalOpen: (isOpen: boolean) => void setIsChangeMultiModalOpen: (isOpen: boolean) => void - setIsSendModalOpen: (isOpen: boolean) => void + onOpenSendModal: (preselected?: EasyTransferTitle) => void + onCloseSendModal: () => void openWalletConnectSessionModal: ({ sessionProposal }: OpenWCModalParams) => void onOpenSigningModal: (info: SigningInfo) => void onOpenWalletConnectSigning: (request: SignClientTypes.EventArguments['session_request']) => void @@ -33,6 +34,9 @@ const ModalsContextProvider: React.FC = ({ children }) const [isSigningModalOpen, setIsSigningModalOpen] = useState(false) const [signingModalInfo, setSigningModalInfo] = useState() const [isOpenWalletConnectSigning, setIsOpenWalletConnectSigning] = useState(false) + const [sendModalPreselection, setSendModalPreselection] = useState( + DEFAULT_EASY_SETUP_SELECTION + ) const [walletConnectRequest, setWalletConnectRequest] = useState< SignClientTypes.EventArguments['session_request'] | undefined >() @@ -41,7 +45,6 @@ const ModalsContextProvider: React.FC = ({ children }) OpenWCModalParams['sessionProposal'] | undefined >() const { refresh } = usePendingTx() - const onCloseSendModal = useCallback(() => setIsSendModalOpen(false), [setIsSendModalOpen]) const onCloseEditModal = useCallback(() => setIsEditModalOpen(false), [setIsEditModalOpen]) const onCloseChangeMultiModal = useCallback( () => setIsChangeMultiModalOpen(false), @@ -52,6 +55,16 @@ const ModalsContextProvider: React.FC = ({ children }) setSigningModalInfo(undefined) }, []) + const onOpenSendModal = useCallback((preselection?: EasyTransferTitle) => { + preselection && setSendModalPreselection(preselection) + setIsSendModalOpen(true) + }, []) + + const onCloseSendModal = useCallback(() => { + setIsSendModalOpen(false) + setSendModalPreselection(DEFAULT_EASY_SETUP_SELECTION) + }, []) + const onSuccessSendModal = useCallback(() => { onCloseSendModal() refresh() @@ -93,7 +106,8 @@ const ModalsContextProvider: React.FC = ({ children }) value={{ setIsEditModalOpen, setIsChangeMultiModalOpen, - setIsSendModalOpen, + onOpenSendModal, + onCloseSendModal, openWalletConnectSessionModal, onOpenSigningModal, onOpenWalletConnectSigning @@ -102,6 +116,7 @@ const ModalsContextProvider: React.FC = ({ children }) {children} {isSendModalOpen && ( { + const { api } = useApi() + const [identity, setIdentity] = useState(null) + + useEffect(() => { + if (!api) { + return + } + + let unsubscribe: () => void + + api.derive.accounts + .info(address, (info: DeriveAccountInfo) => { + setIdentity(info.identity) + }) + .then((unsub) => { + unsubscribe = unsub + }) + .catch((e) => console.error(e)) + + return () => unsubscribe && unsubscribe() + }, [address, api]) + + return identity +} diff --git a/packages/ui/src/pages/Home/MultisigActionMenu.tsx b/packages/ui/src/pages/Home/MultisigActionMenu.tsx index 2ffa0fbe..f2598deb 100644 --- a/packages/ui/src/pages/Home/MultisigActionMenu.tsx +++ b/packages/ui/src/pages/Home/MultisigActionMenu.tsx @@ -5,7 +5,10 @@ import { HiOutlinePencil } from 'react-icons/hi2' import { MdOutlineLockReset as LockResetIcon } from 'react-icons/md' import { useMultiProxy } from '../../contexts/MultiProxyContext' import { useModals } from '../../contexts/ModalsContext' -import { HiOutlineArrowTopRightOnSquare as LaunchIcon } from 'react-icons/hi2' +import { + HiOutlineArrowTopRightOnSquare as LaunchIcon, + HiOutlineUserPlus as IdentityIcon +} from 'react-icons/hi2' import { useGetSubscanLinks } from '../../hooks/useSubscanLink' interface MultisigActionMenuProps { @@ -18,7 +21,7 @@ const MultisigActionMenu = ({ menuButtonBorder }: MultisigActionMenuProps) => { const { selectedHasProxy, selectedIsWatched, selectedMultiProxy } = useMultiProxy() - const { setIsEditModalOpen, setIsChangeMultiModalOpen, setIsSendModalOpen } = useModals() + const { setIsEditModalOpen, setIsChangeMultiModalOpen, onOpenSendModal } = useModals() const { getSubscanAccountLink } = useGetSubscanLinks() const options: MenuOption[] = useMemo(() => { @@ -40,6 +43,12 @@ const MultisigActionMenu = ({ } ] + !selectedIsWatched && + opts.push({ + text: 'Set identity', + icon: , + onClick: () => onOpenSendModal('Set identity') + }) // allow rotation only for the multisigs with a proxy selectedHasProxy && !selectedIsWatched && @@ -62,12 +71,13 @@ const MultisigActionMenu = ({ return opts }, [ - selectedHasProxy, selectedIsWatched, - getSubscanAccountLink, + selectedHasProxy, + setIsEditModalOpen, selectedMultiProxy, - setIsChangeMultiModalOpen, - setIsEditModalOpen + getSubscanAccountLink, + onOpenSendModal, + setIsChangeMultiModalOpen ]) return ( @@ -76,7 +86,7 @@ const MultisigActionMenu = ({