From 9848c41dcdbb8df964b73524312f292bb6dcb16f Mon Sep 17 00:00:00 2001 From: brusher_ru Date: Thu, 5 Sep 2024 17:37:41 -0300 Subject: [PATCH] feat: implement edit/delete accounts --- src/components/CreateAccountModal.tsx | 43 +-- src/components/EditAccountModal.tsx | 377 +++++++++++++++++++++++++ src/components/FormAddressSelect.tsx | 34 ++- src/components/KeyManager.tsx | 72 ++++- src/components/createAccountSchema.tsx | 34 +-- src/store/useWallet.ts | 70 +++++ 6 files changed, 567 insertions(+), 63 deletions(-) create mode 100644 src/components/EditAccountModal.tsx diff --git a/src/components/CreateAccountModal.tsx b/src/components/CreateAccountModal.tsx index 5dc352a..00ff527 100644 --- a/src/components/CreateAccountModal.tsx +++ b/src/components/CreateAccountModal.tsx @@ -55,8 +55,8 @@ function CreateAccountModal({ const keys = wallet?.keychain || []; const defaultValues = { displayName: '', - required: 1, - publicKeys: [keys[0]?.publicKey || '0x1'], + Required: 1, + PublicKeys: [keys[0]?.publicKey || '0x1'], }; const { watch, @@ -73,36 +73,36 @@ function CreateAccountModal({ defaultValues, }); const selectedTemplate = watch('templateAddress'); - const selectedOwner = watch('owner'); - const totalAmount = watch('totalAmount'); + const selectedOwner = watch('Owner'); + const totalAmount = watch('TotalAmount'); useEffect(() => { if (selectedTemplate === StdPublicKeys.Vault) { - register('initialUnlockAmount', { + register('InitialUnlockAmount', { required: 'Please specify the initial unlock amount', }); - return () => unregister('initialUnlockAmount'); + return () => unregister('InitialUnlockAmount'); } return noop; }, [register, selectedTemplate, unregister]); useEffect(() => { - const owner = selectedOwner || getValues('owner'); + const owner = selectedOwner || getValues('Owner'); if (Object.hasOwn(GENESIS_VESTING_ACCOUNTS, owner)) { const amount = GENESIS_VESTING_ACCOUNTS[ owner as keyof typeof GENESIS_VESTING_ACCOUNTS ]; - setValue('totalAmount', String(amount)); - setValue('initialUnlockAmount', String(amount / 4n)); - setValue('vestingStart', GENESIS_VESTING_START); - setValue('vestingEnd', GENESIS_VESTING_END); + setValue('TotalAmount', String(amount)); + setValue('InitialUnlockAmount', String(amount / 4n)); + setValue('VestingStart', GENESIS_VESTING_START); + setValue('VestingEnd', GENESIS_VESTING_END); } }, [getValues, selectedOwner, selectedTemplate, setValue]); useEffect(() => { if (totalAmount) { - setValue('initialUnlockAmount', String(BigInt(totalAmount) / 4n)); + setValue('InitialUnlockAmount', String(BigInt(totalAmount) / 4n)); } }, [totalAmount, setValue]); @@ -138,7 +138,7 @@ function CreateAccountModal({ pre-defined vesting schedule. acc.templateAddress === StdPublicKeys.Vesting )} @@ -153,7 +153,7 @@ function CreateAccountModal({ { @@ -210,7 +210,7 @@ function CreateAccountModal({ { @@ -251,7 +251,7 @@ function CreateAccountModal({ ); diff --git a/src/components/EditAccountModal.tsx b/src/components/EditAccountModal.tsx new file mode 100644 index 0000000..b615d30 --- /dev/null +++ b/src/components/EditAccountModal.tsx @@ -0,0 +1,377 @@ +import { useEffect } from 'react'; +import { Form, useForm } from 'react-hook-form'; + +import { + Box, + Button, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Text, +} from '@chakra-ui/react'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { StdPublicKeys } from '@spacemesh/sm-codec'; + +import { useCurrentHRP } from '../hooks/useNetworkSelectors'; +import { useAccountsList } from '../hooks/useWalletSelectors'; +import usePassword from '../store/usePassword'; +import useWallet from '../store/useWallet'; +import { + GENESIS_VESTING_ACCOUNTS, + GENESIS_VESTING_END, + GENESIS_VESTING_START, +} from '../utils/constants'; +import { noop } from '../utils/func'; +import { + getTemplateNameByKey, + MultiSigSpawnArguments, + SingleSigSpawnArguments, + VaultSpawnArguments, + VestingSpawnArguments, +} from '../utils/templates'; + +import { + extractSpawnArgs, + FormSchema, + FormValues, +} from './createAccountSchema'; +import FormAddressSelect from './FormAddressSelect'; +import FormInput from './FormInput'; +import FormKeySelect from './FormKeySelect'; +import FormMultiKeySelect from './FormMultiKeySelect'; +import FormSelect from './FormSelect'; + +type EditAccountModalProps = { + accountIndex: number; + isOpen: boolean; + onClose: () => void; +}; + +function EditAccountModal({ + accountIndex, + isOpen, + onClose, +}: EditAccountModalProps): JSX.Element { + const { editAccount, wallet } = useWallet(); + const { withPassword } = usePassword(); + const hrp = useCurrentHRP(); + const accounts = useAccountsList(hrp); + const keys = wallet?.keychain || []; + const defaultValues = { + displayName: '', + required: 1, + publicKeys: [keys[0]?.publicKey || '0x1'], + }; + const { + watch, + register, + unregister, + reset, + control, + handleSubmit, + setValue, + getValues, + formState: { errors, isSubmitted }, + } = useForm({ + resolver: zodResolver(FormSchema), + defaultValues, + }); + const selectedTemplate = watch('templateAddress'); + const selectedOwner = watch('Owner'); + const totalAmount = watch('TotalAmount'); + + const account = wallet?.accounts[accountIndex]; + if (!account) { + throw new Error(`Account with index ${accountIndex} not found`); + } + + useEffect(() => { + const formValues = { + displayName: account.displayName, + templateAddress: account.templateAddress, + ...account.spawnArguments, + } as Parameters[0]; + reset(formValues); + + return () => reset(); + }, [account, reset]); + + useEffect(() => { + if (selectedTemplate === StdPublicKeys.Vault) { + register('InitialUnlockAmount', { + required: 'Please specify the initial unlock amount', + }); + return () => unregister('InitialUnlockAmount'); + } + return noop; + }, [register, selectedTemplate, unregister]); + + useEffect(() => { + const owner = selectedOwner || getValues('Owner'); + if (Object.hasOwn(GENESIS_VESTING_ACCOUNTS, owner)) { + const amount = + GENESIS_VESTING_ACCOUNTS[ + owner as keyof typeof GENESIS_VESTING_ACCOUNTS + ]; + setValue('TotalAmount', String(amount)); + setValue('InitialUnlockAmount', String(amount / 4n)); + setValue('VestingStart', GENESIS_VESTING_START); + setValue('VestingEnd', GENESIS_VESTING_END); + } + }, [getValues, selectedOwner, selectedTemplate, setValue]); + + useEffect(() => { + if (totalAmount) { + setValue('InitialUnlockAmount', String(BigInt(totalAmount) / 4n)); + } + }, [totalAmount, setValue]); + + const submit = handleSubmit(async (data) => { + const success = await withPassword( + (password) => + editAccount( + accountIndex, + data.displayName, + data.templateAddress, + extractSpawnArgs(data), + password + ), + 'Edit Account', + // eslint-disable-next-line max-len + `Please enter the password to save changes in the account "${ + data.displayName + }" of type "${getTemplateNameByKey(data.templateAddress)}"` + ); + if (success) { + reset(defaultValues); + onClose(); + } + }); + + const renderTemplateSpecificFields = () => { + switch (selectedTemplate) { + case StdPublicKeys.Vault: { + const args = account.spawnArguments as VaultSpawnArguments; + return ( + <> + Vault + + The vault holds some funds for its owner and unlocks them due to + pre-defined vesting schedule. + + acc.templateAddress === StdPublicKeys.Vesting + )} + register={register} + unregister={unregister} + errors={errors} + isSubmitted={isSubmitted} + isRequired + setValue={setValue} + getValues={getValues} + /> + + + + + ); + } + case StdPublicKeys.Vesting: { + const args = account.spawnArguments as VestingSpawnArguments; + return ( + <> + Vesting Account + + It is used to drain Vault Accounts + + { + if (n < 1) { + return 'Required amount must be grater than 0'; + } + if (n > 10) { + return 'Required amount must be less or equal to 10'; + } + return true; + }, + })} + errors={errors} + isSubmitted={isSubmitted} + /> + + + ); + } + case StdPublicKeys.MultiSig: { + const args = account.spawnArguments as MultiSigSpawnArguments; + return ( + <> + Multiple Signatures + + MultiSig account requires more than one signature to submit a + transaction. You need to know Public Keys of other parties to + create a MultiSig account. + + { + if (n < 1) { + return 'Required amount must be grater than 0'; + } + if (n > 10) { + return 'Required amount must be less or equal to 10'; + } + return true; + }, + })} + errors={errors} + isSubmitted={isSubmitted} + /> + + + ); + } + case StdPublicKeys.SingleSig: + default: { + const args = account.spawnArguments as SingleSigSpawnArguments; + return ( + <> + Single Signature + + The default account that requires only one signature to submit a + transaction. + + + + ); + } + } + }; + + return ( + +
+ + + + Edit Account + + + + + {renderTemplateSpecificFields()} + + + + + + + +
+ ); +} + +export default EditAccountModal; diff --git a/src/components/FormAddressSelect.tsx b/src/components/FormAddressSelect.tsx index 9c97eff..323ad28 100644 --- a/src/components/FormAddressSelect.tsx +++ b/src/components/FormAddressSelect.tsx @@ -66,24 +66,35 @@ function FormAddressSelect>({ setValue, getValues, }: Props): JSX.Element { + const value = getValues(fieldName); + const isLocalAddress = !!accounts.find((x) => x.address === value); const [origin, setOrigin] = useState( - defaultForeign ? Origin.Foreign : Origin.Local + defaultForeign || (value && !isLocalAddress) ? Origin.Foreign : Origin.Local ); const prevOrigin = useRef(origin); + useEffect(() => () => unregister(fieldName), [unregister, fieldName, origin]); useEffect(() => { const firstLocal = accounts?.[0]?.address; - const val = getValues(fieldName); - const foundVal = accounts.find((x) => x.address === val); if ( prevOrigin.current !== origin && origin === Origin.Local && !!firstLocal && - !foundVal + !isLocalAddress ) { + // If switched from foreign to local, + // but values is not a local one — switch to the first local setValue(fieldName, firstLocal as PathValue); } - }, [prevOrigin, origin, accounts, setValue, fieldName, getValues]); + }, [ + prevOrigin, + origin, + accounts, + setValue, + fieldName, + value, + isLocalAddress, + ]); const error = get(errors, fieldName) as FieldError | undefined; const renderInputs = () => { @@ -93,9 +104,7 @@ function FormAddressSelect>({ , + value, validate: (val) => { try { Bech32AddressSchema.parse(val); @@ -110,7 +119,14 @@ function FormAddressSelect>({ case Origin.Local: default: return ( - , + })} + > {accounts.map((acc) => (