Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add utility function to get supported chains from the Security Alerts API #25716

Merged
merged 11 commits into from
Jul 22, 2024
4 changes: 4 additions & 0 deletions app/scripts/lib/ppom/ppom-middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { createPPOMMiddleware } from './ppom-middleware';
import {
generateSecurityAlertId,
handlePPOMError,
isChainSupported,
validateRequestWithPPOM,
} from './ppom-util';
import { SecurityAlertResponse } from './types';
Expand Down Expand Up @@ -97,13 +98,15 @@ describe('PPOMMiddleware', () => {
const validateRequestWithPPOMMock = jest.mocked(validateRequestWithPPOM);
const generateSecurityAlertIdMock = jest.mocked(generateSecurityAlertId);
const handlePPOMErrorMock = jest.mocked(handlePPOMError);
const isChainSupportedMock = jest.mocked(isChainSupported);

beforeEach(() => {
jest.resetAllMocks();

validateRequestWithPPOMMock.mockResolvedValue(SECURITY_ALERT_RESPONSE_MOCK);
generateSecurityAlertIdMock.mockReturnValue(SECURITY_ALERT_ID_MOCK);
handlePPOMErrorMock.mockReturnValue(SECURITY_ALERT_RESPONSE_MOCK);
isChainSupportedMock.mockResolvedValue(true);
});

it('updates alert response after validating request', async () => {
Expand Down Expand Up @@ -174,6 +177,7 @@ describe('PPOMMiddleware', () => {
});

it('does not do validation if user is not on a supported network', async () => {
isChainSupportedMock.mockResolvedValue(false);
const middlewareFunction = createMiddleware({
chainId: '0x2',
});
Expand Down
9 changes: 4 additions & 5 deletions app/scripts/lib/ppom/ppom-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@ import { MESSAGE_TYPE } from '../../../../shared/constants/app';
import { SIGNING_METHODS } from '../../../../shared/constants/transaction';
import { PreferencesController } from '../../controllers/preferences';
import { AppStateController } from '../../controllers/app-state';
import {
LOADING_SECURITY_ALERT_RESPONSE,
SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS,
} from '../../../../shared/constants/security-provider';
import { LOADING_SECURITY_ALERT_RESPONSE } from '../../../../shared/constants/security-provider';
import {
generateSecurityAlertId,
handlePPOMError,
isChainSupported,
validateRequestWithPPOM,
} from './ppom-util';
import { SecurityAlertResponse } from './types';
Expand Down Expand Up @@ -72,11 +70,12 @@ export function createPPOMMiddleware<
preferencesController.store.getState()?.securityAlertsEnabled;

const { chainId } = networkController.state.providerConfig;
const isSupportedChain = await isChainSupported(chainId);

if (
!securityAlertsEnabled ||
!CONFIRMATION_METHODS.includes(req.method) ||
!SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS.includes(chainId)
!isSupportedChain
) {
return;
}
Expand Down
37 changes: 37 additions & 0 deletions app/scripts/lib/ppom/ppom-util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { AppStateController } from '../../controllers/app-state';
import {
generateSecurityAlertId,
isChainSupported,
updateSecurityAlertResponse,
validateRequestWithPPOM,
} from './ppom-util';
Expand Down Expand Up @@ -99,6 +100,10 @@ describe('PPOM Utils', () => {
const normalizeTransactionParamsMock = jest.mocked(
normalizeTransactionParams,
);
const getSupportedChainIdsMock = jest.spyOn(
securityAlertAPI,
'getSecurityAlertsAPISupportedChainIds',
);
let isSecurityAlertsEnabledMock: jest.SpyInstance;

beforeEach(() => {
Expand Down Expand Up @@ -351,4 +356,36 @@ describe('PPOM Utils', () => {
);
});
});

describe('isChainSupported', () => {
describe('when security alerts API is enabled', () => {
beforeEach(async () => {
isSecurityAlertsEnabledMock.mockReturnValue(true);
getSupportedChainIdsMock.mockResolvedValue([CHAIN_ID_MOCK]);
});

it('returns true if chain is supported', async () => {
expect(await isChainSupported(CHAIN_ID_MOCK)).toStrictEqual(true);
});

it('returns false if chain is not supported', async () => {
expect(await isChainSupported('0x2')).toStrictEqual(false);
});

it('returns correctly if security alerts API throws', async () => {
getSupportedChainIdsMock.mockRejectedValue(new Error('Test Error'));
expect(await isChainSupported(CHAIN_ID_MOCK)).toStrictEqual(true);
});
});

describe('when security alerts API is disabled', () => {
it('returns true if chain is supported', async () => {
expect(await isChainSupported(CHAIN_ID_MOCK)).toStrictEqual(true);
});

it('returns false if chain is not supported', async () => {
expect(await isChainSupported('0x2')).toStrictEqual(false);
});
});
});
});
18 changes: 18 additions & 0 deletions app/scripts/lib/ppom/ppom-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import { SignatureController } from '@metamask/signature-controller';
import {
BlockaidReason,
BlockaidResultType,
SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS,
SecurityAlertSource,
} from '../../../../shared/constants/security-provider';
import { SIGNING_METHODS } from '../../../../shared/constants/transaction';
import { AppStateController } from '../../controllers/app-state';
import { SecurityAlertResponse } from './types';
import {
getSecurityAlertsAPISupportedChainIds,
isSecurityAlertsAPIEnabled,
validateWithSecurityAlertsAPI,
} from './security-alerts-api';
Expand Down Expand Up @@ -111,6 +113,22 @@ export function handlePPOMError(
};
}

export async function isChainSupported(chainId: Hex): Promise<boolean> {
let supportedChainIds = SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS;

try {
if (isSecurityAlertsAPIEnabled()) {
supportedChainIds = await getSecurityAlertsAPISupportedChainIds();
}
} catch (error: unknown) {
handlePPOMError(
error,
`Error fetching supported chains from security alerts API`,
);
}
return supportedChainIds.includes(chainId);
}

function normalizePPOMRequest(request: JsonRpcRequest): JsonRpcRequest {
if (request.method !== METHOD_SEND_TRANSACTION) {
return request;
Expand Down
28 changes: 28 additions & 0 deletions app/scripts/lib/ppom/security-alerts-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
BlockaidResultType,
} from '../../../../shared/constants/security-provider';
import {
getSecurityAlertsAPISupportedChainIds,
isSecurityAlertsAPIEnabled,
validateWithSecurityAlertsAPI,
} from './security-alerts-api';
Expand Down Expand Up @@ -86,4 +87,31 @@ describe('Security Alerts API', () => {
expect(isEnabled).toBe(false);
});
});

describe('getSecurityAlertsAPISupportedChainIds', () => {
it('sends GET request', async () => {
const SUPPORTED_CHAIN_IDS_MOCK = ['0x1', '0x2'];
fetchMock.mockResolvedValue({
ok: true,
json: async () => SUPPORTED_CHAIN_IDS_MOCK,
});
const response = await getSecurityAlertsAPISupportedChainIds();

expect(response).toEqual(SUPPORTED_CHAIN_IDS_MOCK);

expect(fetchMock).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledWith(
`https://example.com/supportedChains`,
undefined,
);
});

it('throws an error if response is not ok', async () => {
fetchMock.mockResolvedValue({ ok: false, status: 404 });

await expect(getSecurityAlertsAPISupportedChainIds()).rejects.toThrow(
'Security alerts API request failed with status: 404',
);
});
});
});
22 changes: 14 additions & 8 deletions app/scripts/lib/ppom/security-alerts-api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Hex } from '@metamask/utils';
import { SecurityAlertResponse } from './types';

