Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ import {
} from '../../../component-library/components/Buttons/Button/Button.types';
import { useSelector } from 'react-redux';
import { selectInternalAccounts } from '../../../selectors/accountsController';
import { KeyringTypes } from '@metamask/keyring-controller';
import Engine from '../../../core/Engine';
import { getUniqueAccountName } from '../../../core/SnapKeyring/utils/getUniqueAccountName';

const SnapAccountCustomNameApproval = () => {
Expand All @@ -54,9 +52,7 @@ const SnapAccountCustomNameApproval = () => {
approvalRequest?.requestData?.snapSuggestedAccountName;
const initialName = suggestedName
? getUniqueAccountName(internalAccounts, suggestedName)
: Engine.context.AccountsController.getNextAvailableAccountName(
KeyringTypes.snap,
);
: '';
setAccountName(initialName);
}, [approvalRequest, internalAccounts]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,12 @@ import {
SNAP_ACCOUNT_CUSTOM_NAME_ADD_ACCOUNT_BUTTON,
} from '../SnapAccountCustomNameApproval.constants';
import { ApprovalRequest } from '@metamask/approval-controller';
import { KeyringTypes } from '@metamask/keyring-controller';
import { SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES } from '../../../../core/RPCMethods/RPCMethodMiddleware';
import SnapAccountCustomNameApproval from '../SnapAccountCustomNameApproval';
import renderWithProvider, {
DeepPartial,
} from '../../../../util/test/renderWithProvider';
import useApprovalRequest from '../../../Views/confirmations/hooks/useApprovalRequest';
import Engine from '../../../../core/Engine';
import { RootState } from '../../../../reducers';
import {
MOCK_ACCOUNTS_CONTROLLER_STATE,
Expand Down Expand Up @@ -63,10 +61,6 @@ const mockApprovalRequest = (approvalRequest?: ApprovalRequest<any>) => {
};

describe('SnapAccountCustomNameApproval', () => {
const getNextAvailableAccountNameMock = (
Engine.context.AccountsController.getNextAvailableAccountName as jest.Mock
).mockImplementation(() => 'Snap Account 3');

beforeEach(() => {
jest.clearAllMocks();
});
Expand Down Expand Up @@ -173,19 +167,13 @@ describe('SnapAccountCustomNameApproval', () => {
};
mockApprovalRequest(mockApprovalRequestData);

getNextAvailableAccountNameMock.mockReturnValue('Snap Account 3');

const { getByTestId } = renderWithProvider(
<SnapAccountCustomNameApproval />,
{ state: initialState },
);

expect(getNextAvailableAccountNameMock).toHaveBeenCalledWith(
KeyringTypes.snap,
);

const input = getByTestId(SNAP_ACCOUNT_CUSTOM_NAME_INPUT);
expect(input.props.value).toBe('Snap Account 3');
expect(input.props.value).toBe('');
});

it('shows error message and disables "Add Account" button when name is taken', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -957,7 +957,7 @@ describe('AccountConnect', () => {
mockMultichainWalletSnapClient.createAccount,
).toHaveBeenCalledWith({
scope: SolScope.Mainnet,
accountNameSuggestion: 'Solana Account 1',
accountNameSuggestion: 'Solana Account ',
entropySource: mockKeyringId,
});
});
Expand Down
88 changes: 2 additions & 86 deletions app/components/Views/AddNewAccount/AddNewAccount.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,20 +174,6 @@ describe('AddNewAccount', () => {
jest.clearAllMocks();
});

it('shows next available account name as placeholder', () => {
const { getByPlaceholderText } = render(initialState, {});

expect(getByPlaceholderText(mockNextAccountName)).toBeDefined();
});

it('handles account name input', () => {
const { getByPlaceholderText } = render(initialState, {});

const input = getByPlaceholderText(mockNextAccountName);
fireEvent.changeText(input, 'My New Account');
expect(input.props.value).toBe('My New Account');
});

it('shows SRP list when selector is clicked', () => {
const { getByText } = render(initialState, {});

Expand Down Expand Up @@ -229,22 +215,7 @@ describe('AddNewAccount', () => {

expect(mockAddNewHdAccount).toHaveBeenCalledWith(
mockKeyring2.metadata.id,
mockNextAccountName,
);
});

it('handles account creation with custom name', async () => {
const { getByText, getByPlaceholderText } = render(initialState, {});

const input = getByPlaceholderText(mockNextAccountName);
fireEvent.changeText(input, 'My Custom Account');

const addButton = getByText(strings('accounts.add'));
fireEvent.press(addButton);

expect(mockAddNewHdAccount).toHaveBeenCalledWith(
mockKeyring2.metadata.id,
'My Custom Account',
'',
);
});

Expand Down Expand Up @@ -286,61 +257,6 @@ describe('AddNewAccount', () => {
});

describe('multichain', () => {
it.each([
{
scope: MultichainNetwork.BitcoinTestnet,
clientType: WalletClientType.Bitcoin,
expectedName: 'Bitcoin Testnet Account 1',
},
{
scope: MultichainNetwork.Bitcoin,
clientType: WalletClientType.Bitcoin,
expectedName: 'Bitcoin Account 1',
},
{
scope: MultichainNetwork.SolanaDevnet,
clientType: WalletClientType.Solana,
expectedName: 'Solana Devnet Account 1',
},
{
scope: MultichainNetwork.SolanaTestnet,
clientType: WalletClientType.Solana,
expectedName: 'Solana Testnet Account 1',
},
{
scope: MultichainNetwork.Solana,
clientType: WalletClientType.Solana,
expectedName: 'Solana Account 1',
},
{
scope: TrxScope.Mainnet,
clientType: WalletClientType.Tron,
expectedName: 'Tron Account 1',
},
{
scope: TrxScope.Nile,
clientType: WalletClientType.Tron,
expectedName: 'Tron Nile Account 1',
},
{
scope: TrxScope.Shasta,
clientType: WalletClientType.Tron,
expectedName: 'Tron Shasta Account 1',
},
])(
'suggested name is $expectedName for scope: $scope',
async ({ scope, clientType, expectedName }) => {
const { getByPlaceholderText } = render(initialState, {
scope,
clientType,
});

const namePlaceholder = getByPlaceholderText(expectedName);

expect(namePlaceholder).toBeDefined();
},
);

it('calls create account with the MultichainWalletSnapClient', async () => {
const { getByTestId } = render(initialState, {
scope: MultichainNetwork.Solana,
Expand All @@ -355,7 +271,7 @@ describe('AddNewAccount', () => {
mockMultichainWalletSnapClient.createAccount,
).toHaveBeenCalledWith({
scope: MultichainNetwork.Solana,
accountNameSuggestion: 'Solana Account 1',
accountNameSuggestion: 'Solana Account ',
entropySource: mockKeyring2.metadata.id,
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import renderWithProvider from '../../../../../util/test/renderWithProvider';
import DeviceSecurityToggle from './DeviceSecurityToggle';
import AUTHENTICATION_TYPE from '../../../../../constants/userProperties';
import { SecurityPrivacyViewSelectorsIDs } from '../SecurityPrivacyView.testIds';
import { AUTHENTICATION_APP_TRIGGERED_AUTH_NO_CREDENTIALS } from '../../../../../constants/error';
import Logger from '../../../../../util/Logger';
import { ReauthenticateErrorType } from '../../../../../core/Authentication/types';

const mockNavigate = jest.fn();
jest.mock('@react-navigation/native', () => {
Expand All @@ -28,13 +28,6 @@ jest.mock('../../../../../core/Authentication/AuthenticationError', () => {
return { __esModule: true, default: AuthenticationError };
});

const MockedAuthenticationError = jest.requireMock(
'../../../../../core/Authentication/AuthenticationError',
).default as new (
message: string,
code: string,
) => Error & { customErrorMessage: string };

const mockUpdateAuthPreference = jest.fn();
const mockGetAuthCapabilities = jest.fn();
const mockUpdateOsAuthEnabled = jest.fn();
Expand Down Expand Up @@ -275,10 +268,7 @@ describe('DeviceSecurityToggle', () => {
describe('password required', () => {
it('navigates to EnterPasswordSimple when updateAuthPreference throws password-required error', async () => {
mockUpdateAuthPreference.mockRejectedValueOnce(
new MockedAuthenticationError(
'Password required',
AUTHENTICATION_APP_TRIGGERED_AUTH_NO_CREDENTIALS,
),
new Error(ReauthenticateErrorType.PASSWORD_NOT_SET_WITH_BIOMETRICS),
);
const { getByTestId } = renderComponent();
const toggle = await waitFor(() =>
Expand All @@ -298,10 +288,7 @@ describe('DeviceSecurityToggle', () => {
let onPasswordSet: ((password: string) => Promise<void>) | undefined;
mockUpdateAuthPreference
.mockRejectedValueOnce(
new MockedAuthenticationError(
'Password required',
AUTHENTICATION_APP_TRIGGERED_AUTH_NO_CREDENTIALS,
),
new Error(ReauthenticateErrorType.PASSWORD_NOT_SET_WITH_BIOMETRICS),
)
.mockResolvedValueOnce(undefined);
mockNavigate.mockImplementation(
Expand Down Expand Up @@ -336,10 +323,7 @@ describe('DeviceSecurityToggle', () => {
it('clears optimistic state when user cancels password entry', async () => {
let onCancel: (() => void) | undefined;
mockUpdateAuthPreference.mockRejectedValueOnce(
new MockedAuthenticationError(
'Password required',
AUTHENTICATION_APP_TRIGGERED_AUTH_NO_CREDENTIALS,
),
new Error(ReauthenticateErrorType.PASSWORD_NOT_SET_WITH_BIOMETRICS),
);
mockNavigate.mockImplementation(
(_: string, params?: { onCancel?: () => void }) => {
Expand Down Expand Up @@ -372,10 +356,7 @@ describe('DeviceSecurityToggle', () => {
let onPasswordSet: ((password: string) => Promise<void>) | undefined;
mockUpdateAuthPreference
.mockRejectedValueOnce(
new MockedAuthenticationError(
'Password required',
AUTHENTICATION_APP_TRIGGERED_AUTH_NO_CREDENTIALS,
),
new Error(ReauthenticateErrorType.PASSWORD_NOT_SET_WITH_BIOMETRICS),
)
.mockRejectedValueOnce(updateError);
mockNavigate.mockImplementation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import {
} from '@metamask/design-system-react-native';
import { useNavigation } from '@react-navigation/native';
import Logger from '../../../../../util/Logger';
import AuthenticationError from '../../../../../core/Authentication/AuthenticationError';
import { AUTHENTICATION_APP_TRIGGERED_AUTH_NO_CREDENTIALS } from '../../../../../constants/error';
import { useStyles } from '../../../../hooks/useStyles';
import useAuthCapabilities from '../../../../../core/Authentication/hooks/useAuthCapabilities';
import { createTurnOffRememberMeModalNavDetails } from '../../../../UI/TurnOffRememberMeModal/TurnOffRememberMeModal';
import { useAuthentication } from '../../../../../core/Authentication';
import { Linking } from 'react-native';
import { strings } from '../../../../../../locales/i18n';
import { containsErrorMessage } from '../../../../../util/errorHandling';
import { ReauthenticateErrorType } from '../../../../../core/Authentication/types';

interface DeviceSecurityToggleProps {
/**
Expand Down Expand Up @@ -95,10 +95,10 @@ const DeviceSecurityToggle = ({
}, 100);
} catch (error) {
// Check if error is "password required" - navigate to password entry
const isPasswordRequiredError =
error instanceof AuthenticationError &&
error.customErrorMessage ===
AUTHENTICATION_APP_TRIGGERED_AUTH_NO_CREDENTIALS;
const isPasswordRequiredError = containsErrorMessage(
error as Error,
ReauthenticateErrorType.PASSWORD_NOT_SET_WITH_BIOMETRICS,
);

if (isPasswordRequiredError) {
// Navigate to password entry - keep optimistic value until callback completes
Expand Down
2 changes: 0 additions & 2 deletions app/constants/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ export const CONTACT_ALREADY_SAVED = 'contactAlreadySaved';
export const SYMBOL_ERROR = 'symbolError';

// Authentication errors
export const AUTHENTICATION_APP_TRIGGERED_AUTH_NO_CREDENTIALS =
'Password does not exist when calling SecureKeychain.getGenericPassword';
export const AUTHENTICATION_FAILED_WALLET_CREATION = 'Failed wallet creation';
export const AUTHENTICATION_RESET_PASSWORD_FAILED_MESSAGE =
'Authentication.resetPassword failed when calling SecureKeychain.resetGenericPassword with:';
Expand Down
18 changes: 18 additions & 0 deletions app/controllers/perps/services/HyperLiquidClientService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1692,5 +1692,23 @@ describe('HyperLiquidClientService', () => {
expect(error).toBeInstanceOf(Error);
expect(error.message).toContain('WebSocket transport ready timeout');
});

it('throws a proper Error (not undefined) when transport.ready() rejects with undefined', async () => {
await service.initialize(mockWallet);

// Simulate the HyperLiquid SDK rejecting with undefined (the root cause of Sentry issues
// 5E7M, 5EF8, 5GBE, 5G91: "Unknown error (no details provided)")
mockWsTransportReady.mockImplementationOnce(() =>
Promise.reject(undefined),
);

const error = await service.ensureTransportReady().catch((e) => e);

// Must be a real Error instance, not undefined
expect(error).toBeInstanceOf(Error);
expect(error.message).toContain(
'HyperLiquidClientService.ensureTransportReady',
);
});
});
});
10 changes: 8 additions & 2 deletions app/controllers/perps/services/HyperLiquidClientService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,13 @@ export class HyperLiquidClientService {
}

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
const timeoutId = setTimeout(
() =>
controller.abort(
new Error(`WebSocket transport ready timeout after ${timeoutMs}ms`),
),
timeoutMs,
);

try {
await subscriptionClient.config_.transport.ready(controller.signal);
Expand All @@ -409,7 +415,7 @@ export class HyperLiquidClientService {
`WebSocket transport ready timeout after ${timeoutMs}ms`,
);
}
throw error;
throw ensureError(error, 'HyperLiquidClientService.ensureTransportReady');
} finally {
clearTimeout(timeoutId);
}
Expand Down
Loading
Loading