diff --git a/app/images/open-sea-security-provider.svg b/app/images/open-sea-security-provider.svg new file mode 100644 index 000000000000..ac79c0026adb --- /dev/null +++ b/app/images/open-sea-security-provider.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/scripts/controllers/metametrics.js b/app/scripts/controllers/metametrics.js index f9266a540e4c..a949f3cc7152 100644 --- a/app/scripts/controllers/metametrics.js +++ b/app/scripts/controllers/metametrics.js @@ -721,6 +721,9 @@ export default class MetaMetricsController { ///: BEGIN:ONLY_INCLUDE_IN(flask) [TRAITS.DESKTOP_ENABLED]: metamaskState.desktopEnabled || false, ///: END:ONLY_INCLUDE_IN + [TRAITS.SECURITY_PROVIDERS]: metamaskState.transactionSecurityCheckEnabled + ? ['opensea'] + : [], }; if (!previousUserTraits) { diff --git a/app/scripts/controllers/metametrics.test.js b/app/scripts/controllers/metametrics.test.js index 494d4f763f95..f3969a7b3843 100644 --- a/app/scripts/controllers/metametrics.test.js +++ b/app/scripts/controllers/metametrics.test.js @@ -952,6 +952,7 @@ describe('MetaMetricsController', function () { theme: 'default', useTokenDetection: true, desktopEnabled: false, + security_providers: [], }); assert.deepEqual(traits, { @@ -970,6 +971,7 @@ describe('MetaMetricsController', function () { [TRAITS.THEME]: 'default', [TRAITS.TOKEN_DETECTION_ENABLED]: true, [TRAITS.DESKTOP_ENABLED]: false, + [TRAITS.SECURITY_PROVIDERS]: [], }); }); diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 32f405617d76..112071ffd604 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -2147,6 +2147,7 @@ export default class TransactionController extends EventEmitter { originalApprovalAmount, finalApprovalAmount, contractMethodName, + securityProviderResponse, } = txMeta; const source = referrer === ORIGIN_METAMASK ? 'user' : 'dapp'; @@ -2298,6 +2299,16 @@ export default class TransactionController extends EventEmitter { } } + let uiCustomizations; + + if (securityProviderResponse?.flagAsDangerous === 1) { + uiCustomizations = ['flagged_as_malicious']; + } else if (securityProviderResponse?.flagAsDangerous === 2) { + uiCustomizations = ['flagged_as_safety_unknown']; + } else { + uiCustomizations = null; + } + let properties = { chain_id: chainId, referrer, @@ -2312,6 +2323,7 @@ export default class TransactionController extends EventEmitter { token_standard: tokenStandard, transaction_type: transactionType, transaction_speed_up: type === TransactionType.retry, + ui_customizations: uiCustomizations, }; if (transactionContractMethod === contractMethodNames.APPROVE) { diff --git a/app/scripts/controllers/transactions/index.test.js b/app/scripts/controllers/transactions/index.test.js index 7cf1f5a013da..096783faebde 100644 --- a/app/scripts/controllers/transactions/index.test.js +++ b/app/scripts/controllers/transactions/index.test.js @@ -1740,6 +1740,9 @@ describe('Transaction Controller', function () { gas: '0x7b0d', gasPrice: '0x77359400', }, + securityProviderResponse: { + flagAsDangerous: 0, + }, }; }); @@ -1766,6 +1769,7 @@ describe('Transaction Controller', function () { token_standard: TokenStandard.none, device_model: 'N/A', transaction_speed_up: false, + ui_customizations: null, }, sensitiveProperties: { default_gas: '0.000031501', @@ -1852,6 +1856,7 @@ describe('Transaction Controller', function () { token_standard: TokenStandard.none, device_model: 'N/A', transaction_speed_up: false, + ui_customizations: null, }, sensitiveProperties: { default_gas: '0.000031501', @@ -1921,6 +1926,9 @@ describe('Transaction Controller', function () { gas: '0x7b0d', gasPrice: '0x77359400', }, + securityProviderResponse: { + flagAsDangerous: 0, + }, }; }); @@ -1947,6 +1955,7 @@ describe('Transaction Controller', function () { token_standard: TokenStandard.none, device_model: 'N/A', transaction_speed_up: false, + ui_customizations: null, }, sensitiveProperties: { default_gas: '0.000031501', @@ -2035,6 +2044,7 @@ describe('Transaction Controller', function () { token_standard: TokenStandard.none, device_model: 'N/A', transaction_speed_up: false, + ui_customizations: null, }, sensitiveProperties: { default_gas: '0.000031501', @@ -2099,6 +2109,9 @@ describe('Transaction Controller', function () { chainId: currentChainId, time: 1624408066355, metamaskNetworkId: currentNetworkId, + securityProviderResponse: { + flagAsDangerous: 0, + }, }; const expectedPayload = { @@ -2122,6 +2135,7 @@ describe('Transaction Controller', function () { token_standard: TokenStandard.none, device_model: 'N/A', transaction_speed_up: false, + ui_customizations: null, }, sensitiveProperties: { gas_price: '2', @@ -2167,6 +2181,9 @@ describe('Transaction Controller', function () { chainId: currentChainId, time: 1624408066355, metamaskNetworkId: currentNetworkId, + securityProviderResponse: { + flagAsDangerous: 0, + }, }; const expectedPayload = { actionId, @@ -2190,6 +2207,155 @@ describe('Transaction Controller', function () { token_standard: TokenStandard.none, device_model: 'N/A', transaction_speed_up: false, + ui_customizations: null, + }, + sensitiveProperties: { + baz: 3.0, + foo: 'bar', + gas_price: '2', + gas_limit: '0x7b0d', + transaction_contract_method: undefined, + transaction_replaced: undefined, + first_seen: 1624408066355, + transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY, + status: 'unapproved', + }, + }; + + await txController._trackTransactionMetricsEvent( + txMeta, + TransactionMetaMetricsEvent.added, + actionId, + { + baz: 3.0, + foo: 'bar', + }, + ); + assert.equal(createEventFragmentSpy.callCount, 1); + assert.equal(finalizeEventFragmentSpy.callCount, 0); + assert.deepEqual( + createEventFragmentSpy.getCall(0).args[0], + expectedPayload, + ); + }); + + it('should call _trackMetaMetricsEvent with the correct payload (extra params) when flagAsDangerous is malicious', async function () { + const txMeta = { + id: 1, + status: TransactionStatus.unapproved, + txParams: { + from: fromAccount.address, + to: '0x1678a085c290ebd122dc42cba69373b5953b831d', + gasPrice: '0x77359400', + gas: '0x7b0d', + nonce: '0x4b', + }, + type: TransactionType.simpleSend, + origin: 'other', + chainId: currentChainId, + time: 1624408066355, + metamaskNetworkId: currentNetworkId, + securityProviderResponse: { + flagAsDangerous: 1, + }, + }; + const expectedPayload = { + actionId, + initialEvent: 'Transaction Added', + successEvent: 'Transaction Approved', + failureEvent: 'Transaction Rejected', + uniqueIdentifier: 'transaction-added-1', + persist: true, + category: EVENT.CATEGORIES.TRANSACTIONS, + properties: { + network: '5', + referrer: 'other', + source: EVENT.SOURCE.TRANSACTION.DAPP, + transaction_type: TransactionType.simpleSend, + chain_id: '0x5', + eip_1559_version: '0', + gas_edit_attempted: 'none', + gas_edit_type: 'none', + account_type: 'MetaMask', + asset_type: AssetType.native, + token_standard: TokenStandard.none, + device_model: 'N/A', + transaction_speed_up: false, + ui_customizations: ['flagged_as_malicious'], + }, + sensitiveProperties: { + baz: 3.0, + foo: 'bar', + gas_price: '2', + gas_limit: '0x7b0d', + transaction_contract_method: undefined, + transaction_replaced: undefined, + first_seen: 1624408066355, + transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY, + status: 'unapproved', + }, + }; + + await txController._trackTransactionMetricsEvent( + txMeta, + TransactionMetaMetricsEvent.added, + actionId, + { + baz: 3.0, + foo: 'bar', + }, + ); + assert.equal(createEventFragmentSpy.callCount, 1); + assert.equal(finalizeEventFragmentSpy.callCount, 0); + assert.deepEqual( + createEventFragmentSpy.getCall(0).args[0], + expectedPayload, + ); + }); + + it('should call _trackMetaMetricsEvent with the correct payload (extra params) when flagAsDangerous is unknown', async function () { + const txMeta = { + id: 1, + status: TransactionStatus.unapproved, + txParams: { + from: fromAccount.address, + to: '0x1678a085c290ebd122dc42cba69373b5953b831d', + gasPrice: '0x77359400', + gas: '0x7b0d', + nonce: '0x4b', + }, + type: TransactionType.simpleSend, + origin: 'other', + chainId: currentChainId, + time: 1624408066355, + metamaskNetworkId: currentNetworkId, + securityProviderResponse: { + flagAsDangerous: 2, + }, + }; + const expectedPayload = { + actionId, + initialEvent: 'Transaction Added', + successEvent: 'Transaction Approved', + failureEvent: 'Transaction Rejected', + uniqueIdentifier: 'transaction-added-1', + persist: true, + category: EVENT.CATEGORIES.TRANSACTIONS, + properties: { + network: '5', + referrer: 'other', + source: EVENT.SOURCE.TRANSACTION.DAPP, + transaction_type: TransactionType.simpleSend, + chain_id: '0x5', + eip_1559_version: '0', + gas_edit_attempted: 'none', + gas_edit_type: 'none', + account_type: 'MetaMask', + asset_type: AssetType.native, + token_standard: TokenStandard.none, + device_model: 'N/A', + transaction_speed_up: false, + ui_customizations: ['flagged_as_safety_unknown'], }, sensitiveProperties: { baz: 3.0, @@ -2245,6 +2411,9 @@ describe('Transaction Controller', function () { maxFeePerGas: '0x77359400', maxPriorityFeePerGas: '0x77359400', }, + securityProviderResponse: { + flagAsDangerous: 0, + }, }; const expectedPayload = { actionId, @@ -2268,6 +2437,7 @@ describe('Transaction Controller', function () { token_standard: TokenStandard.none, device_model: 'N/A', transaction_speed_up: false, + ui_customizations: null, }, sensitiveProperties: { baz: 3.0, diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.js index e1376ae1e785..8a5186d42148 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.js @@ -107,14 +107,16 @@ const rateLimitTimeouts = {}; * MetaMetricsController * @param {number} [opts.rateLimitSeconds] - number of seconds to wait before * allowing another set of events to be tracked. + * @param opts.securityProviderRequest * @returns {Function} */ export default function createRPCMethodTrackingMiddleware({ trackEvent, getMetricsState, rateLimitSeconds = 60 * 5, + securityProviderRequest, }) { - return function rpcMethodTrackingMiddleware( + return async function rpcMethodTrackingMiddleware( /** @type {any} */ req, /** @type {any} */ res, /** @type {Function} */ next, @@ -162,20 +164,63 @@ export default function createRPCMethodTrackingMiddleware({ const properties = {}; + let msgParams; + if (event === EVENT_NAMES.SIGNATURE_REQUESTED) { properties.signature_type = method; - } else { - properties.method = method; - } - if (method === MESSAGE_TYPE.PERSONAL_SIGN) { const data = req?.params?.[0]; - const { isSIWEMessage } = detectSIWE({ data }); - if (isSIWEMessage) { - properties.ui_customizations = [ - METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS].SIWE, - ]; + 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({ @@ -192,7 +237,7 @@ export default function createRPCMethodTrackingMiddleware({ }, SECOND * rateLimitSeconds); } - next((callback) => { + next(async (callback) => { if (shouldTrackEvent === false || typeof eventType === 'undefined') { return callback(); } @@ -216,20 +261,63 @@ export default function createRPCMethodTrackingMiddleware({ event = eventType.APPROVED; } + let msgParams; + if (eventType.REQUESTED === EVENT_NAMES.SIGNATURE_REQUESTED) { properties.signature_type = method; - } else { - properties.method = method; - } - if (method === MESSAGE_TYPE.PERSONAL_SIGN) { const data = req?.params?.[0]; - const { isSIWEMessage } = detectSIWE({ data }); - if (isSIWEMessage) { - properties.ui_customizations = [ - METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS].SIWE, - ]; + 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({ diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js index 9a9f72157703..af910279e859 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js @@ -8,10 +8,19 @@ const trackEvent = jest.fn(); const metricsState = { participateInMetaMetrics: null }; const getMetricsState = () => metricsState; +let flagAsDangerous = 0; + +const securityProviderRequest = () => { + return { + flagAsDangerous, + }; +}; + const handler = createRPCMethodTrackingMiddleware({ trackEvent, getMetricsState, rateLimitSeconds: 1, + securityProviderRequest, }); function getNext(timeout = 500) { @@ -92,7 +101,7 @@ describe('createRPCMethodTrackingMiddleware', () => { metricsState.participateInMetaMetrics = true; }); - it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event`, () => { + it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event`, async () => { const req = { method: MESSAGE_TYPE.ETH_SIGN, origin: 'some.dapp', @@ -102,12 +111,14 @@ describe('createRPCMethodTrackingMiddleware', () => { error: null, }; const { next } = getNext(); - handler(req, res, next); + 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 }, + properties: { + signature_type: MESSAGE_TYPE.ETH_SIGN, + }, referrer: { url: 'some.dapp' }, }); }); @@ -122,13 +133,15 @@ describe('createRPCMethodTrackingMiddleware', () => { error: null, }; const { next, executeMiddlewareStack } = getNext(); - handler(req, res, next); + 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.ETH_SIGN_TYPED_DATA_V4 }, + properties: { + signature_type: MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4, + }, referrer: { url: 'some.dapp' }, }); }); @@ -143,13 +156,15 @@ describe('createRPCMethodTrackingMiddleware', () => { error: { code: 4001 }, }; const { next, executeMiddlewareStack } = getNext(); - handler(req, res, next); + 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_REJECTED, - properties: { signature_type: MESSAGE_TYPE.PERSONAL_SIGN }, + properties: { + signature_type: MESSAGE_TYPE.PERSONAL_SIGN, + }, referrer: { url: 'some.dapp' }, }); }); @@ -162,7 +177,7 @@ describe('createRPCMethodTrackingMiddleware', () => { const res = {}; const { next, executeMiddlewareStack } = getNext(); - handler(req, res, next); + await handler(req, res, next); await executeMiddlewareStack(); expect(trackEvent).toHaveBeenCalledTimes(2); expect(trackEvent.mock.calls[1][0]).toMatchObject({ @@ -227,7 +242,7 @@ describe('createRPCMethodTrackingMiddleware', () => { }; const { next, executeMiddlewareStack } = getNext(); - handler(req, res, next); + await handler(req, res, next); await executeMiddlewareStack(); expect(trackEvent).toHaveBeenCalledTimes(2); @@ -244,4 +259,93 @@ describe('createRPCMethodTrackingMiddleware', () => { }); }); }); + + describe('participateInMetaMetrics is set to true with a request flagged as safe', () => { + beforeEach(() => { + metricsState.participateInMetaMetrics = true; + }); + + 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', + }; + + 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' }, + }); + }); + }); + + describe('participateInMetaMetrics is set to true with a request flagged as malicious', () => { + beforeEach(() => { + metricsState.participateInMetaMetrics = true; + 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', + }; + + 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' }, + }); + }); + }); + + describe('participateInMetaMetrics is set to true with a request flagged as safety unknown', () => { + beforeEach(() => { + metricsState.participateInMetaMetrics = true; + 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', + }; + + 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' }, + }); + }); + }); }); diff --git a/app/scripts/lib/security-provider-helpers.js b/app/scripts/lib/security-provider-helpers.js index db66c462d213..b05c9a40bb8f 100644 --- a/app/scripts/lib/security-provider-helpers.js +++ b/app/scripts/lib/security-provider-helpers.js @@ -39,12 +39,12 @@ export async function securityProviderCheck( rpc_method_name: methodName, chain_id: chainId, data: { - from_address: requestData.txParams.from, - to_address: requestData.txParams.to, - gas: requestData.txParams.gas, - gasPrice: requestData.txParams.gasPrice, - value: requestData.txParams.value, - data: requestData.txParams.data, + from_address: requestData?.txParams?.from, + to_address: requestData?.txParams?.to, + gas: requestData?.txParams?.gas, + gasPrice: requestData?.txParams?.gasPrice, + value: requestData?.txParams?.value, + data: requestData?.txParams?.data, }, currentLocale, }; diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 6a2c1107d07c..b4c2ef4ba55e 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -748,7 +748,7 @@ export default class MetamaskController extends EventEmitter { ///: BEGIN:ONLY_INCLUDE_IN(flask) const snapExecutionServiceArgs = { iframeUrl: new URL( - 'https://metamask.github.io/iframe-execution-environment/0.13.0', + 'https://metamask.github.io/iframe-execution-environment/0.14.0', ), messenger: this.controllerMessenger.getRestricted({ name: 'ExecutionService', @@ -3729,6 +3729,7 @@ export default class MetamaskController extends EventEmitter { getMetricsState: this.metaMetricsController.store.getState.bind( this.metaMetricsController.store, ), + securityProviderRequest: this.securityProviderRequest.bind(this), }), ); @@ -4383,11 +4384,11 @@ export default class MetamaskController extends EventEmitter { const { currentLocale, transactionSecurityCheckEnabled } = this.preferencesController.store.getState(); - const chainId = Number( - hexToDecimal(this.networkController.store.getState().provider.chainId), - ); - if (transactionSecurityCheckEnabled) { + const chainId = Number( + hexToDecimal(this.networkController.store.getState().provider.chainId), + ); + try { const securityProviderResponse = await securityProviderCheck( requestData, diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index faf9af160bf7..3a298e699c41 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -176,9 +176,14 @@ export default class ExtensionPlatform { _showFailedTransaction(txMeta, errorMessage) { const nonce = parseInt(txMeta.txParams.nonce, 16); const title = 'Failed transaction'; - const message = `Transaction ${nonce} failed! ${ + let message = `Transaction ${nonce} failed! ${ errorMessage || txMeta.err.message }`; + ///: BEGIN:ONLY_INCLUDE_IN(mmi) + if (isNaN(nonce)) { + message = `Transaction failed! ${errorMessage || txMeta.err.message}`; + } + ///: END:ONLY_INCLUDE_IN this._showNotification(title, message); } diff --git a/shared/constants/bridge.ts b/shared/constants/bridge.ts new file mode 100644 index 000000000000..a70c0a62a55a --- /dev/null +++ b/shared/constants/bridge.ts @@ -0,0 +1,10 @@ +import { CHAIN_IDS } from './network'; + +export const ALLOWED_BRIDGE_CHAIN_IDS = [ + CHAIN_IDS.MAINNET, + CHAIN_IDS.BSC, + CHAIN_IDS.POLYGON, + CHAIN_IDS.AVALANCHE, + CHAIN_IDS.OPTIMISM, + CHAIN_IDS.ARBITRUM, +]; diff --git a/shared/constants/metametrics.js b/shared/constants/metametrics.js index 853807ce5ab9..a9691d5a29a2 100644 --- a/shared/constants/metametrics.js +++ b/shared/constants/metametrics.js @@ -187,6 +187,8 @@ * identify the token_detection_enabled trait * @property {'install_date_ext'} INSTALL_DATE_EXT - when the user installed the extension * @property {'desktop_enabled'} [DESKTOP_ENABLED] - optional / does the user have desktop enabled? + * @property {'security_providers'} SECURITY_PROVIDERS - when security provider feature is toggled we + * identify the security_providers trait */ /** @@ -210,6 +212,7 @@ export const TRAITS = { THREE_BOX_ENABLED: 'three_box_enabled', TOKEN_DETECTION_ENABLED: 'token_detection_enabled', DESKTOP_ENABLED: 'desktop_enabled', + SECURITY_PROVIDERS: 'security_providers', }; /** @@ -240,6 +243,7 @@ export const TRAITS = { * @property {string} [theme] - which theme the user has selected * @property {boolean} [token_detection_enabled] - does the user have token detection is enabled? * @property {boolean} [desktop_enabled] - optional / does the user have desktop enabled? + * @property {Array} [security_providers] - whether security provider feature toggle is on or off */ // Mixpanel converts the zero address value to a truly anonymous event, which diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 0774126239b1..e2d6cf40eebe 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -688,4 +688,4 @@ export const FEATURED_RPCS: RPCDefinition[] = [ ]; export const SHOULD_SHOW_LINEA_TESTNET_NETWORK = - new Date().getTime() > Date.UTC(2023, 2, 28); + new Date().getTime() > Date.UTC(2023, 2, 28, 8); diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js index c1fa2a4a1de4..6f3754bd8879 100644 --- a/test/e2e/metamask-ui.spec.js +++ b/test/e2e/metamask-ui.spec.js @@ -313,11 +313,10 @@ describe('MetaMask', function () { text: 'Transfer', }); - const tokenAmount = await driver.findElement( - '.confirm-page-container-summary__title-text', - ); - const tokenAmountText = await tokenAmount.getText(); - assert.equal(tokenAmountText, '1 TST'); + await driver.findElement({ + tag: 'h1', + text: '1 TST', + }); await driver.waitForSelector({ tag: 'p', @@ -419,11 +418,10 @@ describe('MetaMask', function () { }); it('submits the transaction', async function () { - const tokenAmount = await driver.findElement( - '.confirm-page-container-summary__title-text', - ); - const tokenAmountText = await tokenAmount.getText(); - assert.equal(tokenAmountText, '1.5 TST'); + await driver.findElement({ + tag: 'h1', + text: '1.5 TST', + }); await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.delay(regularDelayMs); diff --git a/test/e2e/tests/eth-sign.spec.js b/test/e2e/tests/eth-sign.spec.js index 51ef2168c217..5b0feb3b3618 100644 --- a/test/e2e/tests/eth-sign.spec.js +++ b/test/e2e/tests/eth-sign.spec.js @@ -31,10 +31,10 @@ describe('Eth sign', function () { await driver.openNewPage('http://127.0.0.1:8080/'); await driver.clickElement('#ethSign'); + await driver.delay(1000); const ethSignButton = await driver.findElement('#ethSign'); const exceptionString = 'ERROR: ETH_SIGN HAS BEEN DISABLED. YOU MUST ENABLE IT IN THE ADVANCED SETTINGS'; - assert.equal(await ethSignButton.getText(), exceptionString); }, ); diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js index 9ac822f5930d..c10bb8c06b55 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js @@ -29,11 +29,9 @@ export default class ConfirmPageContainerContent extends Component { ///: END:ONLY_INCLUDE_IN errorKey: PropTypes.string, errorMessage: PropTypes.string, - hideSubtitle: PropTypes.bool, tokenAddress: PropTypes.string, nonce: PropTypes.string, subtitleComponent: PropTypes.node, - title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), image: PropTypes.string, titleComponent: PropTypes.node, warning: PropTypes.string, @@ -48,7 +46,6 @@ export default class ConfirmPageContainerContent extends Component { disabled: PropTypes.bool, unapprovedTxCount: PropTypes.number, rejectNText: PropTypes.string, - hideTitle: PropTypes.bool, supportsEIP1559: PropTypes.bool, hasTopBorder: PropTypes.bool, nativeCurrency: PropTypes.string, @@ -136,11 +133,9 @@ export default class ConfirmPageContainerContent extends Component { action, errorKey, errorMessage, - title, image, titleComponent, subtitleComponent, - hideSubtitle, tokenAddress, nonce, detailsComponent, @@ -156,7 +151,6 @@ export default class ConfirmPageContainerContent extends Component { rejectNText, origin, ethGasPriceWarning, - hideTitle, supportsEIP1559, hasTopBorder, nativeCurrency, @@ -199,15 +193,12 @@ export default class ConfirmPageContainerContent extends Component { !detailsComponent || !dataComponent, })} action={action} - title={title} image={image} titleComponent={titleComponent} subtitleComponent={subtitleComponent} - hideSubtitle={hideSubtitle} tokenAddress={tokenAddress} nonce={nonce} origin={origin} - hideTitle={hideTitle} toAddress={toAddress} transactionType={transactionType} /> diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js index 90f3dc162572..46a3f068f42e 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js @@ -14,8 +14,6 @@ import { getIpfsGateway } from '../../../../../selectors'; import Identicon from '../../../../ui/identicon'; import InfoTooltip from '../../../../ui/info-tooltip'; import NicknamePopovers from '../../../modals/nickname-popovers'; -import { Text } from '../../../../component-library'; -import { TextVariant } from '../../../../../helpers/constants/design-system'; import { ORIGIN_METAMASK } from '../../../../../../shared/constants/app'; import SiteOrigin from '../../../../ui/site-origin'; import { getAssetImageURL } from '../../../../../helpers/utils/util'; @@ -23,16 +21,13 @@ import { getAssetImageURL } from '../../../../../helpers/utils/util'; const ConfirmPageContainerSummary = (props) => { const { action, - title, titleComponent, subtitleComponent, - hideSubtitle, className, tokenAddress, toAddress, nonce, origin, - hideTitle, image, transactionType, } = props; @@ -130,26 +125,9 @@ const ConfirmPageContainerSummary = (props) => { <>
{renderImage()} - {!hideTitle ? ( - - {titleComponent || title} - - ) : null} + {titleComponent}
- {hideSubtitle ? null : ( -
- {subtitleComponent} -
- )} + {subtitleComponent} {showNicknamePopovers && ( { ConfirmPageContainerSummary.propTypes = { action: PropTypes.string, - title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), image: PropTypes.string, titleComponent: PropTypes.node, subtitleComponent: PropTypes.node, - hideSubtitle: PropTypes.bool, className: PropTypes.string, tokenAddress: PropTypes.string, toAddress: PropTypes.string, nonce: PropTypes.string, origin: PropTypes.string.isRequired, - hideTitle: PropTypes.bool, transactionType: PropTypes.string, }; diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss index 05e7132e416a..9514cbef278f 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss +++ b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss @@ -69,31 +69,6 @@ margin-right: 8px; } - &__title-text { - @include H1; - - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - &__title-text-long { - @include H3; - - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - &__subtitle { - @include H5; - - color: var(--color-text-alternative); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - &--border { border-bottom: 1px solid var(--color-border-muted); } diff --git a/ui/components/app/confirm-page-container/confirm-page-container.component.js b/ui/components/app/confirm-page-container/confirm-page-container.component.js index 4c31366c75cd..1943c964b145 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container.component.js @@ -66,7 +66,6 @@ const ConfirmPageContainer = (props) => { image, titleComponent, subtitleComponent, - hideSubtitle, detailsComponent, dataComponent, dataHexComponent, @@ -123,11 +122,6 @@ const ConfirmPageContainer = (props) => { const shouldDisplayWarning = contentComponent && disabled && (errorKey || errorMessage); - const hideTitle = - (currentTransaction.type === TransactionType.contractInteraction || - currentTransaction.type === TransactionType.deployContract) && - currentTransaction.txParams?.value === '0x0'; - const networkName = NETWORK_TO_NAME_MAP[currentTransaction.chainId] || networkIdentifier; @@ -203,7 +197,6 @@ const ConfirmPageContainer = (props) => { image={image} titleComponent={titleComponent} subtitleComponent={subtitleComponent} - hideSubtitle={hideSubtitle} detailsComponent={detailsComponent} dataComponent={dataComponent} dataHexComponent={dataHexComponent} @@ -225,7 +218,6 @@ const ConfirmPageContainer = (props) => { rejectNText={t('rejectTxsN', [unapprovedTxCount])} origin={origin} ethGasPriceWarning={ethGasPriceWarning} - hideTitle={hideTitle} supportsEIP1559={supportsEIP1559} currentTransaction={currentTransaction} nativeCurrency={nativeCurrency} @@ -341,7 +333,6 @@ const ConfirmPageContainer = (props) => { ConfirmPageContainer.propTypes = { // Header action: PropTypes.string, - hideSubtitle: PropTypes.bool, onEdit: PropTypes.func, showEdit: PropTypes.bool, subtitleComponent: PropTypes.node, diff --git a/ui/components/app/confirm-subtitle/README.mdx b/ui/components/app/confirm-subtitle/README.mdx new file mode 100644 index 000000000000..8c5da7bec89d --- /dev/null +++ b/ui/components/app/confirm-subtitle/README.mdx @@ -0,0 +1,10 @@ +import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; +import { ConfirmSubTitle } from '.'; + +# Confirm Sub Title + +Confirm Sub Title is used on confirmation screen to display transaction amoutn in header. + + + + diff --git a/ui/components/app/confirm-subtitle/confirm-subtitle.js b/ui/components/app/confirm-subtitle/confirm-subtitle.js new file mode 100644 index 000000000000..fd4366d870ae --- /dev/null +++ b/ui/components/app/confirm-subtitle/confirm-subtitle.js @@ -0,0 +1,56 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; + +import { SECONDARY } from '../../../helpers/constants/common'; +import { Text } from '../../component-library'; +import { + Color, + FONT_WEIGHT, + TextVariant, +} from '../../../helpers/constants/design-system'; +import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'; +import { getShouldShowFiat } from '../../../selectors'; +import { useTransactionInfo } from '../../../hooks/useTransactionInfo'; + +const ConfirmSubTitle = ({ + txData, + hexTransactionAmount, + subtitleComponent, +}) => { + const shouldShowFiat = useSelector(getShouldShowFiat); + const { isNftTransfer } = useTransactionInfo(txData); + + if (!shouldShowFiat && !isNftTransfer) { + return null; + } + + if (subtitleComponent) { + return subtitleComponent; + } + + return ( + + + + ); +}; + +ConfirmSubTitle.propTypes = { + hexTransactionAmount: PropTypes.string, + subtitleComponent: PropTypes.element, + txData: PropTypes.object.isRequired, +}; + +export default ConfirmSubTitle; diff --git a/ui/components/app/confirm-subtitle/confirm-subtitle.stories.js b/ui/components/app/confirm-subtitle/confirm-subtitle.stories.js new file mode 100644 index 000000000000..9a05e509cde5 --- /dev/null +++ b/ui/components/app/confirm-subtitle/confirm-subtitle.stories.js @@ -0,0 +1,51 @@ +import React from 'react'; +import { Provider } from 'react-redux'; + +import mockState from '../../../../test/data/mock-state.json'; +import configureStore from '../../../store/store'; + +import README from './README.mdx'; +import ConfirmSubTitle from './confirm-subtitle'; + +mockState.metamask.preferences.showFiatInTestnets = true; +const store = configureStore(mockState); + +export default { + title: 'Components/App/ConfirmSubTitle', + + component: ConfirmSubTitle, + decorators: [(story) => {story()}], + parameters: { + docs: { + page: README, + }, + }, + argTypes: { + txData: 'object', + hexTransactionAmount: 'number', + title: 'string', + }, + args: { + txData: { + txParams: {}, + type: 'transfer', + }, + hexTransactionAmount: '0x9184e72a000', + subtitleComponent: undefined, + }, +}; + +export const DefaultStory = (args) => { + return ; +}; + +DefaultStory.storyName = 'Default'; + +export const CustomSubTitleStory = (args) => { + return ; +}; + +CustomSubTitleStory.storyName = 'CustomSubTitle'; +CustomSubTitleStory.args = { + subtitleComponent: 'Any custom sub title passed', +}; diff --git a/ui/components/app/confirm-subtitle/confirm-subtitle.test.js b/ui/components/app/confirm-subtitle/confirm-subtitle.test.js new file mode 100644 index 000000000000..ac2662a396a3 --- /dev/null +++ b/ui/components/app/confirm-subtitle/confirm-subtitle.test.js @@ -0,0 +1,80 @@ +import React from 'react'; + +import mockState from '../../../../test/data/mock-state.json'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import configureStore from '../../../store/store'; +import ConfirmSubTitle from './confirm-subtitle'; + +describe('ConfirmSubTitle', () => { + let store; + beforeEach(() => { + mockState.metamask.preferences.showFiatInTestnets = true; + store = configureStore(mockState); + }); + + it('should render subtitle correctly', async () => { + const { findByText } = renderWithProvider( + , + store, + ); + expect(await findByText('$0.01')).toBeInTheDocument(); + }); + + it('should return null if showFiatInTestnets preference if false', () => { + mockState.metamask.preferences.showFiatInTestnets = false; + store = configureStore(mockState); + + const { container } = renderWithProvider( + , + store, + ); + expect(container.firstChild).toStrictEqual(null); + }); + + it('should not null if showFiatInTestnets preference if false but it is NFT Transfer', async () => { + mockState.metamask.preferences.showFiatInTestnets = false; + mockState.metamask.allNftContracts = { + [mockState.metamask.selectedAddress]: { + [mockState.metamask.provider.chainId]: [{ address: '0x9' }], + }, + }; + store = configureStore(mockState); + + const { findByText } = renderWithProvider( + , + store, + ); + expect(await findByText('0.00001')).toBeInTheDocument(); + }); + + it('should render subtitleComponent if passed', () => { + const { getByText } = renderWithProvider( + dummy_sub_title_passed} + />, + store, + ); + expect(getByText('dummy_sub_title_passed')).toBeInTheDocument(); + }); +}); diff --git a/ui/components/app/confirm-subtitle/index.js b/ui/components/app/confirm-subtitle/index.js new file mode 100644 index 000000000000..ecb69693168d --- /dev/null +++ b/ui/components/app/confirm-subtitle/index.js @@ -0,0 +1 @@ +export { default as ConfirmSubTitle } from './confirm-subtitle'; diff --git a/ui/components/app/confirm-title/README.mdx b/ui/components/app/confirm-title/README.mdx new file mode 100644 index 000000000000..d68f7d30cea3 --- /dev/null +++ b/ui/components/app/confirm-title/README.mdx @@ -0,0 +1,10 @@ +import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; +import { ConfirmTitle } from '.'; + +# Confirm Title + +Confirm Title is used on the confirmation screen to display the transaction amount in the header. + + + + diff --git a/ui/components/app/confirm-title/confirm-title.js b/ui/components/app/confirm-title/confirm-title.js new file mode 100644 index 000000000000..125d8d8a347f --- /dev/null +++ b/ui/components/app/confirm-title/confirm-title.js @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { TransactionType } from '../../../../shared/constants/transaction'; +import { PRIMARY } from '../../../helpers/constants/common'; +import { Text } from '../../component-library'; +import { + FONT_WEIGHT, + TextVariant, +} from '../../../helpers/constants/design-system'; +import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'; + +const ConfirmTitle = ({ title, hexTransactionAmount, txData }) => { + const isContractInteraction = + txData.type === TransactionType.contractInteraction; + + const hideTitle = + (isContractInteraction || txData.type === TransactionType.deployContract) && + txData.txParams?.value === '0x0'; + + if (hideTitle) { + return null; + } + + if (title) { + return ( + + {title} + + ); + } + + return ( + + + + ); +}; + +ConfirmTitle.propTypes = { + txData: PropTypes.object.isRequired, + title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + hexTransactionAmount: PropTypes.string, +}; + +export default ConfirmTitle; diff --git a/ui/components/app/confirm-title/confirm-title.stories.js b/ui/components/app/confirm-title/confirm-title.stories.js new file mode 100644 index 000000000000..60037daa1db3 --- /dev/null +++ b/ui/components/app/confirm-title/confirm-title.stories.js @@ -0,0 +1,54 @@ +import React from 'react'; +import README from './README.mdx'; +import ConfirmTitle from './confirm-title'; + +export default { + title: 'Components/App/ConfirmTitle', + + component: ConfirmTitle, + parameters: { + docs: { + page: README, + }, + }, + argTypes: { + txData: 'object', + hexTransactionAmount: 'string', + title: 'string', + }, + args: { + txData: { + txParams: {}, + type: 'transfer', + }, + hexTransactionAmount: '0x9184e72a000', + title: undefined, + }, +}; + +export const DefaultStory = (args) => { + return ; +}; + +DefaultStory.storyName = 'Default'; + +export const ContractInteractionStory = (args) => { + return ; +}; + +ContractInteractionStory.storyName = 'ContractInteraction'; +ContractInteractionStory.args = { + txData: { + txParams: {}, + type: 'contractInteraction', + }, +}; + +export const CustomTitleStory = (args) => { + return ; +}; + +CustomTitleStory.storyName = 'CustomTitle'; +CustomTitleStory.args = { + title: 'Any custom title passed', +}; diff --git a/ui/components/app/confirm-title/confirm-title.test.js b/ui/components/app/confirm-title/confirm-title.test.js new file mode 100644 index 000000000000..e6098dd04791 --- /dev/null +++ b/ui/components/app/confirm-title/confirm-title.test.js @@ -0,0 +1,54 @@ +import React from 'react'; +import { TransactionType } from '../../../../shared/constants/transaction'; + +import mockState from '../../../../test/data/mock-state.json'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; + +import configureStore from '../../../store/store'; +import ConfirmTitle from './confirm-title'; + +describe('ConfirmTitle', () => { + const store = configureStore(mockState); + + it('should render title correctly', async () => { + const { findByText } = renderWithProvider( + , + store, + ); + expect(await findByText('0.00001')).toBeInTheDocument(); + }); + + it('should return null if transaction is contract interation with 0 value', () => { + const { container } = renderWithProvider( + , + store, + ); + expect(container.firstChild).toStrictEqual(null); + }); + + it('should render title if passed', () => { + const { getByText } = renderWithProvider( + , + store, + ); + expect(getByText('dummy_title_passed')).toBeInTheDocument(); + }); +}); diff --git a/ui/components/app/confirm-title/index.js b/ui/components/app/confirm-title/index.js new file mode 100644 index 000000000000..11db0d32c5c1 --- /dev/null +++ b/ui/components/app/confirm-title/index.js @@ -0,0 +1 @@ +export { default as ConfirmTitle } from './confirm-title'; diff --git a/ui/components/app/dropdowns/network-dropdown.js b/ui/components/app/dropdowns/network-dropdown.js index 09e94c4981d7..c40ccc79d887 100644 --- a/ui/components/app/dropdowns/network-dropdown.js +++ b/ui/components/app/dropdowns/network-dropdown.js @@ -286,22 +286,20 @@ class NetworkDropdown extends Component { } renderNonInfuraDefaultNetwork(networkConfigurations, network) { - const { - provider: { type: providerType }, - setActiveNetwork, - upsertNetworkConfiguration, - } = this.props; + const { provider, setActiveNetwork, upsertNetworkConfiguration } = + this.props; - const isCurrentRpcTarget = providerType === NETWORK_TYPES.RPC; + const { chainId, ticker, blockExplorerUrl } = BUILT_IN_NETWORKS[network]; + const networkName = NETWORK_TO_NAME_MAP[network]; + const rpcUrl = CHAIN_ID_TO_RPC_URL_MAP[chainId]; + + const isCurrentRpcTarget = + provider.type === NETWORK_TYPES.RPC && rpcUrl === provider.rpcUrl; return ( { - const { chainId, ticker, blockExplorerUrl } = - BUILT_IN_NETWORKS[network]; - const networkName = NETWORK_TO_NAME_MAP[network]; - const networkConfiguration = pickBy( networkConfigurations, (config) => config.rpcUrl === CHAIN_ID_TO_RPC_URL_MAP[chainId], @@ -310,7 +308,6 @@ class NetworkDropdown extends Component { let configurationId = null; // eslint-disable-next-line no-extra-boolean-cast, no-implicit-coercion if (!!networkConfiguration) { - const rpcUrl = CHAIN_ID_TO_RPC_URL_MAP[chainId]; configurationId = await upsertNetworkConfiguration( { rpcUrl, @@ -346,7 +343,7 @@ class NetworkDropdown extends Component { data-testid={`${network}-network-item`} style={{ color: - providerType === network + provider.type === network ? 'var(--color-text-default)' : 'var(--color-text-alternative)', }} diff --git a/ui/components/app/permissions-connect-header/permissions-connect-header.component.js b/ui/components/app/permissions-connect-header/permissions-connect-header.component.js index cc8f955135df..43b6d5f7e323 100644 --- a/ui/components/app/permissions-connect-header/permissions-connect-header.component.js +++ b/ui/components/app/permissions-connect-header/permissions-connect-header.component.js @@ -64,6 +64,7 @@ export default class PermissionsConnectHeader extends Component {
* { + flex: 0 0 auto; + min-height: 0; + } } &__origin { @@ -92,13 +99,9 @@ } &__rows { - overflow-y: auto; - overflow-x: hidden; border-top: 1px solid var(--color-border-default); display: flex; flex-flow: column; - flex: 1 1 0; - min-height: 0; } &__row { diff --git a/ui/components/app/signature-request-original/signature-request-original.component.js b/ui/components/app/signature-request-original/signature-request-original.component.js index 662f665d3689..9d46e92d1671 100644 --- a/ui/components/app/signature-request-original/signature-request-original.component.js +++ b/ui/components/app/signature-request-original/signature-request-original.component.js @@ -154,6 +154,7 @@ export default class SignatureRequestOriginal extends Component { ) : null}
{ const showFiat = useSelector(getShouldShowFiat); const balance = useSelector(getSelectedAccountCachedBalance); const isSwapsChain = useSelector(getIsSwapsChain); + const isBridgeChain = useSelector(getIsBridgeChain); const isBuyableChain = useSelector(getIsBuyableChain); const primaryTokenImage = useSelector(getNativeCurrencyImage); const defaultSwapsToken = useSelector(getSwapsDefaultToken); @@ -232,26 +234,41 @@ const EthOverview = ({ className }) => { /> } label={t('bridge')} onClick={() => { - const portfolioUrl = process.env.PORTFOLIO_URL; - const bridgeUrl = `${portfolioUrl}/bridge`; - global.platform.openTab({ - url: `${bridgeUrl}?metamaskEntry=ext`, - }); - trackEvent({ - category: EVENT.CATEGORIES.NAVIGATION, - event: EVENT_NAMES.BRIDGE_LINK_CLICKED, - properties: { - location: 'Home', - text: 'Bridge', - }, - }); + if (isBridgeChain) { + const portfolioUrl = process.env.PORTFOLIO_URL; + const bridgeUrl = `${portfolioUrl}/bridge`; + global.platform.openTab({ + url: `${bridgeUrl}?metamaskEntry=ext`, + }); + trackEvent({ + category: EVENT.CATEGORIES.NAVIGATION, + event: EVENT_NAMES.BRIDGE_LINK_CLICKED, + properties: { + location: 'Home', + text: 'Bridge', + }, + }); + } }} + tooltipRender={ + isBridgeChain + ? null + : (contents) => ( + + {contents} + + ) + } /> } diff --git a/ui/components/app/wallet-overview/eth-overview.test.js b/ui/components/app/wallet-overview/eth-overview.test.js index c7337719940c..3d4f459c18f2 100644 --- a/ui/components/app/wallet-overview/eth-overview.test.js +++ b/ui/components/app/wallet-overview/eth-overview.test.js @@ -145,13 +145,30 @@ describe('EthOverview', () => { expect(secondaryBalance).toHaveTextContent('0'); }); - it('should always show the Bridge button', () => { - const { queryByTestId } = renderWithProvider(, store); + it('should have the Bridge button enabled if chain id is part of supported chains', () => { + const mockedAvalancheStore = { + ...mockStore, + metamask: { + ...mockStore.metamask, + provider: { ...mockStore.metamask.provider, chainId: '0xa86a' }, + }, + }; + const mockedStore = configureMockStore([thunk])(mockedAvalancheStore); + + const { queryByTestId, queryByText } = renderWithProvider( + , + mockedStore, + ); const bridgeButton = queryByTestId(ETH_OVERVIEW_BRIDGE); expect(bridgeButton).toBeInTheDocument(); + expect(bridgeButton).toBeEnabled(); + expect(queryByText('Bridge').parentElement).not.toHaveAttribute( + 'data-original-title', + 'Unavailable on this network', + ); }); - it('should open the Bridge URI when clicking on Bridge button', async () => { + it('should open the Bridge URI when clicking on Bridge button on supported network', async () => { const { queryByTestId } = renderWithProvider(, store); const bridgeButton = queryByTestId(ETH_OVERVIEW_BRIDGE); @@ -169,6 +186,29 @@ describe('EthOverview', () => { ); }); + it('should have the Bridge button disabled if chain id is not part of supported chains', () => { + const mockedFantomStore = { + ...mockStore, + metamask: { + ...mockStore.metamask, + provider: { ...mockStore.metamask.provider, chainId: '0xfa' }, + }, + }; + const mockedStore = configureMockStore([thunk])(mockedFantomStore); + + const { queryByTestId, queryByText } = renderWithProvider( + , + mockedStore, + ); + const bridgeButton = queryByTestId(ETH_OVERVIEW_BRIDGE); + expect(bridgeButton).toBeInTheDocument(); + expect(bridgeButton).toBeDisabled(); + expect(queryByText('Bridge').parentElement).toHaveAttribute( + 'data-original-title', + 'Unavailable on this network', + ); + }); + it('should always show the Portfolio button', () => { const { queryByTestId } = renderWithProvider(, store); const portfolioButton = queryByTestId(ETH_OVERVIEW_PORTFOLIO); diff --git a/ui/components/component-library/avatar-account/__snapshots__/avatar-account.test.js.snap b/ui/components/component-library/avatar-account/__snapshots__/avatar-account.test.js.snap index 0dfe9536f963..6a46eb54663b 100644 --- a/ui/components/component-library/avatar-account/__snapshots__/avatar-account.test.js.snap +++ b/ui/components/component-library/avatar-account/__snapshots__/avatar-account.test.js.snap @@ -3,7 +3,7 @@ exports[`AvatarAccount should render correctly 1`] = `