const ENDPOINT_VALIDATE = 'validate';
const ENDPOINT_SUPPORTED_CHAINS = 'supportedChains';

export type SecurityAlertsAPIRequest = {
method: string;
Expand All @@ -14,22 +16,26 @@ export function isSecurityAlertsAPIEnabled() {

export async function validateWithSecurityAlertsAPI(
chainId: string,
request: SecurityAlertsAPIRequest,
body: SecurityAlertsAPIRequest,
): Promise<SecurityAlertResponse> {
const endpoint = `${ENDPOINT_VALIDATE}/${chainId}`;
return postRequest(endpoint, request);
}

async function postRequest(endpoint: string, body: unknown) {
const url = getUrl(endpoint);

const response = await fetch(url, {
return request(endpoint, {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
},
});
}

export async function getSecurityAlertsAPISupportedChainIds(): Promise<Hex[]> {
return request(ENDPOINT_SUPPORTED_CHAINS);
}

async function request(endpoint: string, options?: RequestInit) {
const url = getUrl(endpoint);

const response = await fetch(url, options);

if (!response.ok) {
throw new Error(
Expand Down
5 changes: 5 additions & 0 deletions app/scripts/lib/transaction/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { UserOperationController } from '@metamask/user-operation-controller';
import { cloneDeep } from 'lodash';
import {
generateSecurityAlertId,
isChainSupported,
validateRequestWithPPOM,
} from '../ppom/ppom-util';
import {
Expand Down Expand Up @@ -100,6 +101,7 @@ describe('Transaction Utils', () => {
let userOperationController: jest.Mocked<UserOperationController>;
const validateRequestWithPPOMMock = jest.mocked(validateRequestWithPPOM);
const generateSecurityAlertIdMock = jest.mocked(generateSecurityAlertId);
const isChainSupportedMock = jest.mocked(isChainSupported);

beforeEach(() => {
jest.resetAllMocks();
Expand All @@ -124,6 +126,7 @@ describe('Transaction Utils', () => {
});

generateSecurityAlertIdMock.mockReturnValue(SECURITY_ALERT_ID_MOCK);
isChainSupportedMock.mockResolvedValue(true);

request.transactionController = transactionController;
request.userOperationController = userOperationController;
Expand Down Expand Up @@ -501,6 +504,8 @@ describe('Transaction Utils', () => {
});

it('unless chain is not supported', async () => {
isChainSupportedMock.mockResolvedValue(false);

await addTransaction({
...request,
securityAlertsEnabled: true,
Expand Down
11 changes: 7 additions & 4 deletions app/scripts/lib/transaction/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ import { PPOMController } from '@metamask/ppom-validator';
import {
generateSecurityAlertId,
handlePPOMError,
isChainSupported,
validateRequestWithPPOM,
} from '../ppom/ppom-util';
import { SecurityAlertResponse } from '../ppom/types';
import {
LOADING_SECURITY_ALERT_RESPONSE,
SECURITY_PROVIDER_EXCLUDED_TRANSACTION_TYPES,
SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS,
} from '../../../../shared/constants/security-provider';

export type AddTransactionOptions = NonNullable<
Expand Down Expand Up @@ -87,7 +87,7 @@ export async function addDappTransaction(
export async function addTransaction(
request: AddTransactionRequest,
): Promise<TransactionMeta> {
validateSecurity(request);
await validateSecurity(request);

const { transactionMeta, waitForHash } = await addTransactionOrUserOperation(
request,
Expand Down Expand Up @@ -216,7 +216,7 @@ function getTransactionByHash(
);
}

function validateSecurity(request: AddTransactionRequest) {
async function validateSecurity(request: AddTransactionRequest) {
const {
chainId,
ppomController,
Expand All @@ -229,14 +229,16 @@ function validateSecurity(request: AddTransactionRequest) {

const { type } = transactionOptions;

const isCurrentChainSupported = await isChainSupported(chainId);

const typeIsExcludedFromPPOM =
SECURITY_PROVIDER_EXCLUDED_TRANSACTION_TYPES.includes(
type as TransactionType,
);

if (
!securityAlertsEnabled ||
!SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS.includes(chainId) ||
!isCurrentChainSupported ||
typeIsExcludedFromPPOM
) {
return;
Expand Down Expand Up @@ -271,6 +273,7 @@ function validateSecurity(request: AddTransactionRequest) {

const securityAlertId = generateSecurityAlertId();

// Intentionally not awaited to avoid blocking the confirmation process while the validation occurs.
validateRequestWithPPOM({
ppomController,
request: ppomRequest,
Expand Down
10 changes: 0 additions & 10 deletions ui/selectors/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ import {
} from '../helpers/constants/survey';
import { PRIVACY_POLICY_DATE } from '../helpers/constants/privacy-policy';
import { ENVIRONMENT_TYPE_POPUP } from '../../shared/constants/app';
import { SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS } from '../../shared/constants/security-provider';
import { MultichainNativeAssets } from '../../shared/constants/multichain/assets';
import {
getAllUnapprovedTransactions,
Expand Down Expand Up @@ -2078,15 +2077,6 @@ export function getNetworkConfigurations(state) {
return state.metamask.networkConfigurations;
}

export function getIsNetworkSupportedByBlockaid(state) {
const currentChainId = getCurrentChainId(state);

const isSupported =
SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS.includes(currentChainId);

return isSupported;
}

export const getAllEnabledNetworks = createDeepEqualSelector(
getNonTestNetworks,
getAllNetworks,
Expand Down
34 changes: 0 additions & 34 deletions ui/selectors/selectors.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -823,40 +823,6 @@ describe('Selectors', () => {
});
});

describe('#getIsNetworkSupportedByBlockaid', () => {
it('returns true if current network is Linea', () => {
const modifiedMockState = {
...mockState,
metamask: {
...mockState.metamask,
providerConfig: {
...mockState.metamask.providerConfig,
chainId: CHAIN_IDS.LINEA_MAINNET,
},
},
};
const isSupported =
selectors.getIsNetworkSupportedByBlockaid(modifiedMockState);
expect(isSupported).toBe(true);
});

it('returns false if current network is Goerli', () => {
const modifiedMockState = {
...mockState,
metamask: {
...mockState.metamask,
providerConfig: {
...mockState.metamask.providerConfig,
chainId: CHAIN_IDS.GOERLI,
},
},
};
const isSupported =
selectors.getIsNetworkSupportedByBlockaid(modifiedMockState);
expect(isSupported).toBe(false);
});
});

describe('#getAllEnabledNetworks', () => {
it('returns only Mainnet and Linea with showTestNetworks off', () => {
const networks = selectors.getAllEnabledNetworks({
Expand Down