Skip to content

Commit

Permalink
feat: (dApp Handler) CP-9471 expose delete accounts endpoint (#115)
Browse files Browse the repository at this point in the history
  • Loading branch information
frichards authored Dec 20, 2024
1 parent 9c6b938 commit dc57fe2
Show file tree
Hide file tree
Showing 14 changed files with 690 additions and 87 deletions.
1 change: 1 addition & 0 deletions src/background/connections/dAppConnection/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions src/background/connections/dAppConnection/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
1 change: 0 additions & 1 deletion src/background/connections/extensionConnection/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 0 additions & 2 deletions src/background/connections/extensionConnection/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
),
});
});
});
Loading

0 comments on commit dc57fe2

Please sign in to comment.