Skip to content

Commit

Permalink
feat: add utility function to get supported chains from the Security …
Browse files Browse the repository at this point in the history
…Alerts API (#10267)

<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**

<!--
Write a short description of the changes included in this pull request,
also include relevant motivation and context. Have in mind the following
questions:
1. What is the reason for the change?
2. What is the improvement/solution?
-->

This PR aims to include a new utility function to get supported chains
from the Security Alerts API when enabled.

#### Related Repository
Refer to the [Security Alerts API
repository](https://github.com/consensys-vertical-apps/va-mmcx-security-alerts-api)
for more details.

## **Related issues**

Fixes: MetaMask/mobile-planning#1870

## **Manual testing steps**

1. Test blockaid regression

2. add the envs 
```shell
export SECURITY_ALERTS_API_URL='https://security-alerts.dev-api.cx.metamask.io'
export SECURITY_ALERTS_API_ENABLED='true'
```
- Go to test dapp and trigger on of the malicious signatures
- To verify in chrome go to dev tools > network. Search for
`security-alerts` and find the call to the API service.

Existing PPOM logic should function as before, even with the above
environment variables added, due to the fallback to the controller in
the event of an error.

## **Screenshots/Recordings**


[Screencast from 19-07-2024
14:42:48.webm](https://github.com/user-attachments/assets/ba6c2f55-11e6-49f5-b9dc-120ed77390bd)


<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**

<!-- [screenshots/recordings] -->

## **Pre-merge author checklist**

- [x] I’ve followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
  • Loading branch information
vinistevam authored Aug 1, 2024
1 parent 3dadaf1 commit 5835f66
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 17 deletions.
31 changes: 25 additions & 6 deletions app/lib/ppom/ppom-util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ import * as TransactionActions from '../../actions/transaction'; // eslint-disab
import * as NetworkControllerSelectors from '../../selectors/networkController'; // eslint-disable-line import/no-namespace
import Engine from '../../core/Engine';
import PPOMUtil from './ppom-util';
import {
isSecurityAlertsAPIEnabled,
validateWithSecurityAlertsAPI,
} from './security-alerts-api';
// eslint-disable-next-line import/no-namespace
import * as securityAlertAPI from './security-alerts-api';

const CHAIN_ID_MOCK = '0x1';

Expand Down Expand Up @@ -90,10 +88,17 @@ const mockSignatureRequest = {

describe('PPOM Utils', () => {
const validateWithSecurityAlertsAPIMock = jest.mocked(
validateWithSecurityAlertsAPI,
securityAlertAPI.validateWithSecurityAlertsAPI,
);

const isSecurityAlertsEnabledMock = jest.mocked(isSecurityAlertsAPIEnabled);
const isSecurityAlertsEnabledMock = jest.mocked(
securityAlertAPI.isSecurityAlertsAPIEnabled,
);

const getSupportedChainIdsMock = jest.spyOn(
securityAlertAPI,
'getSecurityAlertsAPISupportedChainIds',
);

const normalizeTransactionParamsMock = jest.mocked(
normalizeTransactionParams,
Expand Down Expand Up @@ -275,6 +280,7 @@ describe('PPOM Utils', () => {

it('uses security alerts API if enabled', async () => {
isSecurityAlertsEnabledMock.mockReturnValue(true);
getSupportedChainIdsMock.mockResolvedValue([CHAIN_ID_MOCK]);

await PPOMUtil.validateRequest(mockRequest, CHAIN_ID_MOCK);

Expand All @@ -289,6 +295,7 @@ describe('PPOM Utils', () => {

it('uses controller if security alerts API throws', async () => {
isSecurityAlertsEnabledMock.mockReturnValue(true);
getSupportedChainIdsMock.mockResolvedValue([CHAIN_ID_MOCK]);

validateWithSecurityAlertsAPIMock.mockRejectedValue(
new Error('Test Error'),
Expand All @@ -306,5 +313,17 @@ describe('PPOM Utils', () => {
mockRequest,
);
});

it('validates correctly if security alerts API throws', async () => {
const spy = jest.spyOn(
TransactionActions,
'setTransactionSecurityAlertResponse',
);
jest
.spyOn(securityAlertAPI, 'getSecurityAlertsAPISupportedChainIds')
.mockRejectedValue(new Error('Test Error'));
await PPOMUtil.validateRequest(mockRequest, CHAIN_ID_MOCK);
expect(spy).toBeCalledTimes(2);
});
});
});
29 changes: 27 additions & 2 deletions app/lib/ppom/ppom-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ import {
import { WALLET_CONNECT_ORIGIN } from '../../util/walletconnect';
import AppConstants from '../../core/AppConstants';
import {
getSecurityAlertsAPISupportedChainIds,
isSecurityAlertsAPIEnabled,
validateWithSecurityAlertsAPI,
} from './security-alerts-api';
import { PPOMController } from '@metamask/ppom-validator';
import { Hex } from '@metamask/utils';
import { BLOCKAID_SUPPORTED_CHAIN_IDS } from '../../util/networks';

export interface PPOMRequest {
method: string;
Expand Down Expand Up @@ -64,8 +67,14 @@ async function validateRequest(req: PPOMRequest, transactionId?: string) {

const chainId = NetworkController.state.providerConfig.chainId;
const isConfirmationMethod = CONFIRMATION_METHODS.includes(req.method);

if (!ppomController || !isBlockaidFeatureEnabled() || !isConfirmationMethod) {
const isSupportedChain = await isChainSupported(chainId);

if (
!ppomController ||
!isBlockaidFeatureEnabled() ||
!isConfirmationMethod ||
!isSupportedChain
) {
return;
}

Expand Down Expand Up @@ -124,6 +133,22 @@ async function validateRequest(req: PPOMRequest, transactionId?: string) {
}
}

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

try {
if (isSecurityAlertsAPIEnabled()) {
supportedChainIds = await getSecurityAlertsAPISupportedChainIds();
}
} catch (e) {
Logger.log(
`Error fetching supported chains from security alerts API: ${e}`,
);
}

return supportedChainIds.includes(chainId);
}

async function validateWithController(
ppomController: PPOMController,
request: PPOMRequest,
Expand Down
32 changes: 31 additions & 1 deletion app/lib/ppom/security-alerts-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import {
Reason,
ResultType,
} from '../../components/Views/confirmations/components/BlockaidBanner/BlockaidBanner.types';
import { validateWithSecurityAlertsAPI } from './security-alerts-api';
import {
getSecurityAlertsAPISupportedChainIds,
validateWithSecurityAlertsAPI,
} from './security-alerts-api';

const CHAIN_ID_MOCK = '0x1';

Expand Down Expand Up @@ -66,4 +69,31 @@ describe('Security Alerts API', () => {
);
});
});

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/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 '../../components/Views/confirmations/components/BlockaidBanner/BlockaidBanner.types';

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

export interface SecurityAlertsAPIRequest {
method: string;
Expand All @@ -13,22 +15,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

0 comments on commit 5835f66

Please sign in to comment.