From eb0709fff82ecb0a036cd561b10b759c75c398e9 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Fri, 11 Apr 2025 19:38:06 +0800 Subject: [PATCH 01/44] refactor: AddNewAccount --- app/components/Nav/App/App.tsx | 2 + .../Views/AccountSelector/AccountSelector.tsx | 2 +- .../AddAccountActions/AddAccountActions.tsx | 28 +- .../AddNewAccount.styles.ts} | 2 +- .../AddNewAccount.test.tsx} | 57 ++-- .../Views/AddNewAccount/AddNewAccount.tsx | 257 ++++++++++++++++++ .../AddNewAccount/AddNewAccount.types.ts | 17 ++ .../index.tsx | 0 .../Views/AddNewHdAccount/AddNewHdAccount.tsx | 181 ------------ .../AddNewHdAccount/AddNewHdAccount.types.ts | 3 - app/constants/navigation/Routes.ts | 1 + app/core/Engine/Engine.ts | 5 + .../SnapKeyring/MultichainWalletSnapClient.ts | 87 ++++++ 13 files changed, 415 insertions(+), 227 deletions(-) rename app/components/Views/{AddNewHdAccount/AddNewHdAccount.styles.ts => AddNewAccount/AddNewAccount.styles.ts} (96%) rename app/components/Views/{AddNewHdAccount/AddNewHdAccount.test.tsx => AddNewAccount/AddNewAccount.test.tsx} (84%) create mode 100644 app/components/Views/AddNewAccount/AddNewAccount.tsx create mode 100644 app/components/Views/AddNewAccount/AddNewAccount.types.ts rename app/components/Views/{AddNewHdAccount => AddNewAccount}/index.tsx (100%) delete mode 100644 app/components/Views/AddNewHdAccount/AddNewHdAccount.tsx delete mode 100644 app/components/Views/AddNewHdAccount/AddNewHdAccount.types.ts create mode 100644 app/core/SnapKeyring/MultichainWalletSnapClient.ts diff --git a/app/components/Nav/App/App.tsx b/app/components/Nav/App/App.tsx index 1188e189d951..64f496b51362 100644 --- a/app/components/Nav/App/App.tsx +++ b/app/components/Nav/App/App.tsx @@ -135,6 +135,7 @@ import ImportNewSecretRecoveryPhrase from '../../Views/ImportNewSecretRecoveryPh import { SelectSRPBottomSheet } from '../../Views/SelectSRP/SelectSRPBottomSheet'; ///: END:ONLY_INCLUDE_IF import NavigationService from '../../../core/NavigationService'; +import AddNewAccount from '../../Views/AddNewAccount'; const clearStackNavigatorOptions = { headerShown: false, @@ -336,6 +337,7 @@ const RootModalFlow = ( name={Routes.SHEET.ACCOUNT_SELECTOR} component={AccountSelector} /> + { const dispatch = useDispatch(); diff --git a/app/components/Views/AddAccountActions/AddAccountActions.tsx b/app/components/Views/AddAccountActions/AddAccountActions.tsx index 37375823e2b5..8fc493d27f9c 100644 --- a/app/components/Views/AddAccountActions/AddAccountActions.tsx +++ b/app/components/Views/AddAccountActions/AddAccountActions.tsx @@ -51,6 +51,7 @@ import { import { BitcoinWalletSnapSender } from '../../../core/SnapKeyring/BitcoinWalletSnap'; // eslint-disable-next-line no-duplicate-imports, import/no-duplicates import { BtcScope } from '@metamask/keyring-api'; +import { WalletClientType } from '../../../core/SnapKeyring/MultichainWalletSnapClient'; ///: END:ONLY_INCLUDE_IF const AddAccountActions = (props: AddAccountActionsProps) => { @@ -69,6 +70,7 @@ const AddAccountActions = (props: AddAccountActionsProps) => { const [isLoading, setIsLoading] = useState(false); ///: BEGIN:ONLY_INCLUDE_IF(multi-srp) const hdKeyrings = useSelector(selectHDKeyrings); + const useCreateAccountWithSrps = hdKeyrings.length > 1; ///: END:ONLY_INCLUDE_IF const openImportAccount = useCallback(() => { @@ -143,6 +145,14 @@ const AddAccountActions = (props: AddAccountActionsProps) => { ); const createBitcoinAccount = async (scope: CaipChainId) => { + if (useCreateAccountWithSrps) { + navigate(Routes.SHEET.ADD_ACCOUNT, { + scope, + clientType: WalletClientType.Bitcoin, + }); + return; + } + try { setIsLoading(true); // Client to create the account using the Bitcoin Snap @@ -162,13 +172,21 @@ const AddAccountActions = (props: AddAccountActionsProps) => { ///: END:ONLY_INCLUDE_IF ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) const createSolanaAccount = async (scope: CaipChainId) => { + trace({ + name: TraceName.CreateSnapAccount, + op: TraceOperation.CreateSnapAccount, + tags: getTraceTags(store.getState()), + }); + if (useCreateAccountWithSrps) { + navigate(Routes.SHEET.ADD_ACCOUNT, { + scope, + clientType: WalletClientType.Solana, + }); + return; + } + try { setIsLoading(true); - trace({ - name: TraceName.CreateSnapAccount, - op: TraceOperation.CreateSnapAccount, - tags: getTraceTags(store.getState()), - }); // Client to create the account using the Solana Snap const client = new KeyringClient(new SolanaWalletSnapSender()); // This will trigger the Snap account creation flow (+ account renaming) diff --git a/app/components/Views/AddNewHdAccount/AddNewHdAccount.styles.ts b/app/components/Views/AddNewAccount/AddNewAccount.styles.ts similarity index 96% rename from app/components/Views/AddNewHdAccount/AddNewHdAccount.styles.ts rename to app/components/Views/AddNewAccount/AddNewAccount.styles.ts index f232d557117c..f77b9b19c6f1 100644 --- a/app/components/Views/AddNewHdAccount/AddNewHdAccount.styles.ts +++ b/app/components/Views/AddNewAccount/AddNewAccount.styles.ts @@ -3,7 +3,7 @@ import { StyleSheet } from 'react-native'; import { Theme } from '../../../util/theme/models'; /** - * Style sheet function for AddNewHdAccount component. + * Style sheet function for AddNewAccount component. * * @returns StyleSheet object. */ diff --git a/app/components/Views/AddNewHdAccount/AddNewHdAccount.test.tsx b/app/components/Views/AddNewAccount/AddNewAccount.test.tsx similarity index 84% rename from app/components/Views/AddNewHdAccount/AddNewHdAccount.test.tsx rename to app/components/Views/AddNewAccount/AddNewAccount.test.tsx index 07f334f23f8d..9897d9b7d975 100644 --- a/app/components/Views/AddNewHdAccount/AddNewHdAccount.test.tsx +++ b/app/components/Views/AddNewAccount/AddNewAccount.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { fireEvent } from '@testing-library/react-native'; import renderWithProvider from '../../../util/test/renderWithProvider'; import { strings } from '../../../../locales/i18n'; -import AddNewHdAccount from './AddNewHdAccount'; +import AddNewAccount from './AddNewAccount'; import { backgroundState } from '../../../util/test/initial-root-state'; import { MOCK_ACCOUNTS_CONTROLLER_STATE, @@ -72,14 +72,14 @@ jest.mock('../../../core/Engine', () => { jest.mocked(Engine); -describe('AddNewHdAccount', () => { +describe('AddNewAccount', () => { beforeEach(() => { jest.clearAllMocks(); }); it('shows next available account name as placeholder', () => { const { getByPlaceholderText } = renderWithProvider( - , + , { state: initialState, }, @@ -89,7 +89,7 @@ describe('AddNewHdAccount', () => { it('handles account name input', () => { const { getByPlaceholderText } = renderWithProvider( - , + , { state: initialState, }, @@ -101,12 +101,9 @@ describe('AddNewHdAccount', () => { }); it('shows SRP list when selector is clicked', () => { - const { getByText } = renderWithProvider( - , - { - state: initialState, - }, - ); + const { getByText } = renderWithProvider(, { + state: initialState, + }); const srpSelector = getByText( strings('accounts.select_secret_recovery_phrase'), @@ -120,7 +117,7 @@ describe('AddNewHdAccount', () => { it('handles SRP selection', async () => { const { getByText, queryByText } = renderWithProvider( - , + , { state: initialState, }, @@ -144,12 +141,9 @@ describe('AddNewHdAccount', () => { }); it('handles account creation', async () => { - const { getByText } = renderWithProvider( - , - { - state: initialState, - }, - ); + const { getByText } = renderWithProvider(, { + state: initialState, + }); const addButton = getByText(strings('accounts.add')); fireEvent.press(addButton); @@ -162,7 +156,7 @@ describe('AddNewHdAccount', () => { it('handles account creation with custom name', async () => { const { getByText, getByPlaceholderText } = renderWithProvider( - , + , { state: initialState, }, @@ -181,12 +175,9 @@ describe('AddNewHdAccount', () => { }); it('handles cancellation', () => { - const { getByText } = renderWithProvider( - , - { - state: initialState, - }, - ); + const { getByText } = renderWithProvider(, { + state: initialState, + }); const cancelButton = getByText(strings('accounts.cancel')); fireEvent.press(cancelButton); @@ -195,12 +186,9 @@ describe('AddNewHdAccount', () => { }); it('handles back navigation from SRP list', () => { - const { getByText } = renderWithProvider( - , - { - state: initialState, - }, - ); + const { getByText } = renderWithProvider(, { + state: initialState, + }); const srpSelector = getByText( strings('accounts.select_secret_recovery_phrase'), @@ -219,12 +207,9 @@ describe('AddNewHdAccount', () => { const mockError = new Error('Failed to create account'); mockAddNewHdAccount.mockRejectedValueOnce(mockError); - const { getByText } = renderWithProvider( - , - { - state: initialState, - }, - ); + const { getByText } = renderWithProvider(, { + state: initialState, + }); const addButton = getByText(strings('accounts.add')); await fireEvent.press(addButton); diff --git a/app/components/Views/AddNewAccount/AddNewAccount.tsx b/app/components/Views/AddNewAccount/AddNewAccount.tsx new file mode 100644 index 000000000000..22ee053f9d11 --- /dev/null +++ b/app/components/Views/AddNewAccount/AddNewAccount.tsx @@ -0,0 +1,257 @@ +// Third party dependencies. +import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react'; +import { SafeAreaView, View } from 'react-native'; + +// External dependencies. +import SheetHeader from '../../../component-library/components/Sheet/SheetHeader'; +import Icon, { + IconName, +} from '../../../component-library/components/Icons/Icon'; +import { strings } from '../../../../locales/i18n'; +import Engine from '../../../core/Engine'; + +// Internal dependencies +import { AddNewAccountProps } from './AddNewAccount.types'; +import { AddNewHdAccountIds } from '../../../../e2e/selectors/MultiSRP/AddHdAccount.selectors'; +import { addNewHdAccount } from '../../../actions/multiSrp'; +import Text, { + TextColor, + TextVariant, +} from '../../../component-library/components/Texts/Text'; +import Input from '../../../component-library/components/Form/TextField/foundation/Input'; +import { useStyles } from '../../hooks/useStyles'; +import styleSheet from './AddNewAccount.styles'; +import { TouchableOpacity } from 'react-native-gesture-handler'; +import { useSelector } from 'react-redux'; +import { selectHDKeyrings } from '../../../selectors/keyringController'; +import Button, { + ButtonVariants, +} from '../../../component-library/components/Buttons/Button'; +import SRPList from '../../UI/SRPList'; +import Logger from '../../../util/Logger'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import { getHdKeyringOfSelectedAccountOrPrimaryKeyring } from '../../../selectors/multisrp'; +import { + MultichainWalletSnapClient, + WalletClientType, +} from '../../../core/SnapKeyring/MultichainWalletSnapClient'; +import { MultichainNetwork } from '@metamask/multichain-transactions-controller'; +import BottomSheet, { + BottomSheetRef, +} from '../../../component-library/components/BottomSheets/BottomSheet'; +import { useNavigation } from '@react-navigation/native'; +import Routes from '../../../constants/navigation/Routes'; + +const AddNewAccount = ({ route }: AddNewAccountProps) => { + Logger.log(111, route.params); + const { navigate } = useNavigation(); + const { scope, clientType } = route.params || {}; + const sheetRef = useRef(null); + const { styles, theme } = useStyles(styleSheet, {}); + const { colors } = theme; + const [isLoading, setIsLoading] = useState(false); + const [accountName, setAccountName] = useState(undefined); + const keyringOfSelectedAccount = useSelector( + getHdKeyringOfSelectedAccountOrPrimaryKeyring, + ); + const [keyringId, setKeyringId] = useState( + keyringOfSelectedAccount.metadata.id, + ); + const hdKeyrings = useSelector(selectHDKeyrings); + const [showSRPList, setShowSRPList] = useState(false); + + const onBack = () => { + navigate(Routes.SHEET.ACCOUNT_SELECTOR); + }; + + const onSubmit = async () => { + if ((clientType && !scope) || (!clientType && scope)) { + throw new Error('Scope and clientType must be provided'); + } + + setIsLoading(true); + try { + if (clientType && scope) { + Logger.log('using multichain wallet snap client', clientType, scope); + const multichainWalletSnapClient = new MultichainWalletSnapClient( + clientType, + ); + await multichainWalletSnapClient.createAccount({ + scope, + accountNameSuggestion: accountName, + entropySource: keyringId, + }); + } else { + await addNewHdAccount(keyringId, accountName); + } + navigate(Routes.WALLET.HOME); + } catch (e) { + Logger.error(e as Error, 'ADD_NEW_HD_ACCOUNT_ERROR'); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + const nextAvailableAccountName = + Engine.context.AccountsController.getNextAvailableAccountName( + clientType ? KeyringTypes.snap : KeyringTypes.hd, + ); + const accountNumber = nextAvailableAccountName.split(' ').pop(); + + let accountNameToUse = nextAvailableAccountName; + switch (clientType) { + case WalletClientType.Bitcoin: { + if (scope === MultichainNetwork.BitcoinTestnet) { + accountNameToUse = `${strings( + 'accounts.bitcoin_testnet_account_name', + )} ${accountNumber}`; + break; + } + accountNameToUse = `${strings( + 'accounts.bitcoin_account_name', + )} ${accountNumber}`; + break; + } + case WalletClientType.Solana: { + switch (scope) { + case MultichainNetwork.SolanaDevnet: + accountNameToUse = `${strings( + 'accounts.solana_devnet_account_name', + )} ${accountNumber}`; + break; + case MultichainNetwork.SolanaTestnet: + accountNameToUse = `${strings( + 'accounts.solana_testnet_account_name', + )} ${accountNumber}`; + break; + default: + accountNameToUse = `${strings( + 'accounts.solana_account_name', + )} ${accountNumber}`; + break; + } + break; + } + default: + break; + } + setAccountName(accountNameToUse); + }, [clientType, scope]); + + const hdKeyringIndex = useMemo( + () => + hdKeyrings.findIndex((keyring) => keyring.metadata.id === keyringId) + 1, + [hdKeyrings, keyringId], + ); + + const numberOfAccounts = useMemo(() => { + const keyring = hdKeyrings.find((kr) => kr.metadata.id === keyringId); + return keyring ? keyring.accounts.length : 0; + }, [hdKeyrings, keyringId]); + + const onKeyringSelection = (id: string) => { + setShowSRPList(false); + setKeyringId(id); + }; + + return ( + + + + { + if (showSRPList) { + setShowSRPList(false); + return; + } + onBack(); + }} + /> + {showSRPList ? ( + onKeyringSelection(id)} /> + ) : ( + + + + { + setAccountName(newName); + }} + placeholder={accountName} + placeholderTextColor={colors.text.default} + onSubmitEditing={onSubmit} + /> + + + + {strings('accounts.select_secret_recovery_phrase')} + + setShowSRPList(true)} + testID={AddNewHdAccountIds.SRP_SELECTOR} + > + + + {`${strings( + 'accounts.secret_recovery_phrase', + )} ${hdKeyringIndex}`} + + + {`${strings( + 'accounts.show_accounts', + )} ${numberOfAccounts} ${strings( + 'accounts.accounts', + )}`} + + + + + + {strings('accounts.add_new_hd_account_helper_text')} + + + +