From dc57fe21b74b248e070b95f438f484d3459cec25 Mon Sep 17 00:00:00 2001 From: Fumiko Richards <6504659+frichards@users.noreply.github.com> Date: Fri, 20 Dec 2024 11:53:48 -0700 Subject: [PATCH] feat: (dApp Handler) CP-9471 expose delete accounts endpoint (#115) --- .../connections/dAppConnection/models.ts | 1 + .../connections/dAppConnection/registry.ts | 2 + .../connections/extensionConnection/models.ts | 1 - .../extensionConnection/registry.ts | 2 - .../middlewares/PermissionMiddleware.ts | 1 + .../handlers/avalanche_deleteAccounts.test.ts | 246 ++++++++++++++++++ .../handlers/avalanche_deleteAccounts.ts | 215 +++++++++++++++ .../accounts/handlers/deleteAccounts.test.ts | 45 ---- .../accounts/handlers/deleteAccounts.ts | 35 --- .../common/approval/ApprovalSection.tsx | 4 +- src/contexts/AccountsProvider.tsx | 6 +- src/localization/locales/en/translation.json | 8 + src/pages/Wallet/DeleteAccounts.tsx | 202 ++++++++++++++ src/popup/ApprovalRoutes.tsx | 9 + 14 files changed, 690 insertions(+), 87 deletions(-) create mode 100644 src/background/services/accounts/handlers/avalanche_deleteAccounts.test.ts create mode 100644 src/background/services/accounts/handlers/avalanche_deleteAccounts.ts delete mode 100644 src/background/services/accounts/handlers/deleteAccounts.test.ts delete mode 100644 src/background/services/accounts/handlers/deleteAccounts.ts create mode 100644 src/pages/Wallet/DeleteAccounts.tsx diff --git a/src/background/connections/dAppConnection/models.ts b/src/background/connections/dAppConnection/models.ts index b76ef3473..830842d6b 100644 --- a/src/background/connections/dAppConnection/models.ts +++ b/src/background/connections/dAppConnection/models.ts @@ -34,6 +34,7 @@ export enum DAppProviderRequest { AVALANCHE_SET_DEVELOPER_MODE = 'avalanche_setDeveloperMode', ACCOUNT_SELECT = 'avalanche_selectAccount', ACCOUNT_RENAME = 'avalanche_renameAccount', + ACCOUNTS_DELETE = 'avalanche_deleteAccounts', AVALANCHE_GET_ACCOUNT_PUB_KEY = 'avalanche_getAccountPubKey', AVALANCHE_SEND_TRANSACTION = 'avalanche_sendTransaction', AVALANCHE_SIGN_TRANSACTION = 'avalanche_signTransaction', diff --git a/src/background/connections/dAppConnection/registry.ts b/src/background/connections/dAppConnection/registry.ts index c5246db61..ffde2a643 100644 --- a/src/background/connections/dAppConnection/registry.ts +++ b/src/background/connections/dAppConnection/registry.ts @@ -31,6 +31,7 @@ import { AvalancheSignTransactionHandler } from '@src/background/services/wallet import { AvalancheSignMessageHandler } from '@src/background/services/messages/handlers/avalanche_signMessage'; import { AvalancheRenameAccountHandler } from '@src/background/services/accounts/handlers/avalanche_renameAccount'; import { AvalancheRenameWalletHandler } from '@src/background/services/secrets/handlers/avalanche_renameWallet'; +import { AvalancheDeleteAccountsHandler } from '@src/background/services/accounts/handlers/avalanche_deleteAccounts'; /** * TODO: GENERATE THIS FILE AS PART OF THE BUILD PROCESS @@ -56,6 +57,7 @@ import { AvalancheRenameWalletHandler } from '@src/background/services/secrets/h { token: 'DAppRequestHandler', useToken: AvalancheSignTransactionHandler }, { token: 'DAppRequestHandler', useToken: AvalancheSignMessageHandler }, { token: 'DAppRequestHandler', useToken: AvalancheRenameAccountHandler }, + { token: 'DAppRequestHandler', useToken: AvalancheDeleteAccountsHandler }, { token: 'DAppRequestHandler', useToken: AvalancheGetAddressesInRangeHandler, diff --git a/src/background/connections/extensionConnection/models.ts b/src/background/connections/extensionConnection/models.ts index 94b73d7f8..4d904f0ad 100644 --- a/src/background/connections/extensionConnection/models.ts +++ b/src/background/connections/extensionConnection/models.ts @@ -19,7 +19,6 @@ export enum ExtensionRequest { ACCOUNT_GET_ACCOUNTS = 'account_get', ACCOUNT_SELECT = 'account_select', ACCOUNT_ADD = 'account_add', - ACCOUNT_DELETE = 'account_delete', ACCOUNT_GET_PRIVATEKEY = 'account_get_privatekey', BALANCES_GET = 'balances_get', diff --git a/src/background/connections/extensionConnection/registry.ts b/src/background/connections/extensionConnection/registry.ts index f2e7e4867..f280265fa 100644 --- a/src/background/connections/extensionConnection/registry.ts +++ b/src/background/connections/extensionConnection/registry.ts @@ -79,7 +79,6 @@ import { GetAvaxBalanceHandler } from '@src/background/services/balances/handler import { GetLedgerVersionWarningHandler } from '@src/background/services/ledger/handlers/getLedgerVersionWarning'; import { LedgerVersionWarningClosedHandler } from '@src/background/services/ledger/handlers/setLedgerVersionWarningClosed'; import { SetLanguageHandler } from '@src/background/services/settings/handlers/setLanguage'; -import { DeleteAccountHandler } from '@src/background/services/accounts/handlers/deleteAccounts'; import { MigrateMissingPublicKeysFromLedgerHandler } from '@src/background/services/ledger/handlers/migrateMissingPublicKeysFromLedger'; import { KeystoneRequestEvents } from '@src/background/services/keystone/events/keystoneDeviceRequest'; import { SubmitKeystoneSignature } from '@src/background/services/keystone/handlers/keystoneSubmitSignature'; @@ -140,7 +139,6 @@ import { GetTotalBalanceForWalletHandler } from '@src/background/services/balanc { token: 'ExtensionRequestHandler', useToken: AddAccountHandler }, { token: 'ExtensionRequestHandler', useToken: GetAccountsHandler }, { token: 'ExtensionRequestHandler', useToken: SelectAccountHandler }, - { token: 'ExtensionRequestHandler', useToken: DeleteAccountHandler }, { token: 'ExtensionRequestHandler', useToken: GetActionHandler }, { token: 'ExtensionRequestHandler', useToken: UpdateActionHandler }, { token: 'ExtensionRequestHandler', useToken: UpdateActionTxDataHandler }, diff --git a/src/background/connections/middlewares/PermissionMiddleware.ts b/src/background/connections/middlewares/PermissionMiddleware.ts index 52957fe98..f1e7108c1 100644 --- a/src/background/connections/middlewares/PermissionMiddleware.ts +++ b/src/background/connections/middlewares/PermissionMiddleware.ts @@ -108,6 +108,7 @@ const CORE_METHODS = Object.freeze([ 'avalanche_selectAccount', 'avalanche_setDeveloperMode', DAppProviderRequest.ACCOUNT_RENAME, + DAppProviderRequest.ACCOUNTS_DELETE, DAppProviderRequest.BITCOIN_SEND_TRANSACTION, DAppProviderRequest.WALLET_GET_CHAIN, DAppProviderRequest.WALLET_RENAME, diff --git a/src/background/services/accounts/handlers/avalanche_deleteAccounts.test.ts b/src/background/services/accounts/handlers/avalanche_deleteAccounts.test.ts new file mode 100644 index 000000000..c927efbf4 --- /dev/null +++ b/src/background/services/accounts/handlers/avalanche_deleteAccounts.test.ts @@ -0,0 +1,246 @@ +import { ethErrors } from 'eth-rpc-errors'; + +import { buildRpcCall } from '@src/tests/test-utils'; +import { DAppProviderRequest } from '@src/background/connections/dAppConnection/models'; +import { openApprovalWindow } from '@src/background/runtime/openApprovalWindow'; +import { DEFERRED_RESPONSE } from '@src/background/connections/middlewares/models'; + +import { + AccountType, + type ImportedAccount, + type PrimaryAccount, +} from '../models'; +import { canSkipApproval } from '@src/utils/canSkipApproval'; +import { AvalancheDeleteAccountsHandler } from './avalanche_deleteAccounts'; +import type { WalletDetails } from '../../wallet/models'; + +jest.mock('@src/utils/canSkipApproval'); +jest.mock('@src/background/runtime/openApprovalWindow'); + +describe('src/background/services/accounts/handlers/avalanche_deleteAccounts', () => { + const deleteAccounts = jest.fn(); + const getAccountByID = jest.fn(); + const getPrimaryAccountsByWalletId = jest.fn(); + + const accountServiceMock = { + getAccountByID, + deleteAccounts, + getPrimaryAccountsByWalletId, + } as any; + + const getPrimaryWalletsDetails = jest.fn(); + + const secretsServiceMock = { + getPrimaryWalletsDetails, + } as any; + const wallet = { + id: 'walletId', + name: 'test wallet', + } as WalletDetails; + + const primaryAccount = { + id: 'primaryAccountId', + name: 'account name', + walletId: wallet.id, + type: AccountType.PRIMARY, + } as PrimaryAccount; + const primaryAccount2 = { + id: 'primaryAccountId2', + name: 'account name2', + walletId: wallet.id, + type: AccountType.PRIMARY, + } as PrimaryAccount; + const importedAccount = { + id: 'importedAccountId', + name: 'account name', + } as ImportedAccount; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('returns error when domain info is not known', async () => { + const handler = new AvalancheDeleteAccountsHandler({} as any, {} as any); + const request = { + id: '123', + method: DAppProviderRequest.ACCOUNTS_DELETE, + params: [['accountId']], + }; + + expect(await handler.handleAuthenticated(buildRpcCall(request))).toEqual({ + ...request, + error: ethErrors.rpc.invalidRequest('Missing dApp domain information'), + }); + }); + + it('prompts approval for non-core requests', async () => { + const handler = new AvalancheDeleteAccountsHandler( + accountServiceMock, + secretsServiceMock, + ); + + jest.mocked(canSkipApproval).mockResolvedValueOnce(false); + + getAccountByID.mockImplementation((id) => { + if (id === primaryAccount2.id) { + return primaryAccount2; + } + return importedAccount; + }); + getPrimaryAccountsByWalletId.mockReturnValue([ + primaryAccount, + primaryAccount2, + ]); + + getPrimaryWalletsDetails.mockResolvedValue([wallet]); + + const request = { + id: '123', + method: DAppProviderRequest.ACCOUNTS_DELETE, + params: [[primaryAccount2.id, importedAccount.id]], + site: { + domain: 'google.com', + tabId: 1, + }, + } as any; + + const result = await handler.handleAuthenticated(buildRpcCall(request)); + + expect(openApprovalWindow).toHaveBeenCalledWith( + expect.objectContaining({ + displayData: { + accounts: { + primary: { + [wallet.id]: [primaryAccount2], + }, + imported: [importedAccount], + wallet: { [wallet.id]: wallet.name }, + }, + }, + }), + 'deleteAccounts', + ); + expect(deleteAccounts).not.toHaveBeenCalled(); + expect(result).toEqual({ ...request, result: DEFERRED_RESPONSE }); + }); + + it('does not prompt approval for core suite', async () => { + const handler = new AvalancheDeleteAccountsHandler( + accountServiceMock, + secretsServiceMock, + ); + + jest.mocked(canSkipApproval).mockResolvedValueOnce(true); + + getAccountByID.mockReturnValueOnce(primaryAccount); + getPrimaryAccountsByWalletId.mockReturnValue([primaryAccount]); + + getPrimaryWalletsDetails.mockResolvedValue([wallet]); + const request = { + id: '123', + method: DAppProviderRequest.ACCOUNTS_DELETE, + params: [[primaryAccount.id]], + site: { + domain: 'core.app', + tabId: 1, + }, + } as any; + + const result = await handler.handleAuthenticated(buildRpcCall(request)); + + expect(openApprovalWindow).not.toHaveBeenCalled(); + expect(deleteAccounts).toHaveBeenCalledTimes(1); + expect(deleteAccounts).toHaveBeenCalledWith([primaryAccount.id]); + expect(result).toEqual({ ...request, result: null }); + }); + + it('returns error when deleting accounts fails', async () => { + jest.mocked(canSkipApproval).mockResolvedValueOnce(true); + + const handler = new AvalancheDeleteAccountsHandler( + accountServiceMock, + secretsServiceMock, + ); + + getAccountByID.mockReturnValueOnce(primaryAccount); + getPrimaryAccountsByWalletId.mockReturnValue([primaryAccount]); + + getPrimaryWalletsDetails.mockResolvedValue([wallet]); + const request = { + id: '123', + method: DAppProviderRequest.ACCOUNTS_DELETE, + params: [[primaryAccount.id]], + site: { + domain: 'core.app', + tabId: 1, + }, + } as any; + deleteAccounts.mockRejectedValueOnce(new Error('some error')); + const result = await handler.handleAuthenticated(buildRpcCall(request)); + expect(result).toEqual({ + ...request, + error: ethErrors.rpc.internal('Account removing failed'), + }); + }); + + it('returns error when no accounts were found using account IDs in request param', async () => { + jest.mocked(canSkipApproval).mockResolvedValueOnce(true); + + const handler = new AvalancheDeleteAccountsHandler( + accountServiceMock, + secretsServiceMock, + ); + + getAccountByID.mockReturnValueOnce(undefined); + + const request = { + id: '123', + method: DAppProviderRequest.ACCOUNTS_DELETE, + params: [['wrongId']], + site: { + domain: 'core.app', + tabId: 1, + }, + } as any; + const result = await handler.handleAuthenticated(buildRpcCall(request)); + expect(result).toEqual({ + ...request, + error: ethErrors.rpc.internal('No account with specified IDs'), + }); + }); + + it('returns error when requested account is not the last index in the wallet', async () => { + const handler = new AvalancheDeleteAccountsHandler( + accountServiceMock, + secretsServiceMock, + ); + + jest.mocked(canSkipApproval).mockResolvedValueOnce(false); + + getAccountByID.mockReturnValue(primaryAccount); + getPrimaryAccountsByWalletId.mockReturnValue([ + primaryAccount, + primaryAccount2, + ]); + + getPrimaryWalletsDetails.mockResolvedValue([wallet]); + + const request = { + id: '123', + method: DAppProviderRequest.ACCOUNTS_DELETE, + params: [[primaryAccount.id]], + site: { + domain: 'google.com', + tabId: 1, + }, + } as any; + + const result = await handler.handleAuthenticated(buildRpcCall(request)); + expect(result).toEqual({ + ...request, + error: ethErrors.rpc.internal( + 'Only the last account of the wallet can be removed', + ), + }); + }); +}); diff --git a/src/background/services/accounts/handlers/avalanche_deleteAccounts.ts b/src/background/services/accounts/handlers/avalanche_deleteAccounts.ts new file mode 100644 index 000000000..214f2a121 --- /dev/null +++ b/src/background/services/accounts/handlers/avalanche_deleteAccounts.ts @@ -0,0 +1,215 @@ +import { ethErrors } from 'eth-rpc-errors'; +import { injectable } from 'tsyringe'; + +import { DAppRequestHandler } from '@src/background/connections/dAppConnection/DAppRequestHandler'; +import { + DAppProviderRequest, + type JsonRpcRequestParams, +} from '@src/background/connections/dAppConnection/models'; +import { DEFERRED_RESPONSE } from '@src/background/connections/middlewares/models'; +import { openApprovalWindow } from '@src/background/runtime/openApprovalWindow'; +import { canSkipApproval } from '@src/utils/canSkipApproval'; + +import type { Action } from '../../actions/models'; +import { SecretsService } from '../../secrets/SecretsService'; +import { AccountsService } from '../AccountsService'; +import type { ImportedAccount, PrimaryAccount } from '../models'; +import { isPrimaryAccount } from '../utils/typeGuards'; + +type PrimaryWalletAccounts = { + [walletId: string]: PrimaryAccount[]; +}; + +export type DeleteAccountsDisplayData = { + primary: PrimaryWalletAccounts; + imported: ImportedAccount[]; + wallet: { + [walletId: string]: string; + }; +}; + +type Params = [accountIds: string[]]; + +@injectable() +export class AvalancheDeleteAccountsHandler extends DAppRequestHandler< + Params, + null +> { + methods = [DAppProviderRequest.ACCOUNTS_DELETE]; + + constructor( + private accountsService: AccountsService, + private secretsService: SecretsService, + ) { + super(); + } + + handleAuthenticated = async ( + rpcCall: JsonRpcRequestParams, + ) => { + const { request, scope } = rpcCall; + const [accountIds] = request.params; + + if (!request.site?.domain || !request.site?.tabId) { + return { + ...request, + error: ethErrors.rpc.invalidRequest({ + message: 'Missing dApp domain information', + }), + }; + } + + const primaryWalletAccounts: PrimaryWalletAccounts = {}; + const importedAccounts: ImportedAccount[] = []; + + // Getting accounts by ids from params and organizing + for (const accountId of accountIds) { + const account = this.accountsService.getAccountByID(accountId); + if (account) { + if (isPrimaryAccount(account)) { + if (primaryWalletAccounts[account.walletId]) { + primaryWalletAccounts[account.walletId]?.push(account); + } else { + primaryWalletAccounts[account.walletId] = [account]; + } + } else { + importedAccounts.push(account); + } + } + } + + if ( + !Object.keys(primaryWalletAccounts).length && + !importedAccounts.length + ) { + return { + ...request, + error: ethErrors.rpc.invalidParams({ + message: 'No account with specified IDs', + }), + }; + } + + //Validating to ensure that the accounts to be deleted has the latest index in the wallet + for (const [walletId, accountsInWallet] of Object.entries( + primaryWalletAccounts, + )) { + //Sort in descending order by index + accountsInWallet.sort((a, b) => b.index - a.index); + const walletAccounts = + this.accountsService.getPrimaryAccountsByWalletId(walletId); + + // This should not happen in normal cases. But need it to satisfy typescript + if (!walletAccounts || !walletAccounts.length) { + return { + ...request, + error: ethErrors.rpc.invalidParams({ + message: 'Unable to find the account', + }), + }; + } + + for (let i = 0; i < accountsInWallet.length; i++) { + const accountToDelete = accountsInWallet[i]; + const accountInWallet = walletAccounts[walletAccounts.length - 1 - i]; + if ( + !accountToDelete || + !accountInWallet || + accountToDelete.id !== accountInWallet.id + ) { + return { + ...request, + error: ethErrors.rpc.invalidParams({ + message: 'Only the last account of the wallet can be removed', + }), + }; + } + } + //Sort in ascending order by index + accountsInWallet.sort((a, b) => a.index - b.index); + } + + if (await canSkipApproval(request.site.domain, request.site.tabId)) { + const allPrimaryAccounts = Object.values(primaryWalletAccounts).flat(); + const allAccount = [...allPrimaryAccounts, ...importedAccounts]; + const allAccountIds = allAccount.map((account) => account.id); + try { + await this.accountsService.deleteAccounts(allAccountIds); + + return { + ...request, + result: null, + }; + } catch { + return { + ...request, + error: ethErrors.rpc.internal('Account removing failed'), + }; + } + } + + // Getting the wallet names + const walletNames: Record = {}; + const primaryWallets = await this.secretsService.getPrimaryWalletsDetails(); + + for (const walletId of Object.keys(primaryWalletAccounts)) { + const primaryWallet = primaryWallets.find( + (wallet) => wallet.id === walletId, + ); + + if (primaryWallet?.name) { + walletNames[walletId] = primaryWallet.name; + } + } + + const actionData: Action<{ + accounts: DeleteAccountsDisplayData; + }> = { + ...request, + scope, + displayData: { + accounts: { + primary: primaryWalletAccounts, + imported: importedAccounts, + wallet: walletNames, + }, + }, + }; + + await openApprovalWindow(actionData, 'deleteAccounts'); + + return { + ...request, + result: DEFERRED_RESPONSE, + }; + }; + + handleUnauthenticated = async ({ request }) => { + return { + ...request, + error: ethErrors.provider.unauthorized(), + }; + }; + + onActionApproved = async ( + pendingAction: Action<{ + accounts: DeleteAccountsDisplayData; + }>, + _, + onSuccess, + onError, + ) => { + try { + const { primary, imported } = pendingAction.displayData.accounts; + const allPrimaryAccounts = Object.values(primary).flat(); + const allAccount = [...allPrimaryAccounts, ...imported]; + const allAccountIds = allAccount.map((account) => account.id); + + await this.accountsService.deleteAccounts(allAccountIds); + + onSuccess(null); + } catch (e) { + onError(e); + } + }; +} diff --git a/src/background/services/accounts/handlers/deleteAccounts.test.ts b/src/background/services/accounts/handlers/deleteAccounts.test.ts deleted file mode 100644 index 15506b25b..000000000 --- a/src/background/services/accounts/handlers/deleteAccounts.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ExtensionRequest } from '@src/background/connections/extensionConnection/models'; -import { DeleteAccountHandler } from './deleteAccounts'; -import { buildRpcCall } from '@src/tests/test-utils'; - -describe('background/services/accounts/handlers/deleteAccount.ts', () => { - const deleteAccounstMock = jest.fn(); - const accountServiceMock = { - deleteAccounts: deleteAccounstMock, - } as any; - - beforeEach(() => { - jest.resetAllMocks(); - }); - - it('removes the imported accounts', async () => { - const handler = new DeleteAccountHandler(accountServiceMock); - const request = buildRpcCall({ - id: '123', - method: ExtensionRequest.ACCOUNT_DELETE, - params: [['0x1', '0x2']], - }); - - const result = await handler.handle(request); - - expect(deleteAccounstMock).toHaveBeenCalledWith(['0x1', '0x2']); - expect(result).toEqual({ ...request.request, result: 'success' }); - }); - - it('returns the error that happened during removal', async () => { - const error = new Error('foo'); - deleteAccounstMock.mockRejectedValueOnce(error); - - const handler = new DeleteAccountHandler(accountServiceMock); - const request = buildRpcCall({ - id: '123', - method: ExtensionRequest.ACCOUNT_DELETE, - params: [['0x1', '0x2']], - }); - - const result = await handler.handle(request); - - expect(deleteAccounstMock).toHaveBeenCalledWith(['0x1', '0x2']); - expect(result).toEqual({ ...request.request, error: error.toString() }); - }); -}); diff --git a/src/background/services/accounts/handlers/deleteAccounts.ts b/src/background/services/accounts/handlers/deleteAccounts.ts deleted file mode 100644 index de81221a1..000000000 --- a/src/background/services/accounts/handlers/deleteAccounts.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ExtensionRequest } from '@src/background/connections/extensionConnection/models'; -import { ExtensionRequestHandler } from '@src/background/connections/models'; -import { injectable } from 'tsyringe'; -import { AccountsService } from '../AccountsService'; - -type HandlerType = ExtensionRequestHandler< - ExtensionRequest.ACCOUNT_DELETE, - 'success' | 'error', - [ids: string[]] ->; - -@injectable() -export class DeleteAccountHandler implements HandlerType { - method = ExtensionRequest.ACCOUNT_DELETE as const; - - constructor(private accountsService: AccountsService) {} - - handle: HandlerType['handle'] = async ({ request }) => { - const [ids] = request.params ?? []; - - try { - await this.accountsService.deleteAccounts(ids); - } catch (e: any) { - return { - ...request, - error: e.toString(), - }; - } - - return { - ...request, - result: 'success', - }; - }; -} diff --git a/src/components/common/approval/ApprovalSection.tsx b/src/components/common/approval/ApprovalSection.tsx index 684b60c78..31d70db43 100644 --- a/src/components/common/approval/ApprovalSection.tsx +++ b/src/components/common/approval/ApprovalSection.tsx @@ -30,7 +30,9 @@ export const ApprovalSectionHeader: React.FC = ({ }} > - {label} + + {label} + {tooltip && ( {tooltipIcon} diff --git a/src/contexts/AccountsProvider.tsx b/src/contexts/AccountsProvider.tsx index e7d8bcd0f..2388a98d3 100644 --- a/src/contexts/AccountsProvider.tsx +++ b/src/contexts/AccountsProvider.tsx @@ -19,9 +19,9 @@ import { GetAccountsHandler } from '@src/background/services/accounts/handlers/g import { SelectAccountHandler } from '@src/background/services/accounts/handlers/selectAccount'; import { AvalancheRenameAccountHandler } from '@src/background/services/accounts/handlers/avalanche_renameAccount'; import { AddAccountHandler } from '@src/background/services/accounts/handlers/addAccount'; -import { DeleteAccountHandler } from '@src/background/services/accounts/handlers/deleteAccounts'; import getAllAddressesForAccount from '@src/utils/getAllAddressesForAccount'; import { DAppProviderRequest } from '@src/background/connections/dAppConnection/models'; +import { AvalancheDeleteAccountsHandler } from '@src/background/services/accounts/handlers/avalanche_deleteAccounts'; const AccountsContext = createContext<{ accounts: Accounts; @@ -125,8 +125,8 @@ export function AccountsContextProvider({ children }: { children: any }) { const deleteAccounts = useCallback( (ids: string[]) => - request({ - method: ExtensionRequest.ACCOUNT_DELETE, + request({ + method: DAppProviderRequest.ACCOUNTS_DELETE, params: [ids], }), [request], diff --git a/src/localization/locales/en/translation.json b/src/localization/locales/en/translation.json index 0403336c2..7d5845d9d 100644 --- a/src/localization/locales/en/translation.json +++ b/src/localization/locales/en/translation.json @@ -19,8 +19,10 @@ "Account Details": "Account Details", "Account not found": "Account not found", "Account renamed": "Account renamed", + "Account to be Removed": "Account to be Removed", "Account to rename": "Account to rename", "Account(s) removal has failed!": "Account(s) removal has failed!", + "Accounts to be Removed": "Accounts to be Removed", "Action Details": "Action Details", "Action was not approved. Please try again.": "Action was not approved. Please try again.", "Actions": "Actions", @@ -358,6 +360,9 @@ "Forum": "Forum", "French": "French", "From": "From", + "From Fireblocks": "From Fireblocks", + "From Private Key": "From Private Key", + "From Wallet Connect": "From Wallet Connect", "From {{start}} to {{end}}": "From {{start}} to {{end}}", "From:": "From:", "Gas Limit": "Gas Limit", @@ -685,10 +690,13 @@ "Reject Transaction": "Reject Transaction", "Remove": "Remove", "Remove Account": "Remove Account", + "Remove Account?": "Remove Account?", + "Remove Accounts?": "Remove Accounts?", "Remove Authenticator?": "Remove Authenticator?", "Remove Subnet Validator": "Remove Subnet Validator", "Remove This Method?": "Remove This Method?", "Removing the account will delete all local account information stored on this computer. Your assets on chain will remain on chain.": "Removing the account will delete all local account information stored on this computer. Your assets on chain will remain on chain.", + "Removing the account will delete all local account information stored on this computer. Your assets on chain will remain on chain.": "Removing the account will delete all local account information stored on this computer. Your assets on chain will remain on chain.", "Removing the accounts will delete all local accounts information stored on this computer. Your assets on chain will remain on chain.": "Removing the accounts will delete all local accounts information stored on this computer. Your assets on chain will remain on chain.", "Removing the last account is not possible.": "Removing the last account is not possible.", "Rename": "Rename", diff --git a/src/pages/Wallet/DeleteAccounts.tsx b/src/pages/Wallet/DeleteAccounts.tsx new file mode 100644 index 000000000..bca6c9cab --- /dev/null +++ b/src/pages/Wallet/DeleteAccounts.tsx @@ -0,0 +1,202 @@ +import { useTranslation } from 'react-i18next'; +import { + Button, + CircularProgress, + Stack, + Typography, +} from '@avalabs/core-k2-components'; +import { useApproveAction } from '@src/hooks/useApproveAction'; +import { ActionStatus } from '@src/background/services/actions/models'; +import { useGetRequestId } from '@src/hooks/useGetRequestId'; +import { + ApprovalSection, + ApprovalSectionBody, + ApprovalSectionHeader, +} from '@src/components/common/approval/ApprovalSection'; +import { WebsiteDetails } from '../SignTransaction/components/ApprovalTxDetails'; +import type { DomainMetadata } from '@src/background/models'; +import { truncateAddress } from '@src/utils/truncateAddress'; +import type { DeleteAccountsDisplayData } from '@src/background/services/accounts/handlers/avalanche_deleteAccounts'; +import { AccountType } from '@src/background/services/accounts/models'; + +export function DeleteAccount() { + const { t } = useTranslation(); + const requestId = useGetRequestId(); + + const { action, updateAction, cancelHandler } = useApproveAction<{ + accounts: DeleteAccountsDisplayData; + }>(requestId); + + if (!action) { + return ( + + + + ); + } + + const primaryAccountsCount = Object.values( + action.displayData.accounts.primary, + ).flat().length; + + const accountsCount = + action.displayData.accounts.imported.length + primaryAccountsCount; + + const site: DomainMetadata = action.site ?? { + domain: '#', + name: t('Unknown website'), + }; + + const getImportedAccountType = ( + type: + | AccountType.IMPORTED + | AccountType.WALLET_CONNECT + | AccountType.FIREBLOCKS, + ) => { + switch (type) { + case AccountType.IMPORTED: + return t('From Private Key'); + case AccountType.WALLET_CONNECT: + return t('From Wallet Connect'); + default: + return t('From Fireblocks'); + } + }; + + return ( + + + + {accountsCount === 1 ? t('Remove Account?') : t('Remove Accounts?')} + + + + + + + + + + + {Object.keys(action.displayData.accounts.primary) && + Object.entries(action.displayData.accounts.primary).map( + ([walletId, primaryAccounts], i) => ( + + {primaryAccounts.map((primaryAccount) => ( + + + + {primaryAccount.name} + + + {truncateAddress(primaryAccount.addressC)} + + + + {t('From')}{' '} + {action.displayData.accounts.wallet[walletId]} + + + ))} + + ), + )} + + {!!action.displayData.accounts.imported.length && ( + + {action.displayData.accounts.imported.map((importedAccount) => ( + + + + {importedAccount.name} + + + {truncateAddress(importedAccount.addressC)} + + + + {getImportedAccountType(importedAccount.type)} + + + ))} + + )} + + + + + + + {t( + 'Removing the account will delete all local account information stored on this computer. Your assets on chain will remain on chain.', + )} + + + + + + + + + ); +} diff --git a/src/popup/ApprovalRoutes.tsx b/src/popup/ApprovalRoutes.tsx index b036d127f..813a96b2a 100644 --- a/src/popup/ApprovalRoutes.tsx +++ b/src/popup/ApprovalRoutes.tsx @@ -100,6 +100,12 @@ const RenameAccount = lazy(() => { })); }); +const DeleteAccounts = lazy(() => { + return import('../pages/Wallet/DeleteAccounts').then((m) => ({ + default: m.DeleteAccount, + })); +}); + const GetAddressesInRange = lazy(() => { return import('../pages/Wallet/GetAddressesInRange').then((m) => ({ default: m.GetAddressesInRange, @@ -184,6 +190,9 @@ export const ApprovalRoutes = (props: SwitchProps) => ( + + +