diff --git a/app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js b/app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js index 7c596f7d0..25b8e5fb4 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js @@ -12,6 +12,7 @@ const sendMetadata = { implementation: sendMetadataHandler, hookNames: { addSubjectMetadata: true, + subjectType: true, }, }; export default sendMetadata; @@ -20,6 +21,7 @@ export default sendMetadata; * @typedef {Record} SendMetadataOptions * @property {Function} addSubjectMetadata - A function that records subject * metadata, bound to the requesting origin. + * @property {string} subjectType - The type of the requesting origin / subject. */ /** @@ -29,11 +31,23 @@ export default sendMetadata; * @param {Function} end - The json-rpc-engine 'end' callback. * @param {SendMetadataOptions} options */ -function sendMetadataHandler(req, res, _next, end, { addSubjectMetadata }) { +function sendMetadataHandler( + req, + res, + _next, + end, + { addSubjectMetadata, subjectType }, +) { const { params } = req; if (params && typeof params === 'object' && !Array.isArray(params)) { const { icon = null, name = null, ...remainingParams } = params; - addSubjectMetadata({ ...remainingParams, iconUrl: icon, name }); + + addSubjectMetadata({ + ...remainingParams, + iconUrl: icon, + name, + subjectType, + }); } else { return end(ethErrors.rpc.invalidParams({ data: params })); } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 0f3b4e138..8bd9d7422 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -58,7 +58,10 @@ import { import { UI_NOTIFICATIONS } from '../../shared/notifications'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { MILLISECOND } from '../../shared/constants/time'; -import { POLLING_TOKEN_ENVIRONMENT_TYPES } from '../../shared/constants/app'; +import { + POLLING_TOKEN_ENVIRONMENT_TYPES, + SUBJECT_TYPES, +} from '../../shared/constants/app'; import { hexToDecimal } from '../../ui/helpers/utils/conversions.util'; import ComposableObservableStore from './lib/ComposableObservableStore'; @@ -2634,7 +2637,12 @@ export default class MetamaskController extends EventEmitter { */ setupProviderConnection(outStream, sender, isInternal) { const origin = isInternal ? 'metamask' : new URL(sender.url).origin; + let subjectType = isInternal + ? SUBJECT_TYPES.INTERNAL + : SUBJECT_TYPES.WEBSITE; + if (sender.id !== this.extension.runtime.id) { + subjectType = SUBJECT_TYPES.EXTENSION; this.subjectMetadataController.addSubjectMetadata(origin, { extensionId: sender.id, }); @@ -2649,7 +2657,7 @@ export default class MetamaskController extends EventEmitter { origin, location: sender.url, tabId, - isInternal, + subjectType, }); // setup connection @@ -2672,14 +2680,15 @@ export default class MetamaskController extends EventEmitter { } /** - * A method for creating a provider that is safely restricted for the requesting domain. + * A method for creating a provider that is safely restricted for the requesting subject. + * * @param {Object} options - Provider engine options * @param {string} options.origin - The origin of the sender * @param {string} options.location - The full URL of the sender + * @param {string} options.subjectType - The type of the sender subject. * @param {tabId} [options.tabId] - The tab ID of the sender - if the sender is within a tab - * @param {boolean} [options.isInternal] - True if called for a connection to an internal process **/ - setupProviderEngine({ origin, location, tabId, isInternal = false }) { + setupProviderEngine({ origin, location, subjectType, tabId }) { // setup json rpc engine stack const engine = new JsonRpcEngine(); const { provider, blockTracker } = this; @@ -2715,6 +2724,8 @@ export default class MetamaskController extends EventEmitter { createMethodMiddleware({ origin, + subjectType, + // Miscellaneous addSubjectMetadata: this.subjectMetadataController.addSubjectMetadata.bind( this.subjectMetadataController, @@ -2796,7 +2807,7 @@ export default class MetamaskController extends EventEmitter { // filter and subscription polyfills engine.push(filterMiddleware); engine.push(subscriptionManager.middleware); - if (!isInternal) { + if (subjectType !== SUBJECT_TYPES.INTERNAL) { // permissions engine.push( this.permissionController.createPermissionMiddleware({ diff --git a/app/scripts/migrations/069.js b/app/scripts/migrations/069.js new file mode 100644 index 000000000..8635fb9cd --- /dev/null +++ b/app/scripts/migrations/069.js @@ -0,0 +1,41 @@ +import { cloneDeep } from 'lodash'; +import { SUBJECT_TYPES } from '../../../shared/constants/app'; + +const version = 69; + +/** + * Adds the `subjectType` property to all subject metadata. + */ +export default { + version, + async migrate(originalVersionedData) { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + const state = versionedData.data; + const newState = transformState(state); + versionedData.data = newState; + return versionedData; + }, +}; + +function transformState(state) { + if (typeof state?.SubjectMetadataController?.subjectMetadata === 'object') { + const { + SubjectMetadataController: { subjectMetadata }, + } = state; + + // mutate SubjectMetadataController.subjectMetadata in place + Object.values(subjectMetadata).forEach((metadata) => { + if ( + metadata && + typeof metadata === 'object' && + !Array.isArray(metadata) + ) { + metadata.subjectType = metadata.extensionId + ? SUBJECT_TYPES.EXTENSION + : SUBJECT_TYPES.WEBSITE; + } + }); + } + return state; +} diff --git a/app/scripts/migrations/069.test.js b/app/scripts/migrations/069.test.js new file mode 100644 index 000000000..8a830d693 --- /dev/null +++ b/app/scripts/migrations/069.test.js @@ -0,0 +1,102 @@ +import { SUBJECT_TYPES } from '../../../shared/constants/app'; +import migration69 from './069'; + +describe('migration #69', () => { + it('should update the version metadata', async () => { + const oldStorage = { + meta: { + version: 68, + }, + data: {}, + }; + + const newStorage = await migration69.migrate(oldStorage); + expect(newStorage.meta).toStrictEqual({ + version: 69, + }); + }); + + it('should migrate all data', async () => { + const oldStorage = { + meta: { + version: 68, + }, + data: { + FooController: { a: 'b' }, + SubjectMetadataController: { + subjectMetadata: { + 'https://1inch.exchange': { + iconUrl: + 'https://1inch.exchange/assets/favicon/favicon-32x32.png', + name: 'DEX Aggregator - 1inch.exchange', + origin: 'https://1inch.exchange', + extensionId: null, + }, + 'https://ascii-tree-generator.com': { + iconUrl: 'https://ascii-tree-generator.com/favicon.ico', + name: 'ASCII Tree Generator', + origin: 'https://ascii-tree-generator.com', + extensionId: 'ascii-tree-generator-extension', + }, + 'https://null.com': null, + 'https://foo.com': 'bad data', + 'https://bar.com': ['bad data'], + }, + }, + }, + }; + + const newStorage = await migration69.migrate(oldStorage); + expect(newStorage).toStrictEqual({ + meta: { + version: 69, + }, + data: { + FooController: { a: 'b' }, + SubjectMetadataController: { + subjectMetadata: { + 'https://1inch.exchange': { + iconUrl: + 'https://1inch.exchange/assets/favicon/favicon-32x32.png', + name: 'DEX Aggregator - 1inch.exchange', + origin: 'https://1inch.exchange', + extensionId: null, + subjectType: SUBJECT_TYPES.WEBSITE, + }, + 'https://ascii-tree-generator.com': { + iconUrl: 'https://ascii-tree-generator.com/favicon.ico', + name: 'ASCII Tree Generator', + origin: 'https://ascii-tree-generator.com', + extensionId: 'ascii-tree-generator-extension', + subjectType: SUBJECT_TYPES.EXTENSION, + }, + 'https://null.com': null, + 'https://foo.com': 'bad data', + 'https://bar.com': ['bad data'], + }, + }, + }, + }); + }); + + it('should handle missing SubjectMetadataController', async () => { + const oldStorage = { + meta: { + version: 68, + }, + data: { + FooController: { a: 'b' }, + }, + }; + + const newStorage = await migration69.migrate(oldStorage); + expect(newStorage).toStrictEqual({ + meta: { + version: 69, + }, + data: { + FooController: { a: 'b' }, + }, + }); + }); +}); diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index ece1046e6..384b3a306 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -72,6 +72,7 @@ import m065 from './065'; import m066 from './066'; import m067 from './067'; import m068 from './068'; +import m069 from './069'; const migrations = [ m002, @@ -141,6 +142,7 @@ const migrations = [ m066, m067, m068, + m069, ]; export default migrations; diff --git a/shared/constants/app.js b/shared/constants/app.js index d6f0a5093..6f81b22e3 100644 --- a/shared/constants/app.js +++ b/shared/constants/app.js @@ -45,6 +45,16 @@ export const MESSAGE_TYPE = { WATCH_ASSET_LEGACY: 'metamask_watchAsset', }; +/** + * The different kinds of subjects that MetaMask may interact with, including + * third parties and itself (e.g. when the background communicated with the UI). + */ +export const SUBJECT_TYPES = { + WEBSITE: 'website', + EXTENSION: 'extension', + INTERNAL: 'internal', +}; + export const POLLING_TOKEN_ENVIRONMENT_TYPES = { [ENVIRONMENT_TYPE_POPUP]: 'popupGasPollTokens', [ENVIRONMENT_TYPE_NOTIFICATION]: 'notificationGasPollTokens',