diff --git a/CHANGELOG.md b/CHANGELOG.md
index aca89dc0deeb..4a7eab9804be 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [10.27.0]
+### Added
+- feat: add the ConsenSys zkEVM (Linea) as a default network ([#17875](https://github.com/MetaMask/metamask-extension/pull/17875))
+
## [10.26.2]
### Changed
- Sign in with Ethereum: re-enable warning UI for mismatched domains / disable domain binding ([#18200](https://github.com/MetaMask/metamask-extension/pull/18200))
@@ -3536,7 +3540,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Uncategorized
- Added the ability to restore accounts from seed words.
-[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.26.2...HEAD
+[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.27.0...HEAD
+[10.27.0]: https://github.com/MetaMask/metamask-extension/compare/v10.26.2...v10.27.0
[10.26.2]: https://github.com/MetaMask/metamask-extension/compare/v10.26.1...v10.26.2
[10.26.1]: https://github.com/MetaMask/metamask-extension/compare/v10.26.0...v10.26.1
[10.26.0]: https://github.com/MetaMask/metamask-extension/compare/v10.25.0...v10.26.0
diff --git a/README.md b/README.md
index 48dbe9f3f65b..bf2671159717 100644
--- a/README.md
+++ b/README.md
@@ -142,6 +142,7 @@ Whenever you change dependencies (adding, removing, or updating, either in `pack
- [How to use the TREZOR emulator](./docs/trezor-emulator.md)
- [Developing on MetaMask](./development/README.md)
- [How to generate a visualization of this repository's development](./development/gource-viz.sh)
+- [How to add new confirmations](./docs/confirmations.md)
## Dapp Developer Resources
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index cf2ebbab5aee..17a25f726c74 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -150,6 +150,9 @@
"accountSelectionRequired": {
"message": "You need to select an account!"
},
+ "activated": {
+ "message": "Active"
+ },
"active": {
"message": "Active"
},
@@ -639,9 +642,39 @@
"close": {
"message": "Close"
},
+ "codefiCompliance": {
+ "message": "Codefi Compliance"
+ },
"coingecko": {
"message": "CoinGecko"
},
+ "complianceBlurb0": {
+ "message": "DeFi raises AML/CFT risk for institutions, given the decentralised pools and pseudonymous counterparties."
+ },
+ "complianceBlurb1": {
+ "message": "Codefi Compliance is the only product capable of running AML/CFT analysis on DeFi pools. This allows you to identify and avoid pools and counterparties that fail your risk setting."
+ },
+ "complianceBlurbStep1": {
+ "message": "Sign up to Codefi Compliance below"
+ },
+ "complianceBlurbStep2": {
+ "message": "Create an organisation"
+ },
+ "complianceBlurbStep3": {
+ "message": "Create a project"
+ },
+ "complianceBlurbStep4": {
+ "message": "Set your compliance settings"
+ },
+ "complianceBlurbStep5": {
+ "message": "Click the \"Enable Compliance in MMI\" button"
+ },
+ "complianceBlurpStep0": {
+ "message": "Steps to enable AML/CFT Compliance:"
+ },
+ "complianceSettingsExplanation": {
+ "message": "Change your settings or view reports by opening up Codefi Compliance or disconnect below."
+ },
"confirm": {
"message": "Confirm"
},
@@ -1332,6 +1365,10 @@
"ethGasPriceFetchWarning": {
"message": "Backup gas price is provided as the main gas estimation service is unavailable right now."
},
+ "ethereumProviderAccess": {
+ "message": "Grant Ethereum provider access to $1",
+ "description": "The parameter is the name of the requesting origin"
+ },
"ethereumPublicAddress": {
"message": "Ethereum public address"
},
@@ -2676,6 +2713,9 @@
"onlyConnectTrust": {
"message": "Only connect with sites you trust."
},
+ "openCodefiCompliance": {
+ "message": "Open Codefi Compliance"
+ },
"openFullScreenForLedgerWebHid": {
"message": "Open MetaMask in full screen to connect your ledger via WebHID.",
"description": "Shown to the user on the confirm screen when they are viewing MetaMask in a popup window but need to connect their ledger via webhid."
@@ -3536,7 +3576,11 @@
"message": "Proceed with caution"
},
"snapInstallWarningKeyAccess": {
- "message": "Grant $2 key access to $1",
+ "message": "Grant $2 account control to $1",
+ "description": "The first parameter is the name of the snap and the second one is the protocol"
+ },
+ "snapInstallWarningPublicKeyAccess": {
+ "message": "Grant $2 public key access to $1",
"description": "The first parameter is the name of the snap and the second one is the protocol"
},
"snapResultError": {
diff --git a/app/manifest/v2/_base.json b/app/manifest/v2/_base.json
index 39b289197617..f962de618d51 100644
--- a/app/manifest/v2/_base.json
+++ b/app/manifest/v2/_base.json
@@ -60,5 +60,19 @@
},
"manifest_version": 2,
"name": "__MSG_appName__",
+ "permissions": [
+ "storage",
+ "unlimitedStorage",
+ "clipboardWrite",
+ "http://localhost:8545/",
+ "https://*.infura.io/",
+ "https://*.codefi.network/",
+ "https://chainid.network/chains.json",
+ "https://lattice.gridplus.io/*",
+ "activeTab",
+ "webRequest",
+ "*://*.eth/",
+ "notifications"
+ ],
"short_name": "__MSG_appName__"
}
diff --git a/app/manifest/v2/chrome.json b/app/manifest/v2/chrome.json
index 9c0e95ec5d6f..a152130d89b2 100644
--- a/app/manifest/v2/chrome.json
+++ b/app/manifest/v2/chrome.json
@@ -4,19 +4,5 @@
"matches": ["https://metamask.io/*"],
"ids": ["*"]
},
- "minimum_chrome_version": "80",
- "permissions": [
- "storage",
- "unlimitedStorage",
- "clipboardWrite",
- "http://localhost:8545/",
- "https://*.infura.io/",
- "https://*.codefi.network/",
- "https://chainid.network/chains.json",
- "https://lattice.gridplus.io/*",
- "activeTab",
- "webRequest",
- "*://*.eth/",
- "notifications"
- ]
+ "minimum_chrome_version": "80"
}
diff --git a/app/manifest/v2/firefox.json b/app/manifest/v2/firefox.json
index 5adf0471356d..d50b26a27ff8 100644
--- a/app/manifest/v2/firefox.json
+++ b/app/manifest/v2/firefox.json
@@ -4,20 +4,5 @@
"id": "webextension@metamask.io",
"strict_min_version": "78.0"
}
- },
- "permissions": [
- "storage",
- "unlimitedStorage",
- "clipboardWrite",
- "http://localhost:8545/",
- "https://*.infura.io/",
- "https://*.codefi.network/",
- "https://chainid.network/chains.json",
- "https://lattice.gridplus.io/*",
- "activeTab",
- "tabs",
- "webRequest",
- "*://*.eth/",
- "notifications"
- ]
+ }
}
diff --git a/app/manifest/v3/_base.json b/app/manifest/v3/_base.json
index 3beeb73790d4..1b9456fd8d93 100644
--- a/app/manifest/v3/_base.json
+++ b/app/manifest/v3/_base.json
@@ -65,5 +65,15 @@
},
"manifest_version": 3,
"name": "__MSG_appName__",
+ "permissions": [
+ "activeTab",
+ "alarms",
+ "clipboardWrite",
+ "notifications",
+ "scripting",
+ "storage",
+ "unlimitedStorage",
+ "webRequest"
+ ],
"short_name": "__MSG_appName__"
}
diff --git a/app/manifest/v3/chrome.json b/app/manifest/v3/chrome.json
index dbb0ee22cca8..486692539eb4 100644
--- a/app/manifest/v3/chrome.json
+++ b/app/manifest/v3/chrome.json
@@ -6,19 +6,5 @@
"matches": ["https://metamask.io/*"],
"ids": ["*"]
},
- "minimum_chrome_version": "80",
- "permissions": [
- "storage",
- "unlimitedStorage",
- "clipboardWrite",
- "http://localhost:8545/",
- "https://*.infura.io/",
- "https://*.codefi.network/",
- "https://chainid.network/chains.json",
- "https://lattice.gridplus.io/*",
- "activeTab",
- "webRequest",
- "*://*.eth/",
- "notifications"
- ]
+ "minimum_chrome_version": "80"
}
diff --git a/app/manifest/v3/firefox.json b/app/manifest/v3/firefox.json
index 67ecf7b09891..5f0e5672fdbe 100644
--- a/app/manifest/v3/firefox.json
+++ b/app/manifest/v3/firefox.json
@@ -22,20 +22,5 @@
"default_title": "MetaMask",
"default_popup": "popup.html"
},
- "manifest_version": 2,
- "permissions": [
- "storage",
- "unlimitedStorage",
- "clipboardWrite",
- "http://localhost:8545/",
- "https://*.infura.io/",
- "https://*.codefi.network/",
- "https://chainid.network/chains.json",
- "https://lattice.gridplus.io/*",
- "tabs",
- "activeTab",
- "webRequest",
- "*://*.eth/",
- "notifications"
- ]
+ "manifest_version": 2
}
diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.js
index 8a5186d42148..cec5b3308515 100644
--- a/app/scripts/lib/createRPCMethodTrackingMiddleware.js
+++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.js
@@ -1,12 +1,12 @@
import { errorCodes } from 'eth-rpc-errors';
import { MESSAGE_TYPE, ORIGIN_METAMASK } from '../../../shared/constants/app';
+import { TransactionStatus } from '../../../shared/constants/transaction';
import { SECOND } from '../../../shared/constants/time';
import { detectSIWE } from '../../../shared/modules/siwe';
import {
EVENT,
EVENT_NAMES,
- METAMETRIC_KEY_OPTIONS,
- METAMETRIC_KEY,
+ METAMETRIC_KEY_OPT,
} from '../../../shared/constants/metametrics';
/**
@@ -41,7 +41,7 @@ const RATE_LIMIT_MAP = {
/**
* For events with user interaction (approve / reject | cancel) this map will
- * return an object with APPROVED, REJECTED and REQUESTED keys that map to the
+ * return an object with APPROVED, REJECTED, REQUESTED, and FAILED keys that map to the
* appropriate event names.
*/
const EVENT_NAME_MAP = {
@@ -142,6 +142,8 @@ export default function createRPCMethodTrackingMiddleware({
// keys for the various events in the flow.
const eventType = EVENT_NAME_MAP[method];
+ const eventProperties = {};
+
// Boolean variable that reduces code duplication and increases legibility
const shouldTrackEvent =
// Don't track if the request came from our own UI or background
@@ -162,27 +164,21 @@ export default function createRPCMethodTrackingMiddleware({
? eventType.REQUESTED
: EVENT_NAMES.PROVIDER_METHOD_CALLED;
- const properties = {};
-
- let msgParams;
-
if (event === EVENT_NAMES.SIGNATURE_REQUESTED) {
- properties.signature_type = method;
+ eventProperties.signature_type = method;
const data = req?.params?.[0];
const from = req?.params?.[1];
const paramsExamplePassword = req?.params?.[2];
- msgParams = {
- ...paramsExamplePassword,
- from,
- data,
- origin,
- };
-
const msgData = {
- msgParams,
- status: 'unapproved',
+ msgParams: {
+ ...paramsExamplePassword,
+ from,
+ data,
+ origin,
+ },
+ status: TransactionStatus.unapproved,
type: req.method,
};
@@ -193,25 +189,21 @@ export default function createRPCMethodTrackingMiddleware({
);
if (securityProviderResponse?.flagAsDangerous === 1) {
- properties.ui_customizations = ['flagged_as_malicious'];
+ eventProperties.ui_customizations = [
+ METAMETRIC_KEY_OPT.ui_customizations.flaggedAsMalicious,
+ ];
} else if (securityProviderResponse?.flagAsDangerous === 2) {
- properties.ui_customizations = ['flagged_as_safety_unknown'];
- } else {
- properties.ui_customizations = null;
+ eventProperties.ui_customizations = [
+ METAMETRIC_KEY_OPT.ui_customizations.flaggedAsSafetyUnknown,
+ ];
}
if (method === MESSAGE_TYPE.PERSONAL_SIGN) {
const { isSIWEMessage } = detectSIWE({ data });
if (isSIWEMessage) {
- properties.ui_customizations === null
- ? (properties.ui_customizations = [
- METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS]
- .SIWE,
- ])
- : properties.ui_customizations.push(
- METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS]
- .SIWE,
- );
+ eventProperties.ui_customizations = (
+ eventProperties.ui_customizations || []
+ ).concat(METAMETRIC_KEY_OPT.ui_customizations.SIWE);
}
}
} catch (e) {
@@ -220,7 +212,7 @@ export default function createRPCMethodTrackingMiddleware({
);
}
} else {
- properties.method = method;
+ eventProperties.method = method;
}
trackEvent({
@@ -229,7 +221,7 @@ export default function createRPCMethodTrackingMiddleware({
referrer: {
url: origin,
},
- properties,
+ properties: eventProperties,
});
rateLimitTimeouts[method] = setTimeout(() => {
@@ -242,8 +234,6 @@ export default function createRPCMethodTrackingMiddleware({
return callback();
}
- const properties = {};
-
// The rpc error methodNotFound implies that 'eth_sign' is disabled in Advanced Settings
const isDisabledEthSignAdvancedSetting =
method === MESSAGE_TYPE.ETH_SIGN &&
@@ -254,79 +244,20 @@ export default function createRPCMethodTrackingMiddleware({
let event;
if (isDisabledRPCMethod) {
event = eventType.FAILED;
- properties.error = res.error;
- } else if (res.error?.code === 4001) {
+ eventProperties.error = res.error;
+ } else if (res.error?.code === errorCodes.provider.userRejectedRequest) {
event = eventType.REJECTED;
} else {
event = eventType.APPROVED;
}
- let msgParams;
-
- if (eventType.REQUESTED === EVENT_NAMES.SIGNATURE_REQUESTED) {
- properties.signature_type = method;
-
- const data = req?.params?.[0];
- const from = req?.params?.[1];
- const paramsExamplePassword = req?.params?.[2];
-
- msgParams = {
- ...paramsExamplePassword,
- from,
- data,
- origin,
- };
-
- const msgData = {
- msgParams,
- status: 'unapproved',
- type: req.method,
- };
-
- try {
- const securityProviderResponse = await securityProviderRequest(
- msgData,
- req.method,
- );
-
- if (securityProviderResponse?.flagAsDangerous === 1) {
- properties.ui_customizations = ['flagged_as_malicious'];
- } else if (securityProviderResponse?.flagAsDangerous === 2) {
- properties.ui_customizations = ['flagged_as_safety_unknown'];
- } else {
- properties.ui_customizations = null;
- }
-
- if (method === MESSAGE_TYPE.PERSONAL_SIGN) {
- const { isSIWEMessage } = detectSIWE({ data });
- if (isSIWEMessage) {
- properties.ui_customizations === null
- ? (properties.ui_customizations = [
- METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS]
- .SIWE,
- ])
- : properties.ui_customizations.push(
- METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS]
- .SIWE,
- );
- }
- }
- } catch (e) {
- console.warn(
- `createRPCMethodTrackingMiddleware: Error calling securityProviderRequest - ${e}`,
- );
- }
- } else {
- properties.method = method;
- }
-
trackEvent({
event,
category: EVENT.CATEGORIES.INPAGE_PROVIDER,
referrer: {
url: origin,
},
- properties,
+ properties: eventProperties,
});
return callback();
diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js
index af910279e859..5be404f43fdd 100644
--- a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js
+++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js
@@ -1,7 +1,11 @@
import { errorCodes } from 'eth-rpc-errors';
import { MESSAGE_TYPE } from '../../../shared/constants/app';
-import { EVENT_NAMES } from '../../../shared/constants/metametrics';
+import {
+ EVENT_NAMES,
+ METAMETRIC_KEY_OPT,
+} from '../../../shared/constants/metametrics';
import { SECOND } from '../../../shared/constants/time';
+import { detectSIWE } from '../../../shared/modules/siwe';
import createRPCMethodTrackingMiddleware from './createRPCMethodTrackingMiddleware';
const trackEvent = jest.fn();
@@ -52,6 +56,12 @@ function getNext(timeout = 500) {
const waitForSeconds = async (seconds) =>
await new Promise((resolve) => setTimeout(resolve, SECOND * seconds));
+jest.mock('../../../shared/modules/siwe', () => ({
+ detectSIWE: jest.fn().mockImplementation(() => {
+ return { isSIWEMessage: false };
+ }),
+}));
+
describe('createRPCMethodTrackingMiddleware', () => {
afterEach(() => {
jest.resetAllMocks();
@@ -153,7 +163,7 @@ describe('createRPCMethodTrackingMiddleware', () => {
};
const res = {
- error: { code: 4001 },
+ error: { code: errorCodes.provider.userRejectedRequest },
};
const { next, executeMiddlewareStack } = getNext();
await handler(req, res, next);
@@ -230,6 +240,36 @@ describe('createRPCMethodTrackingMiddleware', () => {
expect(trackEvent.mock.calls[1][0].properties.method).toBe('eth_chainId');
});
+ it('should track Sign-in With Ethereum (SIWE) message if detected', async () => {
+ const req = {
+ method: MESSAGE_TYPE.PERSONAL_SIGN,
+ origin: 'some.dapp',
+ };
+ const res = {
+ error: null,
+ };
+ const { next, executeMiddlewareStack } = getNext();
+
+ detectSIWE.mockImplementation(() => {
+ return { isSIWEMessage: true };
+ });
+
+ await handler(req, res, next);
+ await executeMiddlewareStack();
+
+ expect(trackEvent).toHaveBeenCalledTimes(2);
+
+ expect(trackEvent.mock.calls[1][0]).toMatchObject({
+ category: 'inpage_provider',
+ event: EVENT_NAMES.SIGNATURE_APPROVED,
+ properties: {
+ signature_type: MESSAGE_TYPE.PERSONAL_SIGN,
+ ui_customizations: [METAMETRIC_KEY_OPT.ui_customizations.SIWE],
+ },
+ referrer: { url: 'some.dapp' },
+ });
+ });
+
describe(`when '${MESSAGE_TYPE.ETH_SIGN}' is disabled in advanced settings`, () => {
it(`should track ${EVENT_NAMES.SIGNATURE_FAILED} and include error property`, async () => {
const mockError = { code: errorCodes.rpc.methodNotFound };
@@ -258,93 +298,89 @@ describe('createRPCMethodTrackingMiddleware', () => {
});
});
});
- });
- describe('participateInMetaMetrics is set to true with a request flagged as safe', () => {
- beforeEach(() => {
- metricsState.participateInMetaMetrics = true;
- });
+ describe('when request is flagged as safe by security provider', () => {
+ it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event`, async () => {
+ const req = {
+ method: MESSAGE_TYPE.ETH_SIGN,
+ origin: 'some.dapp',
+ };
+ const res = {
+ error: null,
+ };
+ const { next } = getNext();
- it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event which is flagged as safe`, async () => {
- const req = {
- method: MESSAGE_TYPE.ETH_SIGN,
- origin: 'some.dapp',
- };
+ await handler(req, res, next);
- const res = {
- error: null,
- };
- const { next } = getNext();
- await handler(req, res, next);
- expect(trackEvent).toHaveBeenCalledTimes(1);
- expect(trackEvent.mock.calls[0][0]).toMatchObject({
- category: 'inpage_provider',
- event: EVENT_NAMES.SIGNATURE_REQUESTED,
- properties: {
- signature_type: MESSAGE_TYPE.ETH_SIGN,
- ui_customizations: null,
- },
- referrer: { url: 'some.dapp' },
+ expect(trackEvent).toHaveBeenCalledTimes(1);
+ expect(trackEvent.mock.calls[0][0]).toMatchObject({
+ category: 'inpage_provider',
+ event: EVENT_NAMES.SIGNATURE_REQUESTED,
+ properties: {
+ signature_type: MESSAGE_TYPE.ETH_SIGN,
+ },
+ referrer: { url: 'some.dapp' },
+ });
});
});
- });
- describe('participateInMetaMetrics is set to true with a request flagged as malicious', () => {
- beforeEach(() => {
- metricsState.participateInMetaMetrics = true;
- flagAsDangerous = 1;
- });
+ describe('when request is flagged as malicious by security provider', () => {
+ beforeEach(() => {
+ flagAsDangerous = 1;
+ });
- it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event which is flagged as malicious`, async () => {
- const req = {
- method: MESSAGE_TYPE.ETH_SIGN,
- origin: 'some.dapp',
- };
+ it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event which is flagged as malicious`, async () => {
+ const req = {
+ method: MESSAGE_TYPE.ETH_SIGN,
+ origin: 'some.dapp',
+ };
+ const res = {
+ error: null,
+ };
+ const { next } = getNext();
- const res = {
- error: null,
- };
- const { next } = getNext();
- await handler(req, res, next);
- expect(trackEvent).toHaveBeenCalledTimes(1);
- expect(trackEvent.mock.calls[0][0]).toMatchObject({
- category: 'inpage_provider',
- event: EVENT_NAMES.SIGNATURE_REQUESTED,
- properties: {
- signature_type: MESSAGE_TYPE.ETH_SIGN,
- ui_customizations: ['flagged_as_malicious'],
- },
- referrer: { url: 'some.dapp' },
+ await handler(req, res, next);
+
+ expect(trackEvent).toHaveBeenCalledTimes(1);
+ expect(trackEvent.mock.calls[0][0]).toMatchObject({
+ category: 'inpage_provider',
+ event: EVENT_NAMES.SIGNATURE_REQUESTED,
+ properties: {
+ signature_type: MESSAGE_TYPE.ETH_SIGN,
+ ui_customizations: ['flagged_as_malicious'],
+ },
+ referrer: { url: 'some.dapp' },
+ });
});
});
- });
- describe('participateInMetaMetrics is set to true with a request flagged as safety unknown', () => {
- beforeEach(() => {
- metricsState.participateInMetaMetrics = true;
- flagAsDangerous = 2;
- });
+ describe('when request flagged as safety unknown by security provider', () => {
+ beforeEach(() => {
+ flagAsDangerous = 2;
+ });
- it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event which is flagged as safety unknown`, async () => {
- const req = {
- method: MESSAGE_TYPE.ETH_SIGN,
- origin: 'some.dapp',
- };
+ it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event which is flagged as safety unknown`, async () => {
+ const req = {
+ method: MESSAGE_TYPE.ETH_SIGN,
+ origin: 'some.dapp',
+ };
+ const res = {
+ error: null,
+ };
+ const { next } = getNext();
- const res = {
- error: null,
- };
- const { next } = getNext();
- await handler(req, res, next);
- expect(trackEvent).toHaveBeenCalledTimes(1);
- expect(trackEvent.mock.calls[0][0]).toMatchObject({
- category: 'inpage_provider',
- event: EVENT_NAMES.SIGNATURE_REQUESTED,
- properties: {
- signature_type: MESSAGE_TYPE.ETH_SIGN,
- ui_customizations: ['flagged_as_safety_unknown'],
- },
- referrer: { url: 'some.dapp' },
+ await handler(req, res, next);
+
+ expect(trackEvent).toHaveBeenCalledTimes(1);
+ expect(trackEvent.mock.calls[0][0]).toMatchObject({
+ category: 'inpage_provider',
+ event: EVENT_NAMES.SIGNATURE_REQUESTED,
+ properties: {
+ signature_type: MESSAGE_TYPE.ETH_SIGN,
+ ui_customizations: ['flagged_as_safety_unknown'],
+ },
+ referrer: { url: 'some.dapp' },
+ });
});
});
});
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index b4c2ef4ba55e..2d210f2a15b7 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -826,10 +826,12 @@ export default class MetamaskController extends EventEmitter {
const originMetadata = subjectMetadataState.subjectMetadata[origin];
- this.platform._showNotification(
- originMetadata?.name ?? origin,
- message,
- );
+ this.platform
+ ._showNotification(originMetadata?.name ?? origin, message)
+ .catch((error) => {
+ log.error('Failed to create notification', error);
+ });
+
return null;
},
showInAppNotification: (origin, message) => {
@@ -969,7 +971,12 @@ export default class MetamaskController extends EventEmitter {
);
rpcPrefs = matchingNetworkConfig?.rpcPrefs ?? {};
}
- this.platform.showTransactionNotification(txMeta, rpcPrefs);
+
+ try {
+ await this.platform.showTransactionNotification(txMeta, rpcPrefs);
+ } catch (error) {
+ log.error('Failed to create transaction notification', error);
+ }
const { txReceipt } = txMeta;
diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js
index 3a298e699c41..098c69efdbd9 100644
--- a/app/scripts/platforms/extension.js
+++ b/app/scripts/platforms/extension.js
@@ -113,19 +113,19 @@ export default class ExtensionPlatform {
}
}
- showTransactionNotification(txMeta, rpcPrefs) {
+ async showTransactionNotification(txMeta, rpcPrefs) {
const { status, txReceipt: { status: receiptStatus } = {} } = txMeta;
if (status === TransactionStatus.confirmed) {
// There was an on-chain failure
receiptStatus === '0x0'
- ? this._showFailedTransaction(
+ ? await this._showFailedTransaction(
txMeta,
'Transaction encountered an error.',
)
- : this._showConfirmedTransaction(txMeta, rpcPrefs);
+ : await this._showConfirmedTransaction(txMeta, rpcPrefs);
} else if (status === TransactionStatus.failed) {
- this._showFailedTransaction(txMeta);
+ await this._showFailedTransaction(txMeta);
}
}
@@ -157,7 +157,7 @@ export default class ExtensionPlatform {
await browser.tabs.remove(tabId);
}
- _showConfirmedTransaction(txMeta, rpcPrefs) {
+ async _showConfirmedTransaction(txMeta, rpcPrefs) {
this._subscribeToNotificationClicked();
const url = getBlockExplorerLink(txMeta, rpcPrefs);
@@ -170,10 +170,10 @@ export default class ExtensionPlatform {
const message = `Transaction ${nonce} confirmed! ${
url.length ? `View on ${view}` : ''
}`;
- this._showNotification(title, message, url);
+ await this._showNotification(title, message, url);
}
- _showFailedTransaction(txMeta, errorMessage) {
+ async _showFailedTransaction(txMeta, errorMessage) {
const nonce = parseInt(txMeta.txParams.nonce, 16);
const title = 'Failed transaction';
let message = `Transaction ${nonce} failed! ${
@@ -184,12 +184,13 @@ export default class ExtensionPlatform {
message = `Transaction failed! ${errorMessage || txMeta.err.message}`;
}
///: END:ONLY_INCLUDE_IN
- this._showNotification(title, message);
+ await this._showNotification(title, message);
}
async _showNotification(title, message, url) {
const iconUrl = await browser.runtime.getURL('../../images/icon-64.png');
- browser.notifications.create(url, {
+
+ await browser.notifications.create(url, {
type: 'basic',
title,
iconUrl,
diff --git a/coverage-targets.js b/coverage-targets.js
index e221e9466c31..6157a706d7be 100644
--- a/coverage-targets.js
+++ b/coverage-targets.js
@@ -6,10 +6,10 @@
// subset of files to check against these targets.
module.exports = {
global: {
- lines: 65,
+ lines: 65.5,
branches: 53.5,
- statements: 64,
- functions: 57.4,
+ statements: 64.75,
+ functions: 58,
},
transforms: {
branches: 100,
diff --git a/development/build/index.js b/development/build/index.js
index 456128c1fa49..9c9f75f16ce8 100755
--- a/development/build/index.js
+++ b/development/build/index.js
@@ -102,6 +102,7 @@ async function defineAndRunBuildTasks() {
'navigator',
'harden',
'console',
+ 'Image', // Used by browser to generate notifications
// globals chrome driver needs to function (test env)
/cdc_[a-zA-Z0-9]+_[a-zA-Z]+/iu,
'performance',
diff --git a/docs/assets/confirmation.png b/docs/assets/confirmation.png
new file mode 100644
index 000000000000..d4b58a2f68db
Binary files /dev/null and b/docs/assets/confirmation.png differ
diff --git a/docs/confirmations.md b/docs/confirmations.md
new file mode 100644
index 000000000000..2f3aef515a39
--- /dev/null
+++ b/docs/confirmations.md
@@ -0,0 +1,253 @@
+# Adding New Confirmations
+
+## Overview
+
+Given the security focused nature of self-custody, confirmations and approvals form a pivotal aspect of the MetaMask extension.
+
+Confirmations can be triggered by dApps and the UI itself, and are used to approve a variety of operations such as:
+
+- Connecting to dApps
+- Giving permissions to dApps
+- Sending Eth
+- Transfering tokens
+- Signing data
+- Interacting with Snaps
+- Adding Ethereum networks
+
+It is vital any new confirmations are implemented using best practices and consistent patterns, to avoid adding complexity to the code, and to minimise the maintenance cost of many alternate confirmations.
+
+As we try to maintain a clean boundary between the UI and background page, the effort to implement a new confirmation can also be split accordingly.
+
+## Background
+
+### 1. Create Messenger
+
+Ensure the controller or logic requiring a confirmation has access to a controller messenger.
+
+Provide a `messenger` argument in the constructor if using a controller.
+
+Ensure the allowed actions include at least the `ApprovalController:addRequest` action.
+
+If the controller extends `BaseControllerV2`, the property `messagingSystem` is available to access the messenger passed to the base controller. Otherwise, ensure the provided messenger is assigned to a private property.
+
+#### Example
+
+```
+this.someController = new SomeController({
+ messenger: this.controllerMessenger.getRestricted({
+ name: 'SomeController',
+ allowedActions: [
+ `${this.approvalController.name}:addRequest`,
+ `${this.approvalController.name}:acceptRequest`,
+ `${this.approvalController.name}:rejectRequest`,
+ ],
+ }),
+ ...
+});
+```
+
+### 2. Create Approval Request
+
+Send an `addRequest` message to the `ApprovalController` to create an approval request.
+
+This message returns a `Promise` which will resolve if the confirmation is approved, and reject if the confirmation is denied or cancelled.
+
+Use an `async` function to send the message so the logic can `await` the confirmation and code execution can continue once approved. This enables the logic ran after approval to be kept in the same flow and therefore the logic to remain readable and encapsulated.
+
+Ensure suitable error handling is in place to handle the confirmation being cancelled or denied and therefore the `Promise` being rejected.
+
+The available message arguments are:
+
+| Name | Description | Example Value |
+| -- | -- | -- |
+| opts.id | The ID of the approval request.
Assigned to a random value if not provided. | `"f81f5c8a-33bb-4f31-a4e2-52f8b94c393b"` |
+| opts.origin | The origin of the request.
Either the dApp host or "metamask" if internal. | `"metamask.github.io"` |
+| opts.type | An arbitrary string identifying the type of request. | `"eth_signTypedData"` |
+| opts.requestData | Additional fixed data for the request.
Must be a JSON compatible object.| `{ transactionId: '123' }` |
+| opts.requestState | Additional mutable data for the request.
Must be a JSON compatible object.
Can be updated using the `ApprovalController.updateRequestState` action. | `{ status: 'pending' }` |
+| shouldShowRequest | A boolean indicating whether the popup should be displayed. | `true` |
+
+#### Example
+
+```
+await this.messagingSystem.call(
+ 'ApprovalController:addRequest',
+ {
+ id,
+ origin,
+ type,
+ requestData,
+ },
+ true,
+);
+```
+
+### 3. Update Approval Request
+
+If you wish to provide additional state to the confirmation while it is visible, send an `updateRequestState` message to the `ApprovalController`.
+
+This requires you to have provided the `id` when creating the approval request, so it can be passed to the update message.
+
+The available message arguments are:
+
+| Name | Description | Example Value |
+| -- | -- | -- |
+| opts.id | The ID of the approval request to update. | `"f81f5c8a-33bb-4f31-a4e2-52f8b94c393b"` |
+| opts.requestState | The updated mutable data for the request.
Must be a JSON compatible object. | `{ status: 'pending' }` |
+
+#### Example
+
+```
+await this.messagingSystem.call(
+ 'ApprovalController:updateRequestState',
+ {
+ id,
+ requestState: { counter },
+ },
+);
+```
+
+## Frontend
+
+### 1. Create Template File
+
+The `ConfirmationPage` component is already configured to display any approval requests generated by the `ApprovalController` and the associated `pendingApprovals` state.
+
+In order to configure how the resulting confirmation is rendered, an **Approval Template** is required.
+
+Create a new JavaScript file in `ui/pages/confirmation/templates` with the name matching the `type` used in the background approval request.
+
+### 2. Update Approval Templates
+
+Add your imported file to the `APPROVAL_TEMPLATES` constant in:
+[ui/pages/confirmation/templates/index.js](../ui/pages/confirmation/templates/index.js)
+
+### 3. Define Values
+
+Inside the template file, define a `getValues` function that returns an object with the following properties:
+
+| Name | Description | Example Value |
+| -- | -- | -- |
+| content | An array of objects defining the components to be rendered in the confirmation.
Processed by the [MetaMaskTemplateRenderer](../ui/components/app/metamask-template-renderer/metamask-template-renderer.js). | See example below. |
+| onSubmit | A callback to execute when the user approves the confirmation. | `actions.resolvePendingApproval(...)` |
+| onCancel | A callback to execute when the user rejects the confirmation. | `actions.rejectPendingApproval(...)` |
+| submitText | Text shown for the accept button. | `t('approveButtonText')` |
+| cancelText | Text shown on the reject button. | `t('cancel')` |
+| loadingText | Text shown while waiting for the onSubmit callback to complete. | `t('addingCustomNetwork')` |
+| networkDisplay | A boolean indicating whether to show the current network at the top of the confirmation. | `true` |
+
+#### Example
+
+```
+function getValues(pendingApproval, t, actions, _history) {
+ return {
+ content: [
+ {
+ element: 'Typography',
+ key: 'title',
+ children: 'Example',
+ props: {
+ variant: TypographyVariant.H3,
+ align: 'center',
+ fontWeight: 'bold',
+ boxProps: {
+ margin: [0, 0, 4],
+ },
+ },
+ },
+ ...
+ ],
+ cancelText: t('cancel'),
+ submitText: t('approveButtonText'),
+ loadingText: t('addingCustomNetwork'),
+ onSubmit: () =>
+ actions.resolvePendingApproval(
+ pendingApproval.id,
+ pendingApproval.requestData,
+ ),
+ onCancel: () =>
+ actions.rejectPendingApproval(
+ pendingApproval.id,
+ ethErrors.provider.userRejectedRequest().serialize(),
+ ),
+ networkDisplay: true,
+ };
+}
+```
+
+### 4. Define Alerts
+
+If any alerts are required in the confirmation, define the `getAlerts` function in the template file.
+
+This needs to return an array of any required alerts, based on the current pending approval.
+
+Each alert is an object with the following properties:
+
+| Name | Description | Example Value |
+| -- | -- | -- |
+| id | A unique string to identify the alert. | `"MISMATCHED_NETWORK_RPC"` |
+| severity | The severity of the alert.
Use the constants from the design system. | `SEVERITIES.DANGER` |
+| content | The component to be rendered inside the alert.
Uses the same format as the `content` returned from `getValues`.
The component can have nested components via the `children` property. | See example below. |
+
+#### Example
+
+```
+function getAlerts(_pendingApproval) {
+ return [
+ {
+ id: 'EXAMPLE_ALERT',
+ severity: SEVERITIES.WARNING,
+ content: {
+ element: 'span',
+ children: {
+ element: 'MetaMaskTranslation',
+ props: {
+ translationKey: 'exampleMessage',
+ },
+ },
+ },
+ },
+ ];
+}
+```
+
+### 5. Export Functions
+
+Ensure the `getValues` and `getAlerts` functions are exported from the template file.
+
+#### Example
+
+```
+const example = {
+ getAlerts,
+ getValues,
+};
+
+export default example;
+```
+
+## Example Branch
+
+See [this branch](https://github.com/MetaMask/metamask-extension/compare/develop...example/confirmation) as an example of the full code needed to add a confirmation.
+
+The confirmation can be tested using the [E2E Test dApp](https://metamask.github.io/test-dapp/) and selecting `Request Permissions`.
+
+## Glossary
+
+### ApprovalController
+
+The [ApprovalController](https://github.com/MetaMask/core/blob/main/packages/approval-controller/src/ApprovalController.ts) is a controller defined in the core repository which is responsible for creating and tracking approvals and confirmations in both the MetaMask extension and MetaMask mobile.
+
+The `pendingApprovals` state used by the `ApprovalController` is not currently persisted, meaning any confirmations created by it will not persist after restarting the browser for example.
+
+### ConfirmationPage
+
+The [ConfirmationPage](../ui/pages/confirmation/confirmation.js) is a React component that aims to provide a generic confirmation window which can be configured using templates, each implementing a consistent interface.
+
+This avoids the need for additional React components when creating confirmations, as additional templates with less logic, and less duplication, can be created instead.
+
+## Screenshots
+
+### Confirmation Window
+
+[](assets/confirmation.png)
diff --git a/package.json b/package.json
index e76f1c517b2b..ec932113371b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "metamask-crx",
- "version": "10.26.2",
+ "version": "10.27.0",
"private": true,
"repository": {
"type": "git",
diff --git a/shared/constants/metametrics.js b/shared/constants/metametrics.js
index a9691d5a29a2..de3244c42af2 100644
--- a/shared/constants/metametrics.js
+++ b/shared/constants/metametrics.js
@@ -462,17 +462,19 @@ export const CONTEXT_PROPS = {
};
/**
- * These types correspond to the keys in the METAMETRIC_KEY_OPTIONS object
+ * These types correspond to the keys in the METAMETRIC_KEY_OPT object
*/
export const METAMETRIC_KEY = {
UI_CUSTOMIZATIONS: `ui_customizations`,
};
/**
- * This object maps a method name to a METAMETRIC_KEY
+ * This object maps a METAMETRIC_KEY to an object of possible options
*/
-export const METAMETRIC_KEY_OPTIONS = {
+export const METAMETRIC_KEY_OPT = {
[METAMETRIC_KEY.UI_CUSTOMIZATIONS]: {
+ flaggedAsMalicious: 'flagged_as_malicious',
+ flaggedAsSafetyUnknown: 'flagged_as_safety_unknown',
SIWE: 'sign_in_with_ethereum',
},
};
diff --git a/test/e2e/snaps/test-snap-bip-32.spec.js b/test/e2e/snaps/test-snap-bip-32.spec.js
index 2a5174498b1d..2babba866499 100644
--- a/test/e2e/snaps/test-snap-bip-32.spec.js
+++ b/test/e2e/snaps/test-snap-bip-32.spec.js
@@ -63,7 +63,10 @@ describe('Test Snap bip-32', function () {
// wait for permissions popover, click checkboxes and confirm
await driver.delay(1000);
await driver.clickElement('#key-access-bip32-m-44h-0h-secp256k1-0');
- await driver.clickElement('#key-access-bip32-m-44h-0h-ed25519-0');
+ await driver.clickElement('#key-access-bip32-m-44h-0h-ed25519-1');
+ await driver.clickElement(
+ '#public-key-access-bip32-m-44h-0h-secp256k1-0',
+ );
await driver.clickElement({
text: 'Confirm',
tag: 'button',
diff --git a/test/e2e/snaps/test-snap-rpc.spec.js b/test/e2e/snaps/test-snap-rpc.spec.js
index 0f512c6fedc1..3c596ba08a98 100644
--- a/test/e2e/snaps/test-snap-rpc.spec.js
+++ b/test/e2e/snaps/test-snap-rpc.spec.js
@@ -64,7 +64,10 @@ describe('Test Snap RPC', function () {
// wait for permissions popover, click checkboxes and confirm
await driver.delay(1000);
await driver.clickElement('#key-access-bip32-m-44h-0h-secp256k1-0');
- await driver.clickElement('#key-access-bip32-m-44h-0h-ed25519-0');
+ await driver.clickElement('#key-access-bip32-m-44h-0h-ed25519-1');
+ await driver.clickElement(
+ '#public-key-access-bip32-m-44h-0h-secp256k1-0',
+ );
await driver.clickElement({
text: 'Confirm',
tag: 'button',
diff --git a/test/e2e/snaps/test-snap-update.spec.js b/test/e2e/snaps/test-snap-update.spec.js
index 523356bac7a5..eaf0b847c8f5 100644
--- a/test/e2e/snaps/test-snap-update.spec.js
+++ b/test/e2e/snaps/test-snap-update.spec.js
@@ -64,7 +64,10 @@ describe('Test Snap update', function () {
// wait for permissions popover, click checkboxes and confirm
await driver.delay(1000);
await driver.clickElement('#key-access-bip32-m-44h-0h-secp256k1-0');
- await driver.clickElement('#key-access-bip32-m-44h-0h-ed25519-0');
+ await driver.clickElement('#key-access-bip32-m-44h-0h-ed25519-1');
+ await driver.clickElement(
+ '#public-key-access-bip32-m-44h-0h-secp256k1-0',
+ );
await driver.clickElement({
text: 'Confirm',
tag: 'button',
diff --git a/ui/components/app/connected-accounts-permissions/connected-accounts-permissions.js b/ui/components/app/connected-accounts-permissions/connected-accounts-permissions.js
index 641dc6e70ff8..10f032424c86 100644
--- a/ui/components/app/connected-accounts-permissions/connected-accounts-permissions.js
+++ b/ui/components/app/connected-accounts-permissions/connected-accounts-permissions.js
@@ -20,7 +20,11 @@ const ConnectedAccountsPermissions = ({ permissions }) => {
const permissionLabels = flatten(
permissions.map(({ key, value }) =>
- getPermissionDescription(t, key, value),
+ getPermissionDescription({
+ t,
+ permissionName: key,
+ permissionValue: value,
+ }),
),
);
diff --git a/ui/components/app/flask/snap-install-warning/index.scss b/ui/components/app/flask/snap-install-warning/index.scss
index bc7d078bdb89..ddbf9c8d4a5b 100644
--- a/ui/components/app/flask/snap-install-warning/index.scss
+++ b/ui/components/app/flask/snap-install-warning/index.scss
@@ -1,4 +1,8 @@
.snap-install-warning {
+ .popover-header {
+ padding-bottom: 0;
+ }
+
.checkbox-label {
@include H7;
diff --git a/ui/components/app/flask/snap-install-warning/snap-install-warning.js b/ui/components/app/flask/snap-install-warning/snap-install-warning.js
index 70cedf0d2245..15c593c74a9c 100644
--- a/ui/components/app/flask/snap-install-warning/snap-install-warning.js
+++ b/ui/components/app/flask/snap-install-warning/snap-install-warning.js
@@ -6,12 +6,17 @@ import { useI18nContext } from '../../../../hooks/useI18nContext';
import CheckBox from '../../../ui/check-box/check-box.component';
import {
+ BackgroundColor,
+ IconColor,
TextVariant,
TEXT_ALIGN,
+ Size,
+ JustifyContent,
} from '../../../../helpers/constants/design-system';
import Popover from '../../../ui/popover';
import Button from '../../../ui/button';
-import { Text } from '../../../component-library';
+import { AvatarIcon, ICON_NAMES, Text } from '../../../component-library';
+import Box from '../../../ui/box/box';
/**
* a very simple reducer using produce from Immer to keep checkboxes state manipulation
@@ -45,13 +50,6 @@ export default function SnapInstallWarning({ onCancel, onSubmit, warnings }) {
const SnapInstallWarningFooter = () => {
return (
+ DeFi raises AML/CFT risk for institutions, given the decentralised pools and pseudonymous counterparties. +
++ Codefi Compliance is the only product capable of running AML/CFT analysis on DeFi pools. This allows you to identify and avoid pools and counterparties that fail your risk setting. +
++ Steps to enable AML/CFT Compliance: +
+