From 6fb006fd7890d2a78ec270c2bfbdbf30d31ebc78 Mon Sep 17 00:00:00 2001 From: brusher_ru Date: Tue, 8 Oct 2024 18:01:35 -0300 Subject: [PATCH 1/4] chore: add `useWhyDidUpdate` hoor for development --- src/hooks/useWhyDidUpdate.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/hooks/useWhyDidUpdate.ts diff --git a/src/hooks/useWhyDidUpdate.ts b/src/hooks/useWhyDidUpdate.ts new file mode 100644 index 0000000..c9098d2 --- /dev/null +++ b/src/hooks/useWhyDidUpdate.ts @@ -0,0 +1,32 @@ +import { useEffect, useRef } from 'react'; + +type IProps = Record; + +function useWhyDidUpdate(componentName: string, props: IProps) { + const prevProps = useRef({}); + + useEffect(() => { + if (prevProps.current) { + const allKeys = Object.keys({ ...prevProps.current, ...props }); + const changedProps: IProps = {}; + + allKeys.forEach((key) => { + if (!Object.is(prevProps.current[key], props[key])) { + changedProps[key] = { + from: prevProps.current[key], + to: props[key], + }; + } + }); + + if (Object.keys(changedProps).length) { + // eslint-disable-next-line no-console + console.log('[why-did-you-update]', componentName, changedProps); + } + } + + prevProps.current = props; + }); +} + +export default useWhyDidUpdate; From 5cd6294e10605373a7b948dd0170f7077d699495 Mon Sep 17 00:00:00 2001 From: brusher_ru Date: Tue, 8 Oct 2024 18:01:52 -0300 Subject: [PATCH 2/4] feat: add `Create New Key` option in Create Account modal --- src/components/CreateAccountModal.tsx | 124 +++++++++++++++++++++---- src/components/FormKeySelect.tsx | 26 +++++- src/components/FormMultiKeySelect.tsx | 16 +++- src/components/createAccountSchema.tsx | 12 ++- src/utils/account.ts | 29 ++++++ src/utils/constants.ts | 2 + 6 files changed, 184 insertions(+), 25 deletions(-) diff --git a/src/components/CreateAccountModal.tsx b/src/components/CreateAccountModal.tsx index 651affa..6660a18 100644 --- a/src/components/CreateAccountModal.tsx +++ b/src/components/CreateAccountModal.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import { Form, useForm } from 'react-hook-form'; import { @@ -20,7 +20,10 @@ import { useCurrentHRP } from '../hooks/useNetworkSelectors'; import { useAccountsList } from '../hooks/useWalletSelectors'; import usePassword from '../store/usePassword'; import useWallet from '../store/useWallet'; +import { findUnusedKey, getUsedPublicKeys } from '../utils/account'; +import Bip32KeyDerivation from '../utils/bip32'; import { + CREATE_NEW_KEY_LITERAL, GENESIS_VESTING_ACCOUNTS, GENESIS_VESTING_END, GENESIS_VESTING_START, @@ -48,11 +51,11 @@ function CreateAccountModal({ isOpen, onClose, }: CreateAccountModalProps): JSX.Element { - const { createAccount, wallet } = useWallet(); + const { createAccount, createKeyPair, wallet } = useWallet(); const { withPassword } = usePassword(); const hrp = useCurrentHRP(); const accounts = useAccountsList(hrp); - const keys = wallet?.keychain || []; + const keys = useMemo(() => wallet?.keychain || [], [wallet]); const defaultValues = { displayName: '', Required: 1, @@ -75,6 +78,32 @@ function CreateAccountModal({ const selectedTemplate = watch('templateAddress'); const selectedOwner = watch('Owner'); const totalAmount = watch('TotalAmount'); + const selectedPublicKey = watch('PublicKey'); + const selectedPublicKeys = watch('PublicKeys'); + + const usedPublicKeys = useMemo( + () => getUsedPublicKeys(accounts, keys), + [accounts, keys] + ); + const unusedKey = useMemo( + () => findUnusedKey(keys, usedPublicKeys), + [keys, usedPublicKeys] + ); + + const isKeyUsed = (() => { + if (selectedTemplate === StdPublicKeys.SingleSig) { + return usedPublicKeys.has(selectedPublicKey); + } + if ( + selectedTemplate === StdPublicKeys.MultiSig || + selectedTemplate === StdPublicKeys.Vesting + ) { + return (selectedPublicKeys || []).some((pk: string) => + usedPublicKeys.has(pk) + ); + } + return false; + })(); useEffect(() => { if (selectedTemplate === StdPublicKeys.Vault) { @@ -87,16 +116,18 @@ function CreateAccountModal({ }, [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); + if (selectedTemplate === StdPublicKeys.Vault) { + 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]); @@ -106,20 +137,66 @@ function CreateAccountModal({ } }, [totalAmount, setValue]); + const multiKeyValues = useMemo( + () => [unusedKey?.publicKey || CREATE_NEW_KEY_LITERAL], + [unusedKey] + ); + const close = () => { reset(defaultValues); onClose(); }; + const createNewKeyPairIfNeeded = async ( + values: FormValues, + password: string + ): Promise => { + if ( + values.templateAddress === StdPublicKeys.SingleSig && + values.PublicKey === CREATE_NEW_KEY_LITERAL + ) { + const path = Bip32KeyDerivation.createPath(wallet?.keychain?.length || 0); + const key = await createKeyPair(values.displayName, path, password); + return { ...values, PublicKey: key.publicKey }; + } + if ( + (values.templateAddress === StdPublicKeys.MultiSig || + values.templateAddress === StdPublicKeys.Vesting) && + values.PublicKeys.some((pk) => pk === CREATE_NEW_KEY_LITERAL) + ) { + let keysCreated = 0; + const newKeys = await values.PublicKeys.reduce(async (acc, pk, idx) => { + const prev = await acc; + if (pk === CREATE_NEW_KEY_LITERAL) { + const path = Bip32KeyDerivation.createPath( + (wallet?.keychain?.length || 0) + keysCreated + ); + keysCreated += 1; + const newKey = await createKeyPair( + `${values.displayName} #${idx}`, + path, + password + ).then((k) => k.publicKey); + return [...prev, newKey]; + } + return [...prev, pk]; + }, Promise.resolve([] as string[])); + return { ...values, PublicKeys: newKeys }; + } + return values; + }; + const submit = handleSubmit(async (data) => { const success = await withPassword( - (password) => - createAccount( + async (password) => { + const formValues = await createNewKeyPairIfNeeded(data, password); + return createAccount( data.displayName, data.templateAddress, - extractSpawnArgs(data), + extractSpawnArgs(formValues), password - ), + ); + }, 'Create an Account', // eslint-disable-next-line max-len `Please enter the password to create the new account "${ @@ -222,6 +299,7 @@ function CreateAccountModal({ unregister={unregister} errors={errors} isSubmitted={isSubmitted} + hasCreateOption /> ); @@ -263,6 +341,8 @@ function CreateAccountModal({ unregister={unregister} errors={errors} isSubmitted={isSubmitted} + values={multiKeyValues} + hasCreateOption /> ); @@ -283,7 +363,8 @@ function CreateAccountModal({ errors={errors} isSubmitted={isSubmitted} isRequired - value={keys[0]?.publicKey} + value={unusedKey?.publicKey} + hasCreateOption /> ); @@ -327,6 +408,13 @@ function CreateAccountModal({ {renderTemplateSpecificFields()} + {isKeyUsed && ( + + The selected key is already used in another account. +
+ Are you sure you want to create another one? +
+ )}