Skip to content

Commit 0364739

Browse files
authored
Stop blocking display of transaction approval window on resolution of ppom security alert response (#22778)
## **Description** Solves this problem: For people on slow networks, the amount of time it takes from "Dapp button click" -> "Popup notification window appears" might be a problem. When blockaid is toggled off, and the network is throttled to slow via the chrome dev tools, I see a ~3 second worse case for this time, but when blockaid is toggled on, I see a 12 second (or even a bit longer) worst case scenario for this time. The risk here is that users unexpectedly see a longer delay then usual, and so think they need to click the button again, and then get shown multiple confirmation requests and confirm all of them, leading to fund loss. This can happen on either slow internet connections, or if an infura request is taking longer than usual (which could effect people regardless of internet connection). Yesterday when I first saw this issue, I wasn't slowing down my connection, but it still took ~20 seconds to open. Probably a somewhat rare case (because as I mentioned I didn't notice any problems again until I intentionally throttled the network requests), but even if it only affected 10% of users only once a month, that would be enough to get plenty of "why did metamask just take 20 seconds to open a confirmation window?!?" complaints. <!-- 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? --> ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- 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** - [ ] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've clearly explained what problem this PR is solving and how it is solved. - [ ] I've linked related issues - [ ] I've included manual testing steps - [ ] I've included screenshots/recordings if applicable - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. - [ ] I’ve properly set the pull request status: - [ ] In case it's not yet "ready for review", I've set it to "draft". - [ ] In case it's "ready for review", I've changed it from "draft" to "non-draft". ## **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.
1 parent 8385506 commit 0364739

File tree

16 files changed

+497
-40
lines changed

16 files changed

+497
-40
lines changed

app/scripts/controllers/app-state.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export default class AppStateController extends EventEmitter {
6565
'0x539': true,
6666
},
6767
surveyLinkLastClickedOrClosed: null,
68+
signatureSecurityAlertResponses: {},
6869
});
6970
this.timer = null;
7071

@@ -441,8 +442,19 @@ export default class AppStateController extends EventEmitter {
441442
},
442443
});
443444
}
444-
445445
///: END:ONLY_INCLUDE_IF
446+
447+
addSignatureSecurityAlertResponse(securityAlertResponse) {
448+
const currentState = this.store.getState();
449+
const { signatureSecurityAlertResponses } = currentState;
450+
this.store.updateState({
451+
signatureSecurityAlertResponses: {
452+
...signatureSecurityAlertResponses,
453+
[securityAlertResponse.securityAlertId]: securityAlertResponse,
454+
},
455+
});
456+
}
457+
446458
/**
447459
* A setter for the currentPopupId which indicates the id of popup window that's currently active
448460
*

app/scripts/lib/ppom/ppom-middleware.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ describe('PPOMMiddleware', () => {
9191
const usePPOM = async () => {
9292
throw new Error('some error');
9393
};
94-
const middlewareFunction = createMiddleWare(usePPOM);
94+
const middlewareFunction = createMiddleWare({ usePPOM });
9595
const req = {
9696
method: 'eth_sendTransaction',
9797
securityAlertResponse: undefined,

app/scripts/lib/ppom/ppom-middleware.ts

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,23 @@
11
import { PPOM } from '@blockaid/ppom_release';
22
import { PPOMController } from '@metamask/ppom-validator';
33
import { NetworkController } from '@metamask/network-controller';
4+
import { v4 as uuid } from 'uuid';
45

56
import {
67
BlockaidReason,
78
BlockaidResultType,
89
} from '../../../../shared/constants/security-provider';
910
import { CHAIN_IDS } from '../../../../shared/constants/network';
11+
import { SIGNING_METHODS } from '../../../../shared/constants/transaction';
1012
import { PreferencesController } from '../../controllers/preferences';
13+
import { SecurityAlertResponse } from '../transaction/util';
1114

1215
const { sentry } = global as any;
1316

14-
const ConfirmationMethods = Object.freeze([
17+
const CONFIRMATION_METHODS = Object.freeze([
1518
'eth_sendRawTransaction',
1619
'eth_sendTransaction',
17-
'eth_sign',
18-
'eth_signTypedData',
19-
'eth_signTypedData_v1',
20-
'eth_signTypedData_v3',
21-
'eth_signTypedData_v4',
22-
'personal_sign',
20+
...SIGNING_METHODS,
2321
]);
2422

2523
export const SUPPORTED_CHAIN_IDS: string[] = [
@@ -44,12 +42,19 @@ export const SUPPORTED_CHAIN_IDS: string[] = [
4442
* @param ppomController - Instance of PPOMController.
4543
* @param preferencesController - Instance of PreferenceController.
4644
* @param networkController - Instance of NetworkController.
45+
* @param appStateController
46+
* @param updateSecurityAlertResponseByTxId
4747
* @returns PPOMMiddleware function.
4848
*/
4949
export function createPPOMMiddleware(
5050
ppomController: PPOMController,
5151
preferencesController: PreferencesController,
5252
networkController: NetworkController,
53+
appStateController: any,
54+
updateSecurityAlertResponseByTxId: (
55+
req: any,
56+
securityAlertResponse: SecurityAlertResponse,
57+
) => void,
5358
) {
5459
return async (req: any, _res: any, next: () => void) => {
5560
try {
@@ -58,15 +63,46 @@ export function createPPOMMiddleware(
5863
const { chainId } = networkController.state.providerConfig;
5964
if (
6065
securityAlertsEnabled &&
61-
ConfirmationMethods.includes(req.method) &&
66+
CONFIRMATION_METHODS.includes(req.method) &&
6267
SUPPORTED_CHAIN_IDS.includes(chainId)
6368
) {
6469
// eslint-disable-next-line require-atomic-updates
65-
req.securityAlertResponse = await ppomController.usePPOM(
66-
async (ppom: PPOM) => {
67-
return ppom.validateJsonRpc(req);
68-
},
69-
);
70+
const securityAlertId = uuid();
71+
72+
ppomController.usePPOM(async (ppom: PPOM) => {
73+
try {
74+
const securityAlertResponse = await ppom.validateJsonRpc(req);
75+
securityAlertResponse.securityAlertId = securityAlertId;
76+
updateSecurityAlertResponseByTxId(req, securityAlertResponse);
77+
} catch (error: any) {
78+
sentry?.captureException(error);
79+
console.error('Error validating JSON RPC using PPOM: ', error);
80+
const securityAlertResponse = {
81+
result_type: BlockaidResultType.Failed,
82+
reason: BlockaidReason.failed,
83+
description:
84+
'Validating the confirmation failed by throwing error.',
85+
};
86+
updateSecurityAlertResponseByTxId(req, securityAlertResponse);
87+
}
88+
});
89+
90+
if (SIGNING_METHODS.includes(req.method)) {
91+
req.securityAlertResponse = {
92+
securityAlertId,
93+
};
94+
appStateController.addSignatureSecurityAlertResponse({
95+
reason: BlockaidResultType.Loading,
96+
result_type: BlockaidReason.inProgress,
97+
securityAlertId,
98+
});
99+
} else {
100+
req.securityAlertResponse = {
101+
reason: BlockaidResultType.Loading,
102+
result_type: BlockaidReason.inProgress,
103+
securityAlertId,
104+
};
105+
}
70106
}
71107
} catch (error: any) {
72108
sentry?.captureException(error);

app/scripts/lib/setupSentry.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ export const SENTRY_BACKGROUND_STATE = {
211211
selectedAddress: false,
212212
snapRegistryList: false,
213213
theme: true,
214+
signatureSecurityAlertResponses: false,
214215
transactionSecurityCheckEnabled: true,
215216
use4ByteResolution: true,
216217
useAddressBarEnsResolution: true,

app/scripts/lib/transaction/util.test.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ import {
1616
addTransaction,
1717
} from './util';
1818

19+
jest.mock('uuid', () => {
20+
const actual = jest.requireActual('uuid');
21+
22+
return {
23+
...actual,
24+
v4: jest.fn(),
25+
};
26+
});
27+
1928
const TRANSACTION_PARAMS_MOCK: TransactionParams = {
2029
from: '0x1',
2130
};
@@ -380,8 +389,8 @@ describe('Transaction Utils', () => {
380389
).toHaveBeenCalledWith(TRANSACTION_PARAMS_MOCK, {
381390
...TRANSACTION_OPTIONS_MOCK,
382391
securityAlertResponse: {
383-
reason: 'testReason',
384-
result_type: 'testResultType',
392+
reason: 'loading',
393+
result_type: 'validation_in_progress',
385394
},
386395
});
387396

app/scripts/lib/transaction/util.ts

Lines changed: 75 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import {
33
TransactionController,
44
TransactionMeta,
55
TransactionParams,
6+
WalletDevice,
7+
TransactionType,
8+
SendFlowHistoryEntry,
9+
Result,
610
} from '@metamask/transaction-controller';
711
import {
812
AddUserOperationOptions,
@@ -12,11 +16,45 @@ import {
1216
import { PPOMController } from '@metamask/ppom-validator';
1317
import { captureException } from '@sentry/browser';
1418
import { addHexPrefix } from 'ethereumjs-util';
19+
import { v4 as uuid } from 'uuid';
1520
import { SUPPORTED_CHAIN_IDS } from '../ppom/ppom-middleware';
21+
import {
22+
BlockaidReason,
23+
BlockaidResultType,
24+
} from '../../../../shared/constants/security-provider';
1625
///: END:ONLY_INCLUDE_IF
1726

27+
/**
28+
* Type for security alert response from transaction validator.
29+
*/
30+
export type SecurityAlertResponse = {
31+
reason: string;
32+
features?: string[];
33+
result_type: string;
34+
providerRequestsCount?: Record<string, number>;
35+
securityAlertId?: string;
36+
};
37+
1838
export type AddTransactionOptions = NonNullable<
19-
Parameters<TransactionController['addTransaction']>[1]
39+
Parameters<
40+
(
41+
txParams: TransactionParams,
42+
options?: {
43+
actionId?: string;
44+
deviceConfirmedOn?: WalletDevice;
45+
method?: string;
46+
origin?: string;
47+
requireApproval?: boolean | undefined;
48+
securityAlertResponse?: SecurityAlertResponse;
49+
sendFlowHistory?: SendFlowHistoryEntry[];
50+
swaps?: {
51+
hasApproveTx?: boolean;
52+
meta?: Partial<TransactionMeta>;
53+
};
54+
type?: TransactionType;
55+
},
56+
) => Promise<Result>
57+
>[1]
2058
>;
2159

2260
type BaseAddTransactionRequest = {
@@ -30,16 +68,6 @@ type BaseAddTransactionRequest = {
3068
userOperationController: UserOperationController;
3169
};
3270

33-
/**
34-
* Type for security alert response from transaction validator.
35-
*/
36-
export type SecurityAlertResponse = {
37-
reason: string;
38-
features?: string[];
39-
result_type: string;
40-
providerRequestsCount?: Record<string, number>;
41-
};
42-
4371
type FinalAddTransactionRequest = BaseAddTransactionRequest & {
4472
transactionOptions: AddTransactionOptions;
4573
};
@@ -83,6 +111,12 @@ export async function addDappTransaction(
83111

84112
export async function addTransaction(
85113
request: AddTransactionRequest,
114+
///: BEGIN:ONLY_INCLUDE_IF(blockaid)
115+
updateSecurityAlertResponseByTxId: (
116+
req: AddTransactionOptions | undefined,
117+
securityAlertResponse: SecurityAlertResponse,
118+
) => void,
119+
///: END:ONLY_INCLUDE_IF
86120
): Promise<TransactionMeta> {
87121
///: BEGIN:ONLY_INCLUDE_IF(blockaid)
88122
const {
@@ -109,13 +143,36 @@ export async function addTransaction(
109143
],
110144
};
111145

112-
const securityAlertResponse = await ppomController.usePPOM(
113-
async (ppom) => {
114-
return ppom.validateJsonRpc(ppomRequest);
115-
},
116-
);
117-
118-
request.transactionOptions.securityAlertResponse = securityAlertResponse;
146+
const securityAlertId = uuid();
147+
148+
ppomController.usePPOM(async (ppom) => {
149+
try {
150+
const securityAlertResponse = await ppom.validateJsonRpc(ppomRequest);
151+
updateSecurityAlertResponseByTxId(
152+
request.transactionOptions,
153+
securityAlertResponse,
154+
);
155+
} catch (e) {
156+
captureException(e);
157+
console.error('Error validating JSON RPC using PPOM: ', e);
158+
const securityAlertResponse = {
159+
result_type: BlockaidResultType.Failed,
160+
reason: BlockaidReason.failed,
161+
description:
162+
'Validating the confirmation failed by throwing error.',
163+
};
164+
updateSecurityAlertResponseByTxId(
165+
request.transactionOptions,
166+
securityAlertResponse,
167+
);
168+
}
169+
});
170+
171+
request.transactionOptions.securityAlertResponse = {
172+
reason: BlockaidResultType.Loading,
173+
result_type: BlockaidReason.inProgress,
174+
securityAlertId,
175+
};
119176
} catch (e) {
120177
captureException(e);
121178
}

app/scripts/metamask-controller.js

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,11 @@ import { BrowserRuntimePostMessageStream } from '@metamask/post-message-stream';
143143
import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils';
144144
///: END:ONLY_INCLUDE_IF
145145

146-
import { AssetType, TokenStandard } from '../../shared/constants/transaction';
146+
import {
147+
AssetType,
148+
TokenStandard,
149+
SIGNING_METHODS,
150+
} from '../../shared/constants/transaction';
147151
import {
148152
GAS_API_BASE_URL,
149153
GAS_DEV_API_BASE_URL,
@@ -2986,6 +2990,7 @@ export default class MetamaskController extends EventEmitter {
29862990
transactionOptions,
29872991
waitForSubmit: false,
29882992
}),
2993+
this.updateSecurityAlertResponseByTxId.bind(this),
29892994
),
29902995
addTransactionAndWaitForPublish: (
29912996
transactionParams,
@@ -2997,6 +3002,7 @@ export default class MetamaskController extends EventEmitter {
29973002
transactionOptions,
29983003
waitForSubmit: true,
29993004
}),
3005+
this.updateSecurityAlertResponseByTxId.bind(this),
30003006
),
30013007
createTransactionEventFragment:
30023008
createTransactionEventFragmentWithTxId.bind(
@@ -4251,6 +4257,42 @@ export default class MetamaskController extends EventEmitter {
42514257
}
42524258
};
42534259

4260+
async updateSecurityAlertResponseByTxId(req, securityAlertResponse) {
4261+
let foundConfirmation = false;
4262+
4263+
while (!foundConfirmation) {
4264+
if (SIGNING_METHODS.includes(req.method)) {
4265+
foundConfirmation = Object.values(
4266+
this.signatureController.messages,
4267+
).find(
4268+
(message) =>
4269+
message.securityAlertResponse?.securityAlertId ===
4270+
req.securityAlertResponse.securityAlertId,
4271+
);
4272+
} else {
4273+
foundConfirmation = this.txController.state.transactions.find(
4274+
(meta) =>
4275+
meta.securityAlertResponse?.securityAlertId ===
4276+
req.securityAlertResponse.securityAlertId,
4277+
);
4278+
}
4279+
if (!foundConfirmation) {
4280+
await new Promise((resolve) => setTimeout(resolve, 100));
4281+
}
4282+
}
4283+
4284+
if (SIGNING_METHODS.includes(req.method)) {
4285+
this.appStateController.addSignatureSecurityAlertResponse(
4286+
securityAlertResponse,
4287+
);
4288+
} else {
4289+
this.txController.updateSecurityAlertResponse(
4290+
foundConfirmation.id,
4291+
securityAlertResponse,
4292+
);
4293+
}
4294+
}
4295+
42544296
//=============================================================================
42554297
// PASSWORD MANAGEMENT
42564298
//=============================================================================
@@ -4643,6 +4685,8 @@ export default class MetamaskController extends EventEmitter {
46434685
this.ppomController,
46444686
this.preferencesController,
46454687
this.networkController,
4688+
this.appStateController,
4689+
this.updateSecurityAlertResponseByTxId.bind(this),
46464690
),
46474691
);
46484692
///: END:ONLY_INCLUDE_IF

app/scripts/metamask-controller.test.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,6 +1084,9 @@ describe('MetaMaskController', () => {
10841084

10851085
beforeEach(() => {
10861086
initializeMockMiddlewareLog();
1087+
metamaskController.preferencesController.setSecurityAlertsEnabled(
1088+
false,
1089+
);
10871090
});
10881091

10891092
afterAll(() => {

0 commit comments

Comments
 (0)