From bf299224885672bbe01756c17657fcf78d9598a5 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Mon, 20 Mar 2023 13:19:50 +0000 Subject: [PATCH] Use core message managers and create sign controller (#18163) --- app/scripts/background.js | 41 +- app/scripts/controllers/sign.test.ts | 609 ++++++++++++++++ app/scripts/controllers/sign.ts | 658 ++++++++++++++++++ app/scripts/lib/message-manager.js | 329 --------- app/scripts/lib/message-manager.test.js | 128 ---- app/scripts/lib/typed-message-manager.js | 421 ----------- app/scripts/lib/typed-message-manager.test.js | 126 ---- app/scripts/metamask-controller.js | 308 ++------ app/scripts/metamask-controller.test.js | 171 ----- jest.config.js | 2 + lavamoat/browserify/beta/policy.json | 41 +- lavamoat/browserify/desktop/policy.json | 41 +- lavamoat/browserify/flask/policy.json | 41 +- lavamoat/browserify/main/policy.json | 41 +- package.json | 2 +- types/eth-keyring-controller.d.ts | 9 + ui/store/actions.test.js | 4 + yarn.lock | 17 +- 18 files changed, 1496 insertions(+), 1493 deletions(-) create mode 100644 app/scripts/controllers/sign.test.ts create mode 100644 app/scripts/controllers/sign.ts delete mode 100644 app/scripts/lib/message-manager.js delete mode 100644 app/scripts/lib/message-manager.test.js delete mode 100644 app/scripts/lib/typed-message-manager.js delete mode 100644 app/scripts/lib/typed-message-manager.test.js create mode 100644 types/eth-keyring-controller.d.ts diff --git a/app/scripts/background.js b/app/scripts/background.js index b6d51f657..d36376e20 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -656,14 +656,6 @@ export function setupController(initState, initLangCode, overrides) { METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE, updateBadge, ); - controller.messageManager.on( - METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE, - updateBadge, - ); - controller.personalMessageManager.on( - METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE, - updateBadge, - ); controller.decryptMessageManager.on( METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE, updateBadge, @@ -672,7 +664,7 @@ export function setupController(initState, initLangCode, overrides) { METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE, updateBadge, ); - controller.typedMessageManager.on( + controller.signController.hub.on( METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE, updateBadge, ); @@ -708,23 +700,17 @@ export function setupController(initState, initLangCode, overrides) { function getUnapprovedTransactionCount() { const unapprovedTxCount = controller.txController.getUnapprovedTxCount(); - const { unapprovedMsgCount } = controller.messageManager; - const { unapprovedPersonalMsgCount } = controller.personalMessageManager; const { unapprovedDecryptMsgCount } = controller.decryptMessageManager; const { unapprovedEncryptionPublicKeyMsgCount } = controller.encryptionPublicKeyManager; - const { unapprovedTypedMessagesCount } = controller.typedMessageManager; const pendingApprovalCount = controller.approvalController.getTotalApprovalCount(); const waitingForUnlockCount = controller.appStateController.waitingForUnlock.length; return ( unapprovedTxCount + - unapprovedMsgCount + - unapprovedPersonalMsgCount + unapprovedDecryptMsgCount + unapprovedEncryptionPublicKeyMsgCount + - unapprovedTypedMessagesCount + pendingApprovalCount + waitingForUnlockCount ); @@ -747,30 +733,7 @@ export function setupController(initState, initLangCode, overrides) { ).forEach((txId) => controller.txController.txStateManager.setTxStatusRejected(txId), ); - controller.messageManager.messages - .filter((msg) => msg.status === 'unapproved') - .forEach((tx) => - controller.messageManager.rejectMsg( - tx.id, - REJECT_NOTFICIATION_CLOSE_SIG, - ), - ); - controller.personalMessageManager.messages - .filter((msg) => msg.status === 'unapproved') - .forEach((tx) => - controller.personalMessageManager.rejectMsg( - tx.id, - REJECT_NOTFICIATION_CLOSE_SIG, - ), - ); - controller.typedMessageManager.messages - .filter((msg) => msg.status === 'unapproved') - .forEach((tx) => - controller.typedMessageManager.rejectMsg( - tx.id, - REJECT_NOTFICIATION_CLOSE_SIG, - ), - ); + controller.signController.rejectUnapproved(REJECT_NOTFICIATION_CLOSE_SIG); controller.decryptMessageManager.messages .filter((msg) => msg.status === 'unapproved') .forEach((tx) => diff --git a/app/scripts/controllers/sign.test.ts b/app/scripts/controllers/sign.test.ts new file mode 100644 index 000000000..9363583fb --- /dev/null +++ b/app/scripts/controllers/sign.test.ts @@ -0,0 +1,609 @@ +import { + MessageManager, + PersonalMessageManager, + TypedMessageManager, +} from '@metamask/message-manager'; +import { + AbstractMessage, + OriginalRequest, +} from '@metamask/message-manager/dist/AbstractMessageManager'; +import { EVENT } from '../../../shared/constants/metametrics'; +import { detectSIWE } from '../../../shared/modules/siwe'; +import SignController, { + SignControllerMessenger, + SignControllerOptions, +} from './sign'; + +jest.mock('@metamask/message-manager', () => ({ + MessageManager: jest.fn(), + PersonalMessageManager: jest.fn(), + TypedMessageManager: jest.fn(), +})); + +jest.mock('../../../shared/modules/siwe', () => ({ + detectSIWE: jest.fn(), +})); + +const messageIdMock = '123'; +const messageIdMock2 = '456'; +const versionMock = '1'; +const signatureMock = '0xAABBCC'; +const stateMock = { test: 123 }; +const securityProviderResponseMock = { test2: 345 }; + +const messageParamsMock = { + from: '0x123', + origin: 'http://test.com', + data: '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', + metamaskId: messageIdMock, + version: 'V1', +}; + +const messageParamsMock2 = { + from: '0x124', + origin: 'http://test4.com', + data: '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA', + metamaskId: messageIdMock, +}; + +const messageMock = { + id: messageIdMock, + time: 123, + status: 'unapproved', + type: 'testType', + rawSig: undefined, +} as any as AbstractMessage; + +const coreMessageMock = { + ...messageMock, + messageParams: messageParamsMock, +}; + +const stateMessageMock = { + ...messageMock, + msgParams: messageParamsMock, + securityProviderResponse: securityProviderResponseMock, +}; + +const requestMock = { + origin: 'http://test2.com', +} as OriginalRequest; + +const siweMockFound = { + isSIWEMessage: true, + parsedMessage: { domain: 'test.com', test: 'value' }, +}; + +const siweMockNotFound = { isSIWEMessage: false }; + +const createMessengerMock = () => + ({ + registerActionHandler: jest.fn(), + publish: jest.fn(), + call: jest.fn(), + } as any as jest.Mocked); + +const createMessageManagerMock = () => + ({ + getUnapprovedMessages: jest.fn(), + getUnapprovedMessagesCount: jest.fn(), + addUnapprovedMessageAsync: jest.fn(), + approveMessage: jest.fn(), + setMessageStatusSigned: jest.fn(), + rejectMessage: jest.fn(), + subscribe: jest.fn(), + update: jest.fn(), + hub: { + on: jest.fn(), + }, + } as any as jest.Mocked); + +const createPreferencesControllerMock = () => ({ + store: { + getState: jest.fn(), + }, +}); + +const createKeyringControllerMock = () => ({ + signMessage: jest.fn(), + signPersonalMessage: jest.fn(), + signTypedMessage: jest.fn(), +}); + +describe('SignController', () => { + let signController: SignController; + + const messageManagerConstructorMock = MessageManager as jest.MockedClass< + typeof MessageManager + >; + const personalMessageManagerConstructorMock = + PersonalMessageManager as jest.MockedClass; + const typedMessageManagerConstructorMock = + TypedMessageManager as jest.MockedClass; + const messageManagerMock = createMessageManagerMock(); + const personalMessageManagerMock = + createMessageManagerMock(); + const typedMessageManagerMock = + createMessageManagerMock(); + const messengerMock = createMessengerMock(); + const preferencesControllerMock = createPreferencesControllerMock(); + const keyringControllerMock = createKeyringControllerMock(); + const detectSIWEMock = detectSIWE as jest.MockedFunction; + const getStateMock = jest.fn(); + const securityProviderRequestMock = jest.fn(); + const metricsEventMock = jest.fn(); + + beforeEach(() => { + jest.resetAllMocks(); + + messageManagerConstructorMock.mockReturnValue(messageManagerMock); + personalMessageManagerConstructorMock.mockReturnValue( + personalMessageManagerMock, + ); + + typedMessageManagerConstructorMock.mockReturnValue(typedMessageManagerMock); + + preferencesControllerMock.store.getState.mockReturnValue({ + disabledRpcMethodPreferences: { eth_sign: true }, + }); + + detectSIWEMock.mockReturnValue(siweMockNotFound); + + signController = new SignController({ + messenger: messengerMock as any, + preferencesController: preferencesControllerMock as any, + keyringController: keyringControllerMock as any, + getState: getStateMock as any, + securityProviderRequest: securityProviderRequestMock as any, + metricsEvent: metricsEventMock as any, + } as SignControllerOptions); + }); + + describe('unapprovedMsgCount', () => { + it('returns value from message manager getter', () => { + messageManagerMock.getUnapprovedMessagesCount.mockReturnValueOnce(10); + expect(signController.unapprovedMsgCount).toBe(10); + }); + }); + + describe('unapprovedPersonalMessagesCount', () => { + it('returns value from personal message manager getter', () => { + personalMessageManagerMock.getUnapprovedMessagesCount.mockReturnValueOnce( + 11, + ); + expect(signController.unapprovedPersonalMessagesCount).toBe(11); + }); + }); + + describe('unapprovedTypedMessagesCount', () => { + it('returns value from typed message manager getter', () => { + typedMessageManagerMock.getUnapprovedMessagesCount.mockReturnValueOnce( + 12, + ); + expect(signController.unapprovedTypedMessagesCount).toBe(12); + }); + }); + + describe('resetState', () => { + it('sets state to initial state', () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + signController.update(() => ({ + unapprovedMsgs: { [messageIdMock]: messageMock } as any, + unapprovedPersonalMsgs: { [messageIdMock]: messageMock } as any, + unapprovedTypedMessages: { [messageIdMock]: messageMock } as any, + unapprovedMsgCount: 1, + unapprovedPersonalMsgCount: 2, + unapprovedTypedMessagesCount: 3, + })); + + signController.resetState(); + + expect(signController.state).toEqual({ + unapprovedMsgs: {}, + unapprovedPersonalMsgs: {}, + unapprovedTypedMessages: {}, + unapprovedMsgCount: 0, + unapprovedPersonalMsgCount: 0, + unapprovedTypedMessagesCount: 0, + }); + }); + }); + + describe('rejectUnapproved', () => { + beforeEach(() => { + const messages = { + [messageIdMock]: messageMock, + [messageIdMock2]: messageMock, + }; + + messageManagerMock.getUnapprovedMessages.mockReturnValueOnce( + messages as any, + ); + personalMessageManagerMock.getUnapprovedMessages.mockReturnValueOnce( + messages as any, + ); + typedMessageManagerMock.getUnapprovedMessages.mockReturnValueOnce( + messages as any, + ); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + signController.update(() => ({ + unapprovedMsgs: messages as any, + unapprovedPersonalMsgs: messages as any, + unapprovedTypedMessages: messages as any, + })); + }); + + it('rejects all messages in all message managers', () => { + signController.rejectUnapproved('Test Reason'); + + expect(messageManagerMock.rejectMessage).toHaveBeenCalledTimes(2); + expect(messageManagerMock.rejectMessage).toHaveBeenCalledWith( + messageIdMock, + ); + expect(messageManagerMock.rejectMessage).toHaveBeenCalledWith( + messageIdMock2, + ); + + expect(personalMessageManagerMock.rejectMessage).toHaveBeenCalledTimes(2); + expect(personalMessageManagerMock.rejectMessage).toHaveBeenCalledWith( + messageIdMock, + ); + expect(personalMessageManagerMock.rejectMessage).toHaveBeenCalledWith( + messageIdMock2, + ); + + expect(typedMessageManagerMock.rejectMessage).toHaveBeenCalledTimes(2); + expect(typedMessageManagerMock.rejectMessage).toHaveBeenCalledWith( + messageIdMock, + ); + expect(typedMessageManagerMock.rejectMessage).toHaveBeenCalledWith( + messageIdMock2, + ); + }); + + it('fires metrics event with reject reason', () => { + signController.rejectUnapproved('Test Reason'); + + expect(metricsEventMock).toHaveBeenCalledTimes(6); + expect(metricsEventMock).toHaveBeenLastCalledWith({ + event: 'Test Reason', + category: EVENT.CATEGORIES.TRANSACTIONS, + properties: { + action: 'Sign Request', + type: messageMock.type, + }, + }); + }); + }); + + describe('clearUnapproved', () => { + it('resets state in all message managers', () => { + signController.clearUnapproved(); + + const defaultState = { + unapprovedMessages: {}, + unapprovedMessagesCount: 0, + }; + + expect(messageManagerMock.update).toHaveBeenCalledTimes(1); + expect(messageManagerMock.update).toHaveBeenCalledWith(defaultState); + + expect(personalMessageManagerMock.update).toHaveBeenCalledTimes(1); + expect(personalMessageManagerMock.update).toHaveBeenCalledWith( + defaultState, + ); + + expect(typedMessageManagerMock.update).toHaveBeenCalledTimes(1); + expect(typedMessageManagerMock.update).toHaveBeenCalledWith(defaultState); + }); + }); + + describe('newUnsignedMessage', () => { + it('throws if eth_sign disabled in preferences', async () => { + preferencesControllerMock.store.getState.mockReturnValueOnce({ + disabledRpcMethodPreferences: { eth_sign: false }, + }); + + await expect( + signController.newUnsignedMessage(messageParamsMock, requestMock), + ).rejects.toThrowError( + 'eth_sign has been disabled. You must enable it in the advanced settings', + ); + }); + + it('throws if data has wrong length', async () => { + await expect( + signController.newUnsignedMessage( + { ...messageParamsMock, data: '0xFF' }, + requestMock, + ), + ).rejects.toThrowError('eth_sign requires 32 byte message hash'); + }); + + it('adds message to message manager', async () => { + await signController.newUnsignedMessage(messageParamsMock, requestMock); + + expect( + messageManagerMock.addUnapprovedMessageAsync, + ).toHaveBeenCalledTimes(1); + expect(messageManagerMock.addUnapprovedMessageAsync).toHaveBeenCalledWith( + messageParamsMock, + requestMock, + ); + }); + }); + + describe('newUnsignedPersonalMessage', () => { + it('adds message to personal message manager', async () => { + await signController.newUnsignedPersonalMessage( + messageParamsMock, + requestMock, + ); + + expect( + personalMessageManagerMock.addUnapprovedMessageAsync, + ).toHaveBeenCalledTimes(1); + + expect( + personalMessageManagerMock.addUnapprovedMessageAsync, + ).toHaveBeenCalledWith( + expect.objectContaining(messageParamsMock), + requestMock, + ); + }); + + it('adds message to personal message manager including Ethereum sign in data', async () => { + detectSIWEMock.mockReturnValueOnce(siweMockFound); + + await signController.newUnsignedPersonalMessage( + messageParamsMock, + requestMock, + ); + + expect( + personalMessageManagerMock.addUnapprovedMessageAsync, + ).toHaveBeenCalledTimes(1); + + expect( + personalMessageManagerMock.addUnapprovedMessageAsync, + ).toHaveBeenCalledWith( + { + ...messageParamsMock, + siwe: siweMockFound, + }, + requestMock, + ); + }); + }); + + describe('newUnsignedTypedMessage', () => { + it('adds message to typed message manager', async () => { + signController.newUnsignedTypedMessage( + messageParamsMock, + requestMock, + versionMock, + ); + + expect( + typedMessageManagerMock.addUnapprovedMessageAsync, + ).toHaveBeenCalledTimes(1); + expect( + typedMessageManagerMock.addUnapprovedMessageAsync, + ).toHaveBeenCalledWith(messageParamsMock, versionMock, requestMock); + }); + }); + + describe.each([ + ['signMessage', messageManagerMock], + ['signPersonalMessage', personalMessageManagerMock], + ['signTypedMessage', typedMessageManagerMock], + ])('%s', (signMethodName, messageManager) => { + beforeEach(() => { + messageManager.approveMessage.mockResolvedValueOnce(messageParamsMock2); + + keyringControllerMock[signMethodName].mockResolvedValueOnce( + signatureMock, + ); + }); + + it('approves message and signs', async () => { + await signController[signMethodName](messageParamsMock); + + const keyringControllerExtraArgs = + signMethodName === 'signTypedMessage' + ? [{ version: messageParamsMock.version }] + : []; + + expect(keyringControllerMock[signMethodName]).toHaveBeenCalledTimes(1); + expect(keyringControllerMock[signMethodName]).toHaveBeenCalledWith( + messageParamsMock2, + ...keyringControllerExtraArgs, + ); + + expect(messageManager.setMessageStatusSigned).toHaveBeenCalledTimes(1); + expect(messageManager.setMessageStatusSigned).toHaveBeenCalledWith( + messageParamsMock2.metamaskId, + signatureMock, + ); + }); + + it('returns current state', async () => { + getStateMock.mockReturnValueOnce(stateMock); + expect(await signController[signMethodName](messageParamsMock)).toEqual( + stateMock, + ); + }); + + it('accepts approval', async () => { + await signController[signMethodName](messageParamsMock); + + expect(messengerMock.call).toHaveBeenCalledTimes(1); + expect(messengerMock.call).toHaveBeenCalledWith( + 'ApprovalController:acceptRequest', + messageParamsMock.metamaskId, + ); + }); + + it('rejects message on error', async () => { + keyringControllerMock[signMethodName].mockReset(); + keyringControllerMock[signMethodName].mockRejectedValue( + new Error('Test Error'), + ); + + await expect( + signController[signMethodName](messageParamsMock), + ).rejects.toThrow('Test Error'); + + expect(messageManager.rejectMessage).toHaveBeenCalledTimes(1); + expect(messageManager.rejectMessage).toHaveBeenCalledWith( + messageParamsMock.metamaskId, + ); + }); + + it('rejects approval on error', async () => { + keyringControllerMock[signMethodName].mockReset(); + keyringControllerMock[signMethodName].mockRejectedValue( + new Error('Test Error'), + ); + + await expect( + signController[signMethodName](messageParamsMock), + ).rejects.toThrow('Test Error'); + + expect(messengerMock.call).toHaveBeenCalledTimes(1); + expect(messengerMock.call).toHaveBeenCalledWith( + 'ApprovalController:rejectRequest', + messageParamsMock.metamaskId, + 'Cancel', + ); + }); + }); + + describe.each([ + ['cancelMessage', messageManagerMock], + ['cancelPersonalMessage', personalMessageManagerMock], + ['cancelTypedMessage', typedMessageManagerMock], + ])('%s', (cancelMethodName, messageManager) => { + it('rejects message using message manager', async () => { + signController[cancelMethodName](messageIdMock); + + expect(messageManager.rejectMessage).toHaveBeenCalledTimes(1); + expect(messageManager.rejectMessage).toHaveBeenCalledWith( + messageParamsMock.metamaskId, + ); + }); + + it('rejects approval using approval controller', async () => { + signController[cancelMethodName](messageIdMock); + + expect(messengerMock.call).toHaveBeenCalledTimes(1); + expect(messengerMock.call).toHaveBeenCalledWith( + 'ApprovalController:rejectRequest', + messageParamsMock.metamaskId, + 'Cancel', + ); + }); + }); + + describe('message manager events', () => { + it.each([ + ['message manager', messageManagerMock], + ['personal message manager', personalMessageManagerMock], + ['typed message manager', typedMessageManagerMock], + ])('bubbles update badge event from %s', (_, messageManager) => { + const mockListener = jest.fn(); + + signController.hub.on('updateBadge', mockListener); + messageManager.hub.on.mock.calls[0][1](); + + expect(mockListener).toHaveBeenCalledTimes(1); + }); + + it.each([ + ['message manager', messageManagerMock, 'eth_sign'], + ['personal message manager', personalMessageManagerMock, 'personal_sign'], + ['typed message manager', typedMessageManagerMock, 'eth_signTypedData'], + ])( + 'requires approval on unapproved message event from %s', + (_, messageManager, methodName) => { + messengerMock.call.mockResolvedValueOnce({}); + + messageManager.hub.on.mock.calls[1][1](messageParamsMock); + + expect(messengerMock.call).toHaveBeenCalledTimes(1); + expect(messengerMock.call).toHaveBeenCalledWith( + 'ApprovalController:addRequest', + { + id: messageIdMock, + origin: messageParamsMock.origin, + type: methodName, + }, + true, + ); + }, + ); + + it('updates state on message manager state change', async () => { + securityProviderRequestMock.mockResolvedValue( + securityProviderResponseMock, + ); + + await messageManagerMock.subscribe.mock.calls[0][0]({ + unapprovedMessages: { [messageIdMock]: coreMessageMock as any }, + unapprovedMessagesCount: 3, + }); + + expect(await signController.state).toEqual({ + unapprovedMsgs: { [messageIdMock]: stateMessageMock as any }, + unapprovedPersonalMsgs: {}, + unapprovedTypedMessages: {}, + unapprovedMsgCount: 3, + unapprovedPersonalMsgCount: 0, + unapprovedTypedMessagesCount: 0, + }); + }); + + it('updates state on personal message manager state change', async () => { + securityProviderRequestMock.mockResolvedValue( + securityProviderResponseMock, + ); + + await personalMessageManagerMock.subscribe.mock.calls[0][0]({ + unapprovedMessages: { [messageIdMock]: coreMessageMock as any }, + unapprovedMessagesCount: 4, + }); + + expect(await signController.state).toEqual({ + unapprovedMsgs: {}, + unapprovedPersonalMsgs: { [messageIdMock]: stateMessageMock as any }, + unapprovedTypedMessages: {}, + unapprovedMsgCount: 0, + unapprovedPersonalMsgCount: 4, + unapprovedTypedMessagesCount: 0, + }); + }); + + it('updates state on typed message manager state change', async () => { + securityProviderRequestMock.mockResolvedValue( + securityProviderResponseMock, + ); + + await typedMessageManagerMock.subscribe.mock.calls[0][0]({ + unapprovedMessages: { [messageIdMock]: coreMessageMock as any }, + unapprovedMessagesCount: 5, + }); + + expect(await signController.state).toEqual({ + unapprovedMsgs: {}, + unapprovedPersonalMsgs: {}, + unapprovedTypedMessages: { [messageIdMock]: stateMessageMock as any }, + unapprovedMsgCount: 0, + unapprovedPersonalMsgCount: 0, + unapprovedTypedMessagesCount: 5, + }); + }); + }); +}); diff --git a/app/scripts/controllers/sign.ts b/app/scripts/controllers/sign.ts new file mode 100644 index 000000000..13157d502 --- /dev/null +++ b/app/scripts/controllers/sign.ts @@ -0,0 +1,658 @@ +import EventEmitter from 'events'; +import log from 'loglevel'; +import { + MessageManager, + MessageParams, + MessageParamsMetamask, + PersonalMessageManager, + PersonalMessageParams, + PersonalMessageParamsMetamask, + TypedMessageManager, + TypedMessageParams, + TypedMessageParamsMetamask, +} from '@metamask/message-manager'; +import { ethErrors } from 'eth-rpc-errors'; +import { bufferToHex } from 'ethereumjs-util'; +import { KeyringController } from '@metamask/eth-keyring-controller'; +import { + AbstractMessageManager, + AbstractMessage, + MessageManagerState, + AbstractMessageParams, + AbstractMessageParamsMetamask, + OriginalRequest, +} from '@metamask/message-manager/dist/AbstractMessageManager'; +import { + BaseControllerV2, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; +import { Patch } from 'immer'; +import { + AcceptRequest, + AddApprovalRequest, + RejectRequest, +} from '@metamask/approval-controller'; +import { EVENT } from '../../../shared/constants/metametrics'; +import { detectSIWE } from '../../../shared/modules/siwe'; +import PreferencesController from './preferences'; + +const controllerName = 'SignController'; +const methodNameSign = 'eth_sign'; +const methodNamePersonalSign = 'personal_sign'; +const methodNameTypedSign = 'eth_signTypedData'; + +const stateMetadata = { + unapprovedMsgs: { persist: false, anonymous: false }, + unapprovedPersonalMsgs: { persist: false, anonymous: false }, + unapprovedTypedMessages: { persist: false, anonymous: false }, + unapprovedMsgCount: { persist: false, anonymous: false }, + unapprovedPersonalMsgCount: { persist: false, anonymous: false }, + unapprovedTypedMessagesCount: { persist: false, anonymous: false }, +}; + +const getDefaultState = () => ({ + unapprovedMsgs: {}, + unapprovedPersonalMsgs: {}, + unapprovedTypedMessages: {}, + unapprovedMsgCount: 0, + unapprovedPersonalMsgCount: 0, + unapprovedTypedMessagesCount: 0, +}); + +export type CoreMessage = AbstractMessage & { + messageParams: AbstractMessageParams; +}; + +export type StateMessage = Required & { + msgParams: Required; + securityProviderResponse: any; +}; + +export type SignControllerState = { + unapprovedMsgs: Record; + unapprovedPersonalMsgs: Record; + unapprovedTypedMessages: Record; + unapprovedMsgCount: number; + unapprovedPersonalMsgCount: number; + unapprovedTypedMessagesCount: number; +}; + +export type GetSignState = { + type: `${typeof controllerName}:getState`; + handler: () => SignControllerState; +}; + +export type SignStateChange = { + type: `${typeof controllerName}:stateChange`; + payload: [SignControllerState, Patch[]]; +}; + +export type SignControllerActions = GetSignState; + +export type SignControllerEvents = SignStateChange; + +type AllowedActions = AddApprovalRequest | AcceptRequest | RejectRequest; + +export type SignControllerMessenger = RestrictedControllerMessenger< + typeof controllerName, + SignControllerActions | AllowedActions, + SignControllerEvents, + AllowedActions['type'], + never +>; + +export type SignControllerOptions = { + messenger: SignControllerMessenger; + keyringController: KeyringController; + preferencesController: PreferencesController; + sendUpdate: () => void; + getState: () => any; + metricsEvent: (payload: any, options?: any) => void; + securityProviderRequest: ( + requestData: any, + methodName: string, + ) => Promise; +}; + +/** + * Controller for creating signing requests requiring user approval. + */ +export default class SignController extends BaseControllerV2< + typeof controllerName, + SignControllerState, + SignControllerMessenger +> { + hub: EventEmitter; + + private _keyringController: KeyringController; + + private _preferencesController: PreferencesController; + + private _getState: () => any; + + private _messageManager: MessageManager; + + private _personalMessageManager: PersonalMessageManager; + + private _typedMessageManager: TypedMessageManager; + + private _messageManagers: AbstractMessageManager< + AbstractMessage, + AbstractMessageParams, + AbstractMessageParamsMetamask + >[]; + + private _metricsEvent: (payload: any, options?: any) => void; + + private _securityProviderRequest: ( + requestData: any, + methodName: string, + ) => Promise; + + /** + * Construct a Sign controller. + * + * @param options - The controller options. + * @param options.messenger - The restricted controller messenger for the sign controller. + * @param options.keyringController - An instance of a keyring controller used to perform the signing operations. + * @param options.preferencesController - An instance of a preferences controller to limit operations based on user configuration. + * @param options.getState - Callback to retrieve all user state. + * @param options.metricsEvent - A function for emitting a metric event. + * @param options.securityProviderRequest - A function for verifying a message, whether it is malicious or not. + */ + constructor({ + messenger, + keyringController, + preferencesController, + getState, + metricsEvent, + securityProviderRequest, + }: SignControllerOptions) { + super({ + name: controllerName, + metadata: stateMetadata, + messenger, + state: getDefaultState(), + }); + + this._keyringController = keyringController; + this._preferencesController = preferencesController; + this._getState = getState; + this._metricsEvent = metricsEvent; + this._securityProviderRequest = securityProviderRequest; + + this.hub = new EventEmitter(); + this._messageManager = new MessageManager(); + this._personalMessageManager = new PersonalMessageManager(); + this._typedMessageManager = new TypedMessageManager(); + + this._messageManagers = [ + this._messageManager, + this._personalMessageManager, + this._typedMessageManager, + ]; + + const methodNames = [ + methodNameSign, + methodNamePersonalSign, + methodNameTypedSign, + ]; + + this._messageManagers.forEach((messageManager, index) => { + this._bubbleEvents(messageManager); + + messageManager.hub.on( + 'unapprovedMessage', + (msgParams: AbstractMessageParamsMetamask) => { + this._requestApproval(msgParams, methodNames[index]); + }, + ); + }); + + this._subscribeToMessageState( + this._messageManager, + (state, newMessages, messageCount) => { + state.unapprovedMsgs = newMessages; + state.unapprovedMsgCount = messageCount; + }, + ); + + this._subscribeToMessageState( + this._personalMessageManager, + (state, newMessages, messageCount) => { + state.unapprovedPersonalMsgs = newMessages; + state.unapprovedPersonalMsgCount = messageCount; + }, + ); + + this._subscribeToMessageState( + this._typedMessageManager, + (state, newMessages, messageCount) => { + state.unapprovedTypedMessages = newMessages; + state.unapprovedTypedMessagesCount = messageCount; + }, + ); + } + + /** + * A getter for the number of 'unapproved' Messages in this.messages + * + * @returns The number of 'unapproved' Messages in this.messages + */ + get unapprovedMsgCount(): number { + return this._messageManager.getUnapprovedMessagesCount(); + } + + /** + * A getter for the number of 'unapproved' PersonalMessages in this.messages + * + * @returns The number of 'unapproved' PersonalMessages in this.messages + */ + get unapprovedPersonalMessagesCount(): number { + return this._personalMessageManager.getUnapprovedMessagesCount(); + } + + /** + * A getter for the number of 'unapproved' TypedMessages in this.messages + * + * @returns The number of 'unapproved' TypedMessages in this.messages + */ + get unapprovedTypedMessagesCount(): number { + return this._typedMessageManager.getUnapprovedMessagesCount(); + } + + /** + * Reset the controller state to the initial state. + */ + resetState() { + this.update(() => getDefaultState()); + } + + /** + * Called when a Dapp uses the eth_sign method, to request user approval. + * eth_sign is a pure signature of arbitrary data. It is on a deprecation + * path, since this data can be a transaction, or can leak private key + * information. + * + * @param msgParams - The params passed to eth_sign. + * @param [req] - The original request, containing the origin. + */ + async newUnsignedMessage( + msgParams: MessageParams, + req: OriginalRequest, + ): Promise { + const { + // eslint-disable-next-line camelcase + disabledRpcMethodPreferences: { eth_sign }, + } = this._preferencesController.store.getState() as any; + + // eslint-disable-next-line camelcase + if (!eth_sign) { + throw ethErrors.rpc.methodNotFound( + 'eth_sign has been disabled. You must enable it in the advanced settings', + ); + } + + const data = this._normalizeMsgData(msgParams.data); + + // 64 hex + "0x" at the beginning + // This is needed because Ethereum's EcSign works only on 32 byte numbers + // For 67 length see: https://github.com/MetaMask/metamask-extension/pull/12679/files#r749479607 + if (data.length !== 66 && data.length !== 67) { + throw ethErrors.rpc.invalidParams( + 'eth_sign requires 32 byte message hash', + ); + } + + return this._messageManager.addUnapprovedMessageAsync(msgParams, req); + } + + /** + * Called when a dapp uses the personal_sign method. + * This is identical to the Geth eth_sign method, and may eventually replace + * eth_sign. + * + * We currently define our eth_sign and personal_sign mostly for legacy Dapps. + * + * @param msgParams - The params of the message to sign & return to the Dapp. + * @param req - The original request, containing the origin. + */ + async newUnsignedPersonalMessage( + msgParams: PersonalMessageParams, + req: OriginalRequest, + ): Promise { + const ethereumSignInData = this._getEthereumSignInData(msgParams); + const finalMsgParams = { ...msgParams, siwe: ethereumSignInData }; + + return this._personalMessageManager.addUnapprovedMessageAsync( + finalMsgParams, + req, + ); + } + + /** + * Called when a dapp uses the eth_signTypedData method, per EIP 712. + * + * @param msgParams - The params passed to eth_signTypedData. + * @param req - The original request, containing the origin. + * @param version + */ + async newUnsignedTypedMessage( + msgParams: TypedMessageParams, + req: OriginalRequest, + version: string, + ): Promise { + return this._typedMessageManager.addUnapprovedMessageAsync( + msgParams, + version, + req, + ); + } + + /** + * Signifies user intent to complete an eth_sign method. + * + * @param msgParams - The params passed to eth_call. + * @returns Full state update. + */ + async signMessage(msgParams: MessageParamsMetamask) { + return await this._signAbstractMessage( + this._messageManager, + methodNameSign, + msgParams, + async (cleanMsgParams) => + await this._keyringController.signMessage(cleanMsgParams), + ); + } + + /** + * Signifies a user's approval to sign a personal_sign message in queue. + * Triggers signing, and the callback function from newUnsignedPersonalMessage. + * + * @param msgParams - The params of the message to sign & return to the Dapp. + * @returns A full state update. + */ + async signPersonalMessage(msgParams: PersonalMessageParamsMetamask) { + return await this._signAbstractMessage( + this._personalMessageManager, + methodNamePersonalSign, + msgParams, + async (cleanMsgParams) => + await this._keyringController.signPersonalMessage(cleanMsgParams), + ); + } + + /** + * The method for a user approving a call to eth_signTypedData, per EIP 712. + * Triggers the callback in newUnsignedTypedMessage. + * + * @param msgParams - The params passed to eth_signTypedData. + * @returns Full state update. + */ + async signTypedMessage(msgParams: TypedMessageParamsMetamask) { + const { version } = msgParams; + + return await this._signAbstractMessage( + this._typedMessageManager, + methodNameTypedSign, + msgParams, + async (cleanMsgParams) => { + // For some reason every version after V1 used stringified params. + if (version !== 'V1') { + // But we don't have to require that. We can stop suggesting it now: + if (typeof cleanMsgParams.data === 'string') { + cleanMsgParams.data = JSON.parse(cleanMsgParams.data); + } + } + + return await this._keyringController.signTypedMessage(cleanMsgParams, { + version, + }); + }, + ); + } + + /** + * Used to cancel a message submitted via eth_sign. + * + * @param msgId - The id of the message to cancel. + */ + cancelMessage(msgId: string) { + this._cancelAbstractMessage(this._messageManager, msgId); + } + + /** + * Used to cancel a personal_sign type message. + * + * @param msgId - The ID of the message to cancel. + */ + cancelPersonalMessage(msgId: string) { + this._cancelAbstractMessage(this._personalMessageManager, msgId); + } + + /** + * Used to cancel a eth_signTypedData type message. + * + * @param msgId - The ID of the message to cancel. + */ + cancelTypedMessage(msgId: string) { + this._cancelAbstractMessage(this._typedMessageManager, msgId); + } + + /** + * Reject all unapproved messages of any type. + * + * @param reason - A message to indicate why. + */ + rejectUnapproved(reason?: string) { + this._messageManagers.forEach((messageManager) => { + Object.keys(messageManager.getUnapprovedMessages()).forEach( + (messageId) => { + this._cancelAbstractMessage(messageManager, messageId, reason); + }, + ); + }); + } + + /** + * Clears all unapproved messages from memory. + */ + clearUnapproved() { + this._messageManagers.forEach((messageManager) => { + messageManager.update({ + unapprovedMessages: {}, + unapprovedMessagesCount: 0, + }); + }); + } + + private async _signAbstractMessage

( + messageManager: AbstractMessageManager< + AbstractMessage, + P, + AbstractMessageParamsMetamask + >, + methodName: string, + msgParams: AbstractMessageParamsMetamask, + getSignature: (cleanMessageParams: P) => Promise, + ) { + log.info(`MetaMaskController - ${methodName}`); + + const messageId = msgParams.metamaskId as string; + + try { + const cleanMessageParams = await messageManager.approveMessage(msgParams); + const signature = await getSignature(cleanMessageParams); + + messageManager.setMessageStatusSigned(messageId, signature); + + this._acceptApproval(messageId); + + return this._getState(); + } catch (error) { + log.info(`MetaMaskController - ${methodName} failed.`, error); + this._cancelAbstractMessage(messageManager, messageId); + throw error; + } + } + + private _cancelAbstractMessage( + messageManager: AbstractMessageManager< + AbstractMessage, + AbstractMessageParams, + AbstractMessageParamsMetamask + >, + messageId: string, + reason?: string, + ) { + if (reason) { + const message = this._getMessage(messageId); + + this._metricsEvent({ + event: reason, + category: EVENT.CATEGORIES.TRANSACTIONS, + properties: { + action: 'Sign Request', + type: message.type, + }, + }); + } + + messageManager.rejectMessage(messageId); + this._rejectApproval(messageId); + + return this._getState(); + } + + private _bubbleEvents( + messageManager: AbstractMessageManager< + AbstractMessage, + AbstractMessageParams, + AbstractMessageParamsMetamask + >, + ) { + messageManager.hub.on('updateBadge', () => { + this.hub.emit('updateBadge'); + }); + } + + private _subscribeToMessageState( + messageManager: AbstractMessageManager< + AbstractMessage, + AbstractMessageParams, + AbstractMessageParamsMetamask + >, + updateState: ( + state: SignControllerState, + newMessages: Record, + messageCount: number, + ) => void, + ) { + messageManager.subscribe( + async (state: MessageManagerState) => { + const newMessages = await this._migrateMessages( + state.unapprovedMessages as any, + ); + + this.update((draftState) => { + updateState(draftState, newMessages, state.unapprovedMessagesCount); + }); + }, + ); + } + + private async _migrateMessages( + coreMessages: Record, + ): Promise> { + const stateMessages: Record = {}; + + for (const messageId of Object.keys(coreMessages)) { + const coreMessage = coreMessages[messageId]; + const stateMessage = await this._migrateMessage(coreMessage); + + stateMessages[messageId] = stateMessage; + } + + return stateMessages; + } + + private async _migrateMessage( + coreMessage: CoreMessage, + ): Promise { + const { messageParams, ...coreMessageData } = coreMessage; + + // Core message managers use messageParams but frontend uses msgParams with lots of references + const stateMessage = { + ...coreMessageData, + rawSig: coreMessage.rawSig as string, + msgParams: { + ...messageParams, + origin: messageParams.origin as string, + }, + }; + + const messageId = coreMessage.id; + const existingMessage = this._getMessage(messageId); + + const securityProviderResponse = existingMessage + ? existingMessage.securityProviderResponse + : await this._securityProviderRequest(stateMessage, stateMessage.type); + + return { ...stateMessage, securityProviderResponse }; + } + + private _normalizeMsgData(data: string) { + if (data.slice(0, 2) === '0x') { + // data is already hex + return data; + } + // data is unicode, convert to hex + return bufferToHex(Buffer.from(data, 'utf8')); + } + + private _getMessage(messageId: string): StateMessage { + return { + ...this.state.unapprovedMsgs, + ...this.state.unapprovedPersonalMsgs, + ...this.state.unapprovedTypedMessages, + }[messageId]; + } + + private _getEthereumSignInData(messgeParams: PersonalMessageParams): any { + return detectSIWE(messgeParams); + } + + private _requestApproval( + msgParams: AbstractMessageParamsMetamask, + type: string, + ) { + const id = msgParams.metamaskId as string; + const origin = msgParams.origin || controllerName; + + this.messagingSystem + .call( + 'ApprovalController:addRequest', + { + id, + origin, + type, + }, + true, + ) + .catch(() => { + // Intentionally ignored as promise not currently used + }); + } + + private _acceptApproval(messageId: string) { + this.messagingSystem.call('ApprovalController:acceptRequest', messageId); + } + + private _rejectApproval(messageId: string) { + this.messagingSystem.call( + 'ApprovalController:rejectRequest', + messageId, + 'Cancel', + ); + } +} diff --git a/app/scripts/lib/message-manager.js b/app/scripts/lib/message-manager.js deleted file mode 100644 index b070e65a0..000000000 --- a/app/scripts/lib/message-manager.js +++ /dev/null @@ -1,329 +0,0 @@ -import EventEmitter from 'events'; -import { ObservableStore } from '@metamask/obs-store'; -import { bufferToHex } from 'ethereumjs-util'; -import { ethErrors } from 'eth-rpc-errors'; -import { MESSAGE_TYPE } from '../../../shared/constants/app'; -import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; -import createId from '../../../shared/modules/random-id'; -import { EVENT } from '../../../shared/constants/metametrics'; - -/** - * Represents, and contains data about, an 'eth_sign' type signature request. These are created when a signature for - * an eth_sign call is requested. - * - * @see {@link https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign} - * @typedef {object} Message - * @property {number} id An id to track and identify the message object - * @property {object} msgParams The parameters to pass to the eth_sign method once the signature request is approved. - * @property {object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask. - * @property {string} msgParams.data A hex string conversion of the raw buffer data of the signature request - * @property {number} time The epoch time at which the this message was created - * @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed' or 'rejected' - * @property {string} type The json-prc signing method for which a signature request has been made. A 'Message' with - * always have a 'eth_sign' type. - */ - -export default class MessageManager extends EventEmitter { - /** - * Controller in charge of managing - storing, adding, removing, updating - Messages. - * - * @param {object} opts - Controller options - * @param {Function} opts.metricsEvent - A function for emitting a metric event. - * @param {Function} opts.securityProviderRequest - A function for verifying a message, whether it is malicious or not. - */ - constructor({ metricsEvent, securityProviderRequest }) { - super(); - this.memStore = new ObservableStore({ - unapprovedMsgs: {}, - unapprovedMsgCount: 0, - }); - - this.resetState = () => { - this.memStore.updateState({ - unapprovedMsgs: {}, - unapprovedMsgCount: 0, - }); - }; - - this.messages = []; - this.metricsEvent = metricsEvent; - this.securityProviderRequest = securityProviderRequest; - } - - /** - * A getter for the number of 'unapproved' Messages in this.messages - * - * @returns {number} The number of 'unapproved' Messages in this.messages - */ - get unapprovedMsgCount() { - return Object.keys(this.getUnapprovedMsgs()).length; - } - - /** - * A getter for the 'unapproved' Messages in this.messages - * - * @returns {object} An index of Message ids to Messages, for all 'unapproved' Messages in this.messages - */ - getUnapprovedMsgs() { - return this.messages - .filter((msg) => msg.status === 'unapproved') - .reduce((result, msg) => { - result[msg.id] = msg; - return result; - }, {}); - } - - /** - * Creates a new Message with an 'unapproved' status using the passed msgParams. this.addMsg is called to add the - * new Message to this.messages, and to save the unapproved Messages from that list to this.memStore. - * - * @param {object} msgParams - The params for the eth_sign call to be made after the message is approved. - * @param {object} [req] - The original request object possibly containing the origin - * @returns {promise} after signature has been - */ - async addUnapprovedMessageAsync(msgParams, req) { - const msgId = await this.addUnapprovedMessage(msgParams, req); - return await new Promise((resolve, reject) => { - // await finished - this.once(`${msgId}:finished`, (data) => { - switch (data.status) { - case 'signed': - return resolve(data.rawSig); - case 'rejected': - return reject( - ethErrors.provider.userRejectedRequest( - 'MetaMask Message Signature: User denied message signature.', - ), - ); - case 'errored': - return reject( - new Error(`MetaMask Message Signature: ${data.error}`), - ); - default: - return reject( - new Error( - `MetaMask Message Signature: Unknown problem: ${JSON.stringify( - msgParams, - )}`, - ), - ); - } - }); - }); - } - - /** - * Creates a new Message with an 'unapproved' status using the passed msgParams. this.addMsg is called to add the - * new Message to this.messages, and to save the unapproved Messages from that list to this.memStore. - * - * @param {object} msgParams - The params for the eth_sign call to be made after the message is approved. - * @param {object} [req] - The original request object where the origin may be specified - * @returns {number} The id of the newly created message. - */ - async addUnapprovedMessage(msgParams, req) { - // add origin from request - if (req) { - msgParams.origin = req.origin; - } - msgParams.data = normalizeMsgData(msgParams.data); - // create txData obj with parameters and meta data - const time = new Date().getTime(); - const msgId = createId(); - const msgData = { - id: msgId, - msgParams, - time, - status: 'unapproved', - type: MESSAGE_TYPE.ETH_SIGN, - }; - this.addMsg(msgData); - - const securityProviderResponse = await this.securityProviderRequest( - msgData, - msgData.type, - ); - - msgData.securityProviderResponse = securityProviderResponse; - - // signal update - this.emit('update'); - return msgId; - } - - /** - * Adds a passed Message to this.messages, and calls this._saveMsgList() to save the unapproved Messages from that - * list to this.memStore. - * - * @param {Message} msg - The Message to add to this.messages - */ - addMsg(msg) { - this.messages.push(msg); - this._saveMsgList(); - } - - /** - * Returns a specified Message. - * - * @param {number} msgId - The id of the Message to get - * @returns {Message|undefined} The Message with the id that matches the passed msgId, or undefined if no Message has that id. - */ - getMsg(msgId) { - return this.messages.find((msg) => msg.id === msgId); - } - - /** - * Approves a Message. Sets the message status via a call to this.setMsgStatusApproved, and returns a promise with - * any the message params modified for proper signing. - * - * @param {object} msgParams - The msgParams to be used when eth_sign is called, plus data added by MetaMask. - * @param {object} msgParams.metamaskId - Added to msgParams for tracking and identification within MetaMask. - * @returns {Promise} Promises the msgParams object with metamaskId removed. - */ - approveMessage(msgParams) { - this.setMsgStatusApproved(msgParams.metamaskId); - return this.prepMsgForSigning(msgParams); - } - - /** - * Sets a Message status to 'approved' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the Message to approve. - */ - setMsgStatusApproved(msgId) { - this._setMsgStatus(msgId, 'approved'); - } - - /** - * Sets a Message status to 'signed' via a call to this._setMsgStatus and updates that Message in this.messages by - * adding the raw signature data of the signature request to the Message - * - * @param {number} msgId - The id of the Message to sign. - * @param {buffer} rawSig - The raw data of the signature request - */ - setMsgStatusSigned(msgId, rawSig) { - const msg = this.getMsg(msgId); - msg.rawSig = rawSig; - this._updateMsg(msg); - this._setMsgStatus(msgId, 'signed'); - } - - /** - * Removes the metamaskId property from passed msgParams and returns a promise which resolves the updated msgParams - * - * @param {object} msgParams - The msgParams to modify - * @returns {Promise} Promises the msgParams with the metamaskId property removed - */ - async prepMsgForSigning(msgParams) { - delete msgParams.metamaskId; - return msgParams; - } - - /** - * Sets a Message status to 'rejected' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the Message to reject. - * @param reason - */ - rejectMsg(msgId, reason = undefined) { - if (reason) { - const msg = this.getMsg(msgId); - this.metricsEvent({ - event: reason, - category: EVENT.CATEGORIES.TRANSACTIONS, - properties: { - action: 'Sign Request', - type: msg.type, - }, - }); - } - this._setMsgStatus(msgId, 'rejected'); - } - - /** - * Sets a Message status to 'errored' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the Message to error - * @param error - */ - errorMessage(msgId, error) { - const msg = this.getMsg(msgId); - msg.error = error; - this._updateMsg(msg); - this._setMsgStatus(msgId, 'errored'); - } - - /** - * Clears all unapproved messages from memory. - */ - clearUnapproved() { - this.messages = this.messages.filter((msg) => msg.status !== 'unapproved'); - this._saveMsgList(); - } - - /** - * Updates the status of a Message in this.messages via a call to this._updateMsg - * - * @private - * @param {number} msgId - The id of the Message to update. - * @param {string} status - The new status of the Message. - * @throws A 'MessageManager - Message not found for id: "${msgId}".' if there is no Message in this.messages with an - * id equal to the passed msgId - * @fires An event with a name equal to `${msgId}:${status}`. The Message is also fired. - * @fires If status is 'rejected' or 'signed', an event with a name equal to `${msgId}:finished` is fired along with the message - */ - _setMsgStatus(msgId, status) { - const msg = this.getMsg(msgId); - if (!msg) { - throw new Error(`MessageManager - Message not found for id: "${msgId}".`); - } - msg.status = status; - this._updateMsg(msg); - this.emit(`${msgId}:${status}`, msg); - if (status === 'rejected' || status === 'signed') { - this.emit(`${msgId}:finished`, msg); - } - } - - /** - * Sets a Message in this.messages to the passed Message if the ids are equal. Then saves the unapprovedMsg list to - * storage via this._saveMsgList - * - * @private - * @param {Message} msg - A Message that will replace an existing Message (with the same id) in this.messages - */ - _updateMsg(msg) { - const index = this.messages.findIndex((message) => message.id === msg.id); - if (index !== -1) { - this.messages[index] = msg; - } - this._saveMsgList(); - } - - /** - * Saves the unapproved messages, and their count, to this.memStore - * - * @private - * @fires 'updateBadge' - */ - _saveMsgList() { - const unapprovedMsgs = this.getUnapprovedMsgs(); - const unapprovedMsgCount = Object.keys(unapprovedMsgs).length; - this.memStore.updateState({ unapprovedMsgs, unapprovedMsgCount }); - this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); - } -} - -/** - * A helper function that converts raw buffer data to a hex, or just returns the data if it is already formatted as a hex. - * - * @param {any} data - The buffer data to convert to a hex - * @returns {string} A hex string conversion of the buffer data - */ -export function normalizeMsgData(data) { - if (data.slice(0, 2) === '0x') { - // data is already hex - return data; - } - // data is unicode, convert to hex - return bufferToHex(Buffer.from(data, 'utf8')); -} diff --git a/app/scripts/lib/message-manager.test.js b/app/scripts/lib/message-manager.test.js deleted file mode 100644 index 9a326d000..000000000 --- a/app/scripts/lib/message-manager.test.js +++ /dev/null @@ -1,128 +0,0 @@ -import { TransactionStatus } from '../../../shared/constants/transaction'; -import MessageManager from './message-manager'; - -describe('Message Manager', () => { - let messageManager; - - beforeEach(() => { - messageManager = new MessageManager({ - metricsEvent: jest.fn(), - }); - }); - - describe('#getMsgList', () => { - it('when new should return empty array', () => { - const result = messageManager.messages; - expect(Array.isArray(result)).toStrictEqual(true); - expect(result).toHaveLength(0); - }); - }); - - describe('#addMsg', () => { - it('adds a Msg returned in getMsgList', () => { - const Msg = { - id: 1, - status: TransactionStatus.approved, - metamaskNetworkId: 'unit test', - }; - messageManager.addMsg(Msg); - const result = messageManager.messages; - expect(Array.isArray(result)).toStrictEqual(true); - expect(result).toHaveLength(1); - expect(result[0].id).toStrictEqual(1); - }); - }); - - describe('#setMsgStatusApproved', () => { - it('sets the Msg status to approved', () => { - const Msg = { - id: 1, - status: 'unapproved', - metamaskNetworkId: 'unit test', - }; - messageManager.addMsg(Msg); - messageManager.setMsgStatusApproved(1); - const result = messageManager.messages; - expect(Array.isArray(result)).toStrictEqual(true); - expect(result).toHaveLength(1); - expect(result[0].status).toStrictEqual(TransactionStatus.approved); - }); - }); - - describe('#rejectMsg', () => { - it('sets the Msg status to rejected', () => { - const Msg = { - id: 1, - status: 'unapproved', - metamaskNetworkId: 'unit test', - }; - messageManager.addMsg(Msg); - messageManager.rejectMsg(1); - const result = messageManager.messages; - expect(Array.isArray(result)).toStrictEqual(true); - expect(result).toHaveLength(1); - expect(result[0].status).toStrictEqual(TransactionStatus.rejected); - }); - }); - - describe('#_updateMsg', () => { - it('replaces the Msg with the same id', () => { - messageManager.addMsg({ - id: '1', - status: 'unapproved', - metamaskNetworkId: 'unit test', - }); - messageManager.addMsg({ - id: '2', - status: TransactionStatus.approved, - metamaskNetworkId: 'unit test', - }); - messageManager._updateMsg({ - id: '1', - status: 'blah', - hash: 'foo', - metamaskNetworkId: 'unit test', - }); - const result = messageManager.getMsg('1'); - expect(result.hash).toStrictEqual('foo'); - }); - }); - - describe('#getUnapprovedMsgs', () => { - it('returns unapproved Msgs in a hash', () => { - messageManager.addMsg({ - id: '1', - status: 'unapproved', - metamaskNetworkId: 'unit test', - }); - messageManager.addMsg({ - id: '2', - status: TransactionStatus.approved, - metamaskNetworkId: 'unit test', - }); - const result = messageManager.getUnapprovedMsgs(); - expect(typeof result).toStrictEqual('object'); - expect(result['1'].status).toStrictEqual('unapproved'); - expect(result['2']).toBeUndefined(); - }); - }); - - describe('#getMsg', () => { - it('returns a Msg with the requested id', () => { - messageManager.addMsg({ - id: '1', - status: 'unapproved', - metamaskNetworkId: 'unit test', - }); - messageManager.addMsg({ - id: '2', - status: TransactionStatus.approved, - metamaskNetworkId: 'unit test', - }); - expect(messageManager.getMsg('1').status).toStrictEqual('unapproved'); - expect(messageManager.getMsg('2').status).toStrictEqual( - TransactionStatus.approved, - ); - }); - }); -}); diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js deleted file mode 100644 index 231aeb470..000000000 --- a/app/scripts/lib/typed-message-manager.js +++ /dev/null @@ -1,421 +0,0 @@ -import EventEmitter from 'events'; -import { strict as assert } from 'assert'; -import { ObservableStore } from '@metamask/obs-store'; -import { ethErrors } from 'eth-rpc-errors'; -import { typedSignatureHash, TYPED_MESSAGE_SCHEMA } from 'eth-sig-util'; -import log from 'loglevel'; -import jsonschema from 'jsonschema'; -import { MESSAGE_TYPE } from '../../../shared/constants/app'; -import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; -import createId from '../../../shared/modules/random-id'; -import { EVENT } from '../../../shared/constants/metametrics'; -import { isValidHexAddress } from '../../../shared/modules/hexstring-utils'; - -/** - * Represents, and contains data about, an 'eth_signTypedData' type signature request. These are created when a - * signature for an eth_signTypedData call is requested. - * - * @typedef {object} TypedMessage - * @property {number} id An id to track and identify the message object - * @property {object} msgParams The parameters to pass to the eth_signTypedData method once the signature request is - * approved. - * @property {object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask. - * @property {object} msgParams.from The address that is making the signature request. - * @property {string} msgParams.data A hex string conversion of the raw buffer data of the signature request - * @property {number} time The epoch time at which the this message was created - * @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed', 'rejected', or 'errored' - * @property {string} type The json-prc signing method for which a signature request has been made. A 'Message' will - * always have a 'eth_signTypedData' type. - */ - -export default class TypedMessageManager extends EventEmitter { - /** - * Controller in charge of managing - storing, adding, removing, updating - TypedMessage. - * - * @param options - * @param options.getCurrentChainId - * @param options.metricsEvent - * @param options.securityProviderRequest - */ - constructor({ getCurrentChainId, metricsEvent, securityProviderRequest }) { - super(); - this._getCurrentChainId = getCurrentChainId; - this.memStore = new ObservableStore({ - unapprovedTypedMessages: {}, - unapprovedTypedMessagesCount: 0, - }); - - this.resetState = () => { - this.memStore.updateState({ - unapprovedTypedMessages: {}, - unapprovedTypedMessagesCount: 0, - }); - }; - - this.messages = []; - this.metricsEvent = metricsEvent; - this.securityProviderRequest = securityProviderRequest; - } - - /** - * A getter for the number of 'unapproved' TypedMessages in this.messages - * - * @returns {number} The number of 'unapproved' TypedMessages in this.messages - */ - get unapprovedTypedMessagesCount() { - return Object.keys(this.getUnapprovedMsgs()).length; - } - - /** - * A getter for the 'unapproved' TypedMessages in this.messages - * - * @returns {object} An index of TypedMessage ids to TypedMessages, for all 'unapproved' TypedMessages in - * this.messages - */ - getUnapprovedMsgs() { - return this.messages - .filter((msg) => msg.status === 'unapproved') - .reduce((result, msg) => { - result[msg.id] = msg; - return result; - }, {}); - } - - /** - * Creates a new TypedMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add - * the new TypedMessage to this.messages, and to save the unapproved TypedMessages from that list to - * this.memStore. Before any of this is done, msgParams are validated - * - * @param {object} msgParams - The params for the eth_sign call to be made after the message is approved. - * @param {object} [req] - The original request object possibly containing the origin - * @param version - * @returns {promise} When the message has been signed or rejected - */ - async addUnapprovedMessageAsync(msgParams, req, version) { - return new Promise((resolve, reject) => { - this.addUnapprovedMessage(msgParams, req, version).then((msgId) => { - this.once(`${msgId}:finished`, (data) => { - switch (data.status) { - case 'signed': - return resolve(data.rawSig); - case 'rejected': - return reject( - ethErrors.provider.userRejectedRequest( - 'MetaMask Message Signature: User denied message signature.', - ), - ); - case 'errored': - return reject( - new Error(`MetaMask Message Signature: ${data.error}`), - ); - default: - return reject( - new Error( - `MetaMask Message Signature: Unknown problem: ${JSON.stringify( - msgParams, - )}`, - ), - ); - } - }); - }); - }); - } - - /** - * Creates a new TypedMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add - * the new TypedMessage to this.messages, and to save the unapproved TypedMessages from that list to - * this.memStore. Before any of this is done, msgParams are validated - * - * @param {object} msgParams - The params for the eth_sign call to be made after the message is approved. - * @param {object} [req] - The original request object possibly containing the origin - * @param version - * @returns {number} The id of the newly created TypedMessage. - */ - async addUnapprovedMessage(msgParams, req, version) { - msgParams.version = version; - if (req) { - msgParams.origin = req.origin; - } - this.validateParams(msgParams); - - log.debug( - `TypedMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`, - ); - - // create txData obj with parameters and meta data - const time = new Date().getTime(); - const msgId = createId(); - const msgData = { - id: msgId, - msgParams, - time, - status: 'unapproved', - type: MESSAGE_TYPE.ETH_SIGN_TYPED_DATA, - }; - this.addMsg(msgData); - - const securityProviderResponse = await this.securityProviderRequest( - msgData, - msgData.type, - ); - - msgData.securityProviderResponse = securityProviderResponse; - - // signal update - this.emit('update'); - return msgId; - } - - /** - * Helper method for this.addUnapprovedMessage. Validates that the passed params have the required properties. - * - * @param {object} params - The params to validate - */ - validateParams(params) { - assert.ok( - params && typeof params === 'object', - 'Params must be an object.', - ); - assert.ok('data' in params, 'Params must include a "data" field.'); - assert.ok('from' in params, 'Params must include a "from" field.'); - assert.ok( - typeof params.from === 'string' && - isValidHexAddress(params.from, { allowNonPrefixed: false }), - '"from" field must be a valid, lowercase, hexadecimal Ethereum address string.', - ); - - switch (params.version) { - case 'V1': - assert.ok( - Array.isArray(params.data), - '"params.data" must be an array.', - ); - assert.doesNotThrow(() => { - typedSignatureHash(params.data); - }, 'Signing data must be valid EIP-712 typed data.'); - break; - case 'V3': - case 'V4': { - assert.equal( - typeof params.data, - 'string', - '"params.data" must be a string.', - ); - let data; - assert.doesNotThrow(() => { - data = JSON.parse(params.data); - }, '"data" must be a valid JSON string.'); - const validation = jsonschema.validate(data, TYPED_MESSAGE_SCHEMA); - if (validation.errors.length !== 0) { - throw ethErrors.rpc.invalidParams({ - message: - 'Signing data must conform to EIP-712 schema. See https://git.io/fNtcx.', - data: validation.errors.map((v) => v.message.toString()), - }); - } - assert.ok( - data.primaryType in data.types, - `Primary type of "${data.primaryType}" has no type definition.`, - ); - let { chainId } = data.domain; - if (chainId) { - const activeChainId = parseInt(this._getCurrentChainId(), 16); - assert.ok( - !Number.isNaN(activeChainId), - `Cannot sign messages for chainId "${chainId}", because MetaMask is switching networks.`, - ); - if (typeof chainId === 'string') { - chainId = parseInt(chainId, chainId.startsWith('0x') ? 16 : 10); - } - assert.equal( - chainId, - activeChainId, - `Provided chainId "${chainId}" must match the active chainId "${activeChainId}"`, - ); - } - break; - } - default: - assert.fail(`Unknown typed data version "${params.version}"`); - } - } - - /** - * Adds a passed TypedMessage to this.messages, and calls this._saveMsgList() to save the unapproved TypedMessages from that - * list to this.memStore. - * - * @param {Message} msg - The TypedMessage to add to this.messages - */ - addMsg(msg) { - this.messages.push(msg); - this._saveMsgList(); - } - - /** - * Returns a specified TypedMessage. - * - * @param {number} msgId - The id of the TypedMessage to get - * @returns {TypedMessage|undefined} The TypedMessage with the id that matches the passed msgId, or undefined - * if no TypedMessage has that id. - */ - getMsg(msgId) { - return this.messages.find((msg) => msg.id === msgId); - } - - /** - * Approves a TypedMessage. Sets the message status via a call to this.setMsgStatusApproved, and returns a promise - * with any the message params modified for proper signing. - * - * @param {object} msgParams - The msgParams to be used when eth_sign is called, plus data added by MetaMask. - * @param {object} msgParams.metamaskId - Added to msgParams for tracking and identification within MetaMask. - * @returns {Promise} Promises the msgParams object with metamaskId removed. - */ - approveMessage(msgParams) { - this.setMsgStatusApproved(msgParams.metamaskId); - return this.prepMsgForSigning(msgParams); - } - - /** - * Sets a TypedMessage status to 'approved' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the TypedMessage to approve. - */ - setMsgStatusApproved(msgId) { - this._setMsgStatus(msgId, 'approved'); - } - - /** - * Sets a TypedMessage status to 'signed' via a call to this._setMsgStatus and updates that TypedMessage in - * this.messages by adding the raw signature data of the signature request to the TypedMessage - * - * @param {number} msgId - The id of the TypedMessage to sign. - * @param {buffer} rawSig - The raw data of the signature request - */ - setMsgStatusSigned(msgId, rawSig) { - const msg = this.getMsg(msgId); - msg.rawSig = rawSig; - this._updateMsg(msg); - this._setMsgStatus(msgId, 'signed'); - } - - /** - * Removes the metamaskId property from passed msgParams and returns a promise which resolves the updated msgParams - * - * @param {object} msgParams - The msgParams to modify - * @returns {Promise} Promises the msgParams with the metamaskId property removed - */ - async prepMsgForSigning(msgParams) { - delete msgParams.metamaskId; - delete msgParams.version; - return msgParams; - } - - /** - * Sets a TypedMessage status to 'rejected' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the TypedMessage to reject. - * @param reason - */ - rejectMsg(msgId, reason = undefined) { - if (reason) { - const msg = this.getMsg(msgId); - this.metricsEvent({ - event: reason, - category: EVENT.CATEGORIES.TRANSACTIONS, - properties: { - action: 'Sign Request', - version: msg.msgParams.version, - type: msg.type, - }, - }); - } - this._setMsgStatus(msgId, 'rejected'); - } - - /** - * Sets a TypedMessage status to 'errored' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the TypedMessage to error - * @param error - */ - errorMessage(msgId, error) { - const msg = this.getMsg(msgId); - msg.error = error; - this._updateMsg(msg); - this._setMsgStatus(msgId, 'errored'); - } - - /** - * Clears all unapproved messages from memory. - */ - clearUnapproved() { - this.messages = this.messages.filter((msg) => msg.status !== 'unapproved'); - this._saveMsgList(); - } - - // - // PRIVATE METHODS - // - - /** - * Updates the status of a TypedMessage in this.messages via a call to this._updateMsg - * - * @private - * @param {number} msgId - The id of the TypedMessage to update. - * @param {string} status - The new status of the TypedMessage. - * @throws A 'TypedMessageManager - TypedMessage not found for id: "${msgId}".' if there is no TypedMessage - * in this.messages with an id equal to the passed msgId - * @fires An event with a name equal to `${msgId}:${status}`. The TypedMessage is also fired. - * @fires If status is 'rejected' or 'signed', an event with a name equal to `${msgId}:finished` is fired along - * with the TypedMessage - */ - _setMsgStatus(msgId, status) { - const msg = this.getMsg(msgId); - if (!msg) { - throw new Error( - `TypedMessageManager - Message not found for id: "${msgId}".`, - ); - } - msg.status = status; - this._updateMsg(msg); - this.emit(`${msgId}:${status}`, msg); - if (status === 'rejected' || status === 'signed' || status === 'errored') { - this.emit(`${msgId}:finished`, msg); - } - } - - /** - * Sets a TypedMessage in this.messages to the passed TypedMessage if the ids are equal. Then saves the - * unapprovedTypedMsgs index to storage via this._saveMsgList - * - * @private - * @param {TypedMessage} msg - A TypedMessage that will replace an existing TypedMessage (with the same - * id) in this.messages - */ - _updateMsg(msg) { - const index = this.messages.findIndex((message) => message.id === msg.id); - if (index !== -1) { - this.messages[index] = msg; - } - this._saveMsgList(); - } - - /** - * Saves the unapproved TypedMessages, and their count, to this.memStore - * - * @private - * @fires 'updateBadge' - */ - _saveMsgList() { - const unapprovedTypedMessages = this.getUnapprovedMsgs(); - const unapprovedTypedMessagesCount = Object.keys( - unapprovedTypedMessages, - ).length; - this.memStore.updateState({ - unapprovedTypedMessages, - unapprovedTypedMessagesCount, - }); - this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); - } -} diff --git a/app/scripts/lib/typed-message-manager.test.js b/app/scripts/lib/typed-message-manager.test.js deleted file mode 100644 index b20b8ab09..000000000 --- a/app/scripts/lib/typed-message-manager.test.js +++ /dev/null @@ -1,126 +0,0 @@ -import sinon from 'sinon'; -import { TransactionStatus } from '../../../shared/constants/transaction'; -import TypedMessageManager from './typed-message-manager'; - -describe('Typed Message Manager', () => { - let typedMessageManager, - msgParamsV1, - msgParamsV3, - typedMsgs, - messages, - msgId, - numberMsgId; - - const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'; - - beforeEach(async () => { - typedMessageManager = new TypedMessageManager({ - getCurrentChainId: sinon.fake.returns('0x1'), - metricsEvent: sinon.fake(), - securityProviderRequest: sinon.fake(), - }); - - msgParamsV1 = { - from: address, - data: [ - { type: 'string', name: 'unit test', value: 'hello there' }, - { - type: 'uint32', - name: 'A number, but not really a number', - value: '$$$', - }, - ], - }; - - msgParamsV3 = { - from: address, - data: JSON.stringify({ - types: { - EIP712Domain: [ - { name: 'name', type: 'string' }, - { name: 'version', type: 'string' }, - { name: 'chainId', type: 'uint256' }, - { name: 'verifyingContract', type: 'address' }, - ], - Person: [ - { name: 'name', type: 'string' }, - { name: 'wallet', type: 'address' }, - ], - Mail: [ - { name: 'from', type: 'Person' }, - { name: 'to', type: 'Person' }, - { name: 'contents', type: 'string' }, - ], - }, - primaryType: 'Mail', - domain: { - name: 'Ether Mainl', - version: '1', - chainId: 1, - verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', - }, - message: { - from: { - name: 'Cow', - wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', - }, - to: { - name: 'Bob', - wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', - }, - contents: 'Hello, Bob!', - }, - }), - }; - - await typedMessageManager.addUnapprovedMessage(msgParamsV3, null, 'V3'); - typedMsgs = typedMessageManager.getUnapprovedMsgs(); - messages = typedMessageManager.messages; - msgId = Object.keys(typedMsgs)[0]; - messages[0].msgParams.metamaskId = parseInt(msgId, 10); - numberMsgId = parseInt(msgId, 10); - }); - - it('supports version 1 of signedTypedData', async () => { - await typedMessageManager.addUnapprovedMessage(msgParamsV1, null, 'V1'); - expect(messages[messages.length - 1].msgParams.data).toStrictEqual( - msgParamsV1.data, - ); - }); - - it('has params address', () => { - expect(typedMsgs[msgId].msgParams.from).toStrictEqual(address); - }); - - it('adds to unapproved messages and sets status to unapproved', () => { - expect(typedMsgs[msgId].status).toStrictEqual(TransactionStatus.unapproved); - }); - - it('validates params', async () => { - await expect(() => { - typedMessageManager.validateParams(messages[0].msgParams); - }).not.toThrow(); - }); - - it('gets unapproved by id', () => { - const getMsg = typedMessageManager.getMsg(numberMsgId); - expect(getMsg.id).toStrictEqual(numberMsgId); - }); - - it('approves messages', async () => { - const messageMetaMaskId = messages[0].msgParams; - typedMessageManager.approveMessage(messageMetaMaskId); - expect(messages[0].status).toStrictEqual(TransactionStatus.approved); - }); - - it('sets msg status to signed and adds a raw sig to message details', () => { - typedMessageManager.setMsgStatusSigned(numberMsgId, 'raw sig'); - expect(messages[0].status).toStrictEqual(TransactionStatus.signed); - expect(messages[0].rawSig).toStrictEqual('raw sig'); - }); - - it('rejects message', () => { - typedMessageManager.rejectMsg(numberMsgId); - expect(messages[0].status).toStrictEqual(TransactionStatus.rejected); - }); -}); diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index c42770804..3e567c5d6 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -12,11 +12,7 @@ import { } from '@metamask/eth-keyring-controller'; import createFilterMiddleware from 'eth-json-rpc-filters'; import createSubscriptionManager from 'eth-json-rpc-filters/subscriptionManager'; -import { - errorCodes as rpcErrorCodes, - EthereumRpcError, - ethErrors, -} from 'eth-rpc-errors'; +import { errorCodes as rpcErrorCodes, EthereumRpcError } from 'eth-rpc-errors'; import { Mutex } from 'await-semaphore'; import log from 'loglevel'; import TrezorKeyring from 'eth-trezor-keyring'; @@ -149,11 +145,8 @@ import AlertController from './controllers/alert'; import OnboardingController from './controllers/onboarding'; import BackupController from './controllers/backup'; import IncomingTransactionsController from './controllers/incoming-transactions'; -import MessageManager, { normalizeMsgData } from './lib/message-manager'; import DecryptMessageManager from './lib/decrypt-message-manager'; import EncryptionPublicKeyManager from './lib/encryption-public-key-manager'; -import PersonalMessageManager from './lib/personal-message-manager'; -import TypedMessageManager from './lib/typed-message-manager'; import TransactionController from './controllers/transactions'; import DetectTokensController from './controllers/detect-tokens'; import SwapsController from './controllers/swaps'; @@ -164,6 +157,7 @@ import { segment } from './lib/segment'; import createMetaRPCHandler from './lib/createMetaRPCHandler'; import { previousValueComparator } from './lib/util'; import createMetamaskMiddleware from './lib/createMetamaskMiddleware'; +import SignController from './controllers/sign'; import { CaveatMutatorFactories, @@ -1053,18 +1047,6 @@ export default class MetamaskController extends EventEmitter { }); this.networkController.lookupNetwork(); - this.messageManager = new MessageManager({ - metricsEvent: this.metaMetricsController.trackEvent.bind( - this.metaMetricsController, - ), - securityProviderRequest: this.securityProviderRequest.bind(this), - }); - this.personalMessageManager = new PersonalMessageManager({ - metricsEvent: this.metaMetricsController.trackEvent.bind( - this.metaMetricsController, - ), - securityProviderRequest: this.securityProviderRequest.bind(this), - }); this.decryptMessageManager = new DecryptMessageManager({ metricsEvent: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, @@ -1075,12 +1057,19 @@ export default class MetamaskController extends EventEmitter { this.metaMetricsController, ), }); - this.typedMessageManager = new TypedMessageManager({ - getCurrentChainId: () => - this.networkController.store.getState().provider.chainId, - metricsEvent: this.metaMetricsController.trackEvent.bind( - this.metaMetricsController, - ), + + this.signController = new SignController({ + messenger: this.controllerMessenger.getRestricted({ + name: 'SignController', + allowedActions: [ + `${this.approvalController.name}:addRequest`, + `${this.approvalController.name}:acceptRequest`, + `${this.approvalController.name}:rejectRequest`, + ], + }), + keyringController: this.keyringController, + preferencesController: this.preferencesController, + getState: this.getState.bind(this), securityProviderRequest: this.securityProviderRequest.bind(this), }); @@ -1136,10 +1125,8 @@ export default class MetamaskController extends EventEmitter { this.networkController.on(NETWORK_EVENTS.NETWORK_WILL_CHANGE, () => { this.txController.txStateManager.clearUnapprovedTxs(); this.encryptionPublicKeyManager.clearUnapproved(); - this.personalMessageManager.clearUnapproved(); - this.typedMessageManager.clearUnapproved(); this.decryptMessageManager.clearUnapproved(); - this.messageManager.clearUnapproved(); + this.signController.clearUnapproved(); }); this.metamaskMiddleware = createMetamaskMiddleware({ @@ -1167,11 +1154,22 @@ export default class MetamaskController extends EventEmitter { // tx signing processTransaction: this.newUnapprovedTransaction.bind(this), // msg signing - processEthSignMessage: this.newUnsignedMessage.bind(this), - processTypedMessage: this.newUnsignedTypedMessage.bind(this), - processTypedMessageV3: this.newUnsignedTypedMessage.bind(this), - processTypedMessageV4: this.newUnsignedTypedMessage.bind(this), - processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), + processEthSignMessage: this.signController.newUnsignedMessage.bind( + this.signController, + ), + processTypedMessage: this.signController.newUnsignedTypedMessage.bind( + this.signController, + ), + processTypedMessageV3: this.signController.newUnsignedTypedMessage.bind( + this.signController, + ), + processTypedMessageV4: this.signController.newUnsignedTypedMessage.bind( + this.signController, + ), + processPersonalMessage: + this.signController.newUnsignedPersonalMessage.bind( + this.signController, + ), processDecryptMessage: this.newRequestDecryptMessage.bind(this), processEncryptionPublicKey: this.newRequestEncryptionPublicKey.bind(this), getPendingNonce: this.getPendingNonce.bind(this), @@ -1195,11 +1193,9 @@ export default class MetamaskController extends EventEmitter { AccountTracker: this.accountTracker.store, TxController: this.txController.memStore, TokenRatesController: this.tokenRatesController, - MessageManager: this.messageManager.memStore, - PersonalMessageManager: this.personalMessageManager.memStore, DecryptMessageManager: this.decryptMessageManager.memStore, EncryptionPublicKeyManager: this.encryptionPublicKeyManager.memStore, - TypesMessageManager: this.typedMessageManager.memStore, + SignController: this.signController, SwapsController: this.swapsController.store, EnsController: this.ensController.store, ApprovalController: this.approvalController, @@ -1277,11 +1273,9 @@ export default class MetamaskController extends EventEmitter { const resetMethods = [ this.accountTracker.resetState, this.txController.resetState, - this.messageManager.resetState, - this.personalMessageManager.resetState, this.decryptMessageManager.resetState, this.encryptionPublicKeyManager.resetState, - this.typedMessageManager.resetState, + this.signController.resetState.bind(this.signController), this.swapsController.resetState, this.ensController.resetState, this.approvalController.clear.bind(this.approvalController), @@ -1975,17 +1969,24 @@ export default class MetamaskController extends EventEmitter { updatePreviousGasParams: txController.updatePreviousGasParams.bind(txController), - // messageManager - signMessage: this.signMessage.bind(this), - cancelMessage: this.cancelMessage.bind(this), - // personalMessageManager - signPersonalMessage: this.signPersonalMessage.bind(this), - cancelPersonalMessage: this.cancelPersonalMessage.bind(this), - - // typedMessageManager - signTypedMessage: this.signTypedMessage.bind(this), - cancelTypedMessage: this.cancelTypedMessage.bind(this), + // signController + signMessage: this.signController.signMessage.bind(this.signController), + cancelMessage: this.signController.cancelMessage.bind( + this.signController, + ), + signPersonalMessage: this.signController.signPersonalMessage.bind( + this.signController, + ), + cancelPersonalMessage: this.signController.cancelPersonalMessage.bind( + this.signController, + ), + signTypedMessage: this.signController.signTypedMessage.bind( + this.signController, + ), + cancelTypedMessage: this.signController.cancelTypedMessage.bind( + this.signController, + ), // decryptMessageManager decryptMessage: this.decryptMessage.bind(this), @@ -3099,46 +3100,6 @@ export default class MetamaskController extends EventEmitter { return await this.txController.newUnapprovedTransaction(txParams, req); } - // eth_sign methods: - - /** - * Called when a Dapp uses the eth_sign method, to request user approval. - * eth_sign is a pure signature of arbitrary data. It is on a deprecation - * path, since this data can be a transaction, or can leak private key - * information. - * - * @param {object} msgParams - The params passed to eth_sign. - * @param {object} [req] - The original request, containing the origin. - */ - async newUnsignedMessage(msgParams, req) { - const { disabledRpcMethodPreferences } = - this.preferencesController.store.getState(); - const { eth_sign } = disabledRpcMethodPreferences; // eslint-disable-line camelcase - const data = normalizeMsgData(msgParams.data); - let promise; - - // eslint-disable-next-line camelcase - if (!eth_sign) { - throw ethErrors.rpc.methodNotFound( - 'eth_sign has been disabled. You must enable it in the advanced settings', - ); - } - - // 64 hex + "0x" at the beginning - // This is needed because Ethereum's EcSign works only on 32 byte numbers - // For 67 length see: https://github.com/MetaMask/metamask-extension/pull/12679/files#r749479607 - if (data.length === 66 || data.length === 67) { - promise = this.messageManager.addUnapprovedMessageAsync(msgParams, req); - this.sendUpdate(); - this.opts.showUserConfirmation(); - } else { - throw ethErrors.rpc.invalidParams( - 'eth_sign requires 32 byte message hash', - ); - } - return await promise; - } - ///: BEGIN:ONLY_INCLUDE_IN(flask) /** * Gets an "app key" corresponding to an Ethereum address. An app key is more @@ -3165,105 +3126,6 @@ export default class MetamaskController extends EventEmitter { } ///: END:ONLY_INCLUDE_IN - /** - * Signifies user intent to complete an eth_sign method. - * - * @param {object} msgParams - The params passed to eth_call. - * @returns {Promise} Full state update. - */ - async signMessage(msgParams) { - log.info('MetaMaskController - signMessage'); - const msgId = msgParams.metamaskId; - try { - // sets the status op the message to 'approved' - // and removes the metamaskId for signing - const cleanMsgParams = await this.messageManager.approveMessage( - msgParams, - ); - const rawSig = await this.keyringController.signMessage(cleanMsgParams); - this.messageManager.setMsgStatusSigned(msgId, rawSig); - return this.getState(); - } catch (error) { - log.info('MetaMaskController - eth_sign failed', error); - this.messageManager.errorMessage(msgId, error); - throw error; - } - } - - /** - * Used to cancel a message submitted via eth_sign. - * - * @param {string} msgId - The id of the message to cancel. - */ - cancelMessage(msgId) { - const { messageManager } = this; - messageManager.rejectMsg(msgId); - return this.getState(); - } - - // personal_sign methods: - - /** - * Called when a dapp uses the personal_sign method. - * This is identical to the Geth eth_sign method, and may eventually replace - * eth_sign. - * - * We currently define our eth_sign and personal_sign mostly for legacy Dapps. - * - * @param {object} msgParams - The params of the message to sign & return to the Dapp. - * @param {object} [req] - The original request, containing the origin. - */ - async newUnsignedPersonalMessage(msgParams, req) { - const promise = this.personalMessageManager.addUnapprovedMessageAsync( - msgParams, - req, - ); - this.sendUpdate(); - this.opts.showUserConfirmation(); - return promise; - } - - /** - * Signifies a user's approval to sign a personal_sign message in queue. - * Triggers signing, and the callback function from newUnsignedPersonalMessage. - * - * @param {object} msgParams - The params of the message to sign & return to the Dapp. - * @returns {Promise} A full state update. - */ - async signPersonalMessage(msgParams) { - log.info('MetaMaskController - signPersonalMessage'); - const msgId = msgParams.metamaskId; - // sets the status op the message to 'approved' - // and removes the metamaskId for signing - try { - const cleanMsgParams = await this.personalMessageManager.approveMessage( - msgParams, - ); - const rawSig = await this.keyringController.signPersonalMessage( - cleanMsgParams, - ); - // tells the listener that the message has been signed - // and can be returned to the dapp - this.personalMessageManager.setMsgStatusSigned(msgId, rawSig); - return this.getState(); - } catch (error) { - log.info('MetaMaskController - eth_personalSign failed', error); - this.personalMessageManager.errorMessage(msgId, error); - throw error; - } - } - - /** - * Used to cancel a personal_sign type message. - * - * @param {string} msgId - The ID of the message to cancel. - */ - cancelPersonalMessage(msgId) { - const messageManager = this.personalMessageManager; - messageManager.rejectMsg(msgId); - return this.getState(); - } - // eth_decrypt methods /** @@ -3456,74 +3318,6 @@ export default class MetamaskController extends EventEmitter { return this.getState(); } - // eth_signTypedData methods - - /** - * Called when a dapp uses the eth_signTypedData method, per EIP 712. - * - * @param {object} msgParams - The params passed to eth_signTypedData. - * @param {object} [req] - The original request, containing the origin. - * @param version - */ - async newUnsignedTypedMessage(msgParams, req, version) { - const promise = this.typedMessageManager.addUnapprovedMessageAsync( - msgParams, - req, - version, - ); - this.sendUpdate(); - this.opts.showUserConfirmation(); - return promise; - } - - /** - * The method for a user approving a call to eth_signTypedData, per EIP 712. - * Triggers the callback in newUnsignedTypedMessage. - * - * @param {object} msgParams - The params passed to eth_signTypedData. - * @returns {object} Full state update. - */ - async signTypedMessage(msgParams) { - log.info('MetaMaskController - eth_signTypedData'); - const msgId = msgParams.metamaskId; - const { version } = msgParams; - try { - const cleanMsgParams = await this.typedMessageManager.approveMessage( - msgParams, - ); - - // For some reason every version after V1 used stringified params. - if (version !== 'V1') { - // But we don't have to require that. We can stop suggesting it now: - if (typeof cleanMsgParams.data === 'string') { - cleanMsgParams.data = JSON.parse(cleanMsgParams.data); - } - } - - const signature = await this.keyringController.signTypedMessage( - cleanMsgParams, - { version }, - ); - this.typedMessageManager.setMsgStatusSigned(msgId, signature); - return this.getState(); - } catch (error) { - log.info('MetaMaskController - eth_signTypedData failed.', error); - this.typedMessageManager.errorMessage(msgId, error); - throw error; - } - } - - /** - * Used to cancel a eth_signTypedData type message. - * - * @param {string} msgId - The ID of the message to cancel. - */ - cancelTypedMessage(msgId) { - const messageManager = this.typedMessageManager; - messageManager.rejectMsg(msgId); - return this.getState(); - } - /** * @returns {boolean} true if the keyring type supports EIP-1559 */ diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index 43d0e5b20..03e335a26 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -897,177 +897,6 @@ describe('MetaMaskController', function () { }); }); - describe('#newUnsignedMessage', function () { - let msgParams, metamaskMsgs, messages, msgId; - - const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'; - const data = - '0x0000000000000000000000000000000000000043727970746f6b697474696573'; - - beforeEach(async function () { - sandbox.stub(metamaskController, 'getBalance'); - metamaskController.getBalance.callsFake(() => { - return Promise.resolve('0x0'); - }); - - await metamaskController.createNewVaultAndRestore( - 'foobar1337', - TEST_SEED_ALT, - ); - - msgParams = { - from: address, - data, - }; - - metamaskController.preferencesController.setDisabledRpcMethodPreference( - 'eth_sign', - true, - ); - const promise = metamaskController.newUnsignedMessage(msgParams); - // handle the promise so it doesn't throw an unhandledRejection - promise.then(noop).catch(noop); - - metamaskMsgs = metamaskController.messageManager.getUnapprovedMsgs(); - messages = metamaskController.messageManager.messages; - msgId = Object.keys(metamaskMsgs)[0]; - messages[0].msgParams.metamaskId = parseInt(msgId, 10); - }); - - it('persists address from msg params', function () { - assert.equal(metamaskMsgs[msgId].msgParams.from, address); - }); - - it('persists data from msg params', function () { - assert.equal(metamaskMsgs[msgId].msgParams.data, data); - }); - - it('sets the status to unapproved', function () { - assert.equal(metamaskMsgs[msgId].status, TransactionStatus.unapproved); - }); - - it('sets the type to eth_sign', function () { - assert.equal(metamaskMsgs[msgId].type, 'eth_sign'); - }); - - it('rejects the message', function () { - const msgIdInt = parseInt(msgId, 10); - metamaskController.cancelMessage(msgIdInt, noop); - assert.equal(messages[0].status, TransactionStatus.rejected); - }); - - it('checks message length', async function () { - msgParams = { - from: address, - data: '0xDEADBEEF', - }; - - try { - await metamaskController.newUnsignedMessage(msgParams); - } catch (error) { - assert.equal(error.message, 'eth_sign requires 32 byte message hash'); - } - }); - - it('errors when signing a message', async function () { - try { - await metamaskController.signMessage(messages[0].msgParams); - } catch (error) { - assert.equal( - error.message, - 'Expected message to be an Uint8Array with length 32', - ); - } - }); - }); - - describe('#newUnsignedPersonalMessage', function () { - let msgParams, metamaskPersonalMsgs, personalMessages, msgId; - - const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'; - const data = '0x43727970746f6b697474696573'; - - beforeEach(async function () { - sandbox.stub(metamaskController, 'getBalance'); - metamaskController.getBalance.callsFake(() => { - return Promise.resolve('0x0'); - }); - - await metamaskController.createNewVaultAndRestore( - 'foobar1337', - TEST_SEED_ALT, - ); - - msgParams = { - from: address, - data, - }; - - const promise = metamaskController.newUnsignedPersonalMessage(msgParams); - // handle the promise so it doesn't throw an unhandledRejection - promise.then(noop).catch(noop); - - metamaskPersonalMsgs = - metamaskController.personalMessageManager.getUnapprovedMsgs(); - personalMessages = metamaskController.personalMessageManager.messages; - msgId = Object.keys(metamaskPersonalMsgs)[0]; - personalMessages[0].msgParams.metamaskId = parseInt(msgId, 10); - }); - - it('errors with no from in msgParams', async function () { - try { - await metamaskController.newUnsignedPersonalMessage({ - data, - }); - assert.fail('should have thrown'); - } catch (error) { - assert.equal( - error.message, - 'MetaMask Message Signature: from field is required.', - ); - } - }); - - it('persists address from msg params', function () { - assert.equal(metamaskPersonalMsgs[msgId].msgParams.from, address); - }); - - it('persists data from msg params', function () { - assert.equal(metamaskPersonalMsgs[msgId].msgParams.data, data); - }); - - it('sets the status to unapproved', function () { - assert.equal( - metamaskPersonalMsgs[msgId].status, - TransactionStatus.unapproved, - ); - }); - - it('sets the type to personal_sign', function () { - assert.equal(metamaskPersonalMsgs[msgId].type, 'personal_sign'); - }); - - it('rejects the message', function () { - const msgIdInt = parseInt(msgId, 10); - metamaskController.cancelPersonalMessage(msgIdInt, noop); - assert.equal(personalMessages[0].status, TransactionStatus.rejected); - }); - - it('errors when signing a message', async function () { - await metamaskController.signPersonalMessage( - personalMessages[0].msgParams, - ); - assert.equal( - metamaskPersonalMsgs[msgId].status, - TransactionStatus.signed, - ); - assert.equal( - metamaskPersonalMsgs[msgId].rawSig, - '0x6a1b65e2b8ed53cf398a769fad24738f9fbe29841fe6854e226953542c4b6a173473cb152b6b1ae5f06d601d45dd699a129b0a8ca84e78b423031db5baa734741b', - ); - }); - }); - describe('#setupUntrustedCommunication', function () { const mockTxParams = { from: TEST_ADDRESS }; diff --git a/jest.config.js b/jest.config.js index 94daa4971..a16469ce1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,6 +3,7 @@ module.exports = { '/app/scripts/constants/error-utils.js', '/app/scripts/controllers/network/**/*.js', '/app/scripts/controllers/permissions/**/*.js', + '/app/scripts/controllers/sign.ts', '/app/scripts/flask/**/*.js', '/app/scripts/lib/**/*.js', '/app/scripts/lib/createRPCMethodTrackingMiddleware.js', @@ -39,6 +40,7 @@ module.exports = { '/app/scripts/controllers/app-state.test.js', '/app/scripts/controllers/network/**/*.test.js', '/app/scripts/controllers/permissions/**/*.test.js', + '/app/scripts/controllers/sign.test.ts', '/app/scripts/flask/**/*.test.js', '/app/scripts/lib/**/*.test.js', '/app/scripts/lib/**/*.test.ts', diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 116df79f1..17687c27b 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -1302,6 +1302,42 @@ "@metamask/logo>gl-vec3": true } }, + "@metamask/message-manager": { + "packages": { + "@metamask/message-manager>@metamask/base-controller": true, + "@metamask/message-manager>@metamask/controller-utils": true, + "@metamask/message-manager>jsonschema": true, + "browserify>buffer": true, + "browserify>events": true, + "eth-sig-util": true, + "ethereumjs-util": true, + "uuid": true + } + }, + "@metamask/message-manager>@metamask/base-controller": { + "packages": { + "immer": true + } + }, + "@metamask/message-manager>@metamask/controller-utils": { + "globals": { + "console.error": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "browserify>buffer": true, + "eslint>fast-deep-equal": true, + "eth-ens-namehash": true, + "ethereumjs-util": true, + "ethjs>ethjs-unit": true + } + }, + "@metamask/message-manager>jsonschema": { + "packages": { + "browserify>url": true + } + }, "@metamask/notification-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -3588,11 +3624,6 @@ "readable-stream": true } }, - "jsonschema": { - "packages": { - "browserify>url": true - } - }, "koa>is-generator-function>has-tostringtag": { "packages": { "string.prototype.matchall>has-symbols": true diff --git a/lavamoat/browserify/desktop/policy.json b/lavamoat/browserify/desktop/policy.json index ac03c7631..ceadebd90 100644 --- a/lavamoat/browserify/desktop/policy.json +++ b/lavamoat/browserify/desktop/policy.json @@ -1356,6 +1356,42 @@ "@metamask/logo>gl-vec3": true } }, + "@metamask/message-manager": { + "packages": { + "@metamask/message-manager>@metamask/base-controller": true, + "@metamask/message-manager>@metamask/controller-utils": true, + "@metamask/message-manager>jsonschema": true, + "browserify>buffer": true, + "browserify>events": true, + "eth-sig-util": true, + "ethereumjs-util": true, + "uuid": true + } + }, + "@metamask/message-manager>@metamask/base-controller": { + "packages": { + "immer": true + } + }, + "@metamask/message-manager>@metamask/controller-utils": { + "globals": { + "console.error": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "browserify>buffer": true, + "eslint>fast-deep-equal": true, + "eth-ens-namehash": true, + "ethereumjs-util": true, + "ethjs>ethjs-unit": true + } + }, + "@metamask/message-manager>jsonschema": { + "packages": { + "browserify>url": true + } + }, "@metamask/notification-controller": { "packages": { "@metamask/controller-utils": true, @@ -4029,11 +4065,6 @@ "readable-stream": true } }, - "jsonschema": { - "packages": { - "browserify>url": true - } - }, "koa>is-generator-function>has-tostringtag": { "packages": { "string.prototype.matchall>has-symbols": true diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index ac03c7631..ceadebd90 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -1356,6 +1356,42 @@ "@metamask/logo>gl-vec3": true } }, + "@metamask/message-manager": { + "packages": { + "@metamask/message-manager>@metamask/base-controller": true, + "@metamask/message-manager>@metamask/controller-utils": true, + "@metamask/message-manager>jsonschema": true, + "browserify>buffer": true, + "browserify>events": true, + "eth-sig-util": true, + "ethereumjs-util": true, + "uuid": true + } + }, + "@metamask/message-manager>@metamask/base-controller": { + "packages": { + "immer": true + } + }, + "@metamask/message-manager>@metamask/controller-utils": { + "globals": { + "console.error": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "browserify>buffer": true, + "eslint>fast-deep-equal": true, + "eth-ens-namehash": true, + "ethereumjs-util": true, + "ethjs>ethjs-unit": true + } + }, + "@metamask/message-manager>jsonschema": { + "packages": { + "browserify>url": true + } + }, "@metamask/notification-controller": { "packages": { "@metamask/controller-utils": true, @@ -4029,11 +4065,6 @@ "readable-stream": true } }, - "jsonschema": { - "packages": { - "browserify>url": true - } - }, "koa>is-generator-function>has-tostringtag": { "packages": { "string.prototype.matchall>has-symbols": true diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 116df79f1..17687c27b 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -1302,6 +1302,42 @@ "@metamask/logo>gl-vec3": true } }, + "@metamask/message-manager": { + "packages": { + "@metamask/message-manager>@metamask/base-controller": true, + "@metamask/message-manager>@metamask/controller-utils": true, + "@metamask/message-manager>jsonschema": true, + "browserify>buffer": true, + "browserify>events": true, + "eth-sig-util": true, + "ethereumjs-util": true, + "uuid": true + } + }, + "@metamask/message-manager>@metamask/base-controller": { + "packages": { + "immer": true + } + }, + "@metamask/message-manager>@metamask/controller-utils": { + "globals": { + "console.error": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "browserify>buffer": true, + "eslint>fast-deep-equal": true, + "eth-ens-namehash": true, + "ethereumjs-util": true, + "ethjs>ethjs-unit": true + } + }, + "@metamask/message-manager>jsonschema": { + "packages": { + "browserify>url": true + } + }, "@metamask/notification-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -3588,11 +3624,6 @@ "readable-stream": true } }, - "jsonschema": { - "packages": { - "browserify>url": true - } - }, "koa>is-generator-function>has-tostringtag": { "packages": { "string.prototype.matchall>has-symbols": true diff --git a/package.json b/package.json index d57328cb5..62e4af2ab 100644 --- a/package.json +++ b/package.json @@ -242,6 +242,7 @@ "@metamask/jazzicon": "^2.0.0", "@metamask/key-tree": "^7.0.0", "@metamask/logo": "^3.1.1", + "@metamask/message-manager": "^2.0.0", "@metamask/metamask-eth-abis": "^3.0.0", "@metamask/notification-controller": "^1.0.0", "@metamask/obs-store": "^5.0.0", @@ -310,7 +311,6 @@ "jest-junit": "^14.0.1", "json-rpc-engine": "^6.1.0", "json-rpc-middleware-stream": "^4.2.1", - "jsonschema": "^1.2.4", "labeled-stream-splicer": "^2.0.2", "localforage": "^1.9.0", "lodash": "^4.17.21", diff --git a/types/eth-keyring-controller.d.ts b/types/eth-keyring-controller.d.ts new file mode 100644 index 000000000..86d8ffc6b --- /dev/null +++ b/types/eth-keyring-controller.d.ts @@ -0,0 +1,9 @@ +declare module '@metamask/eth-keyring-controller' { + export class KeyringController { + signMessage: (...any) => any; + + signPersonalMessage: (...any) => any; + + signTypedMessage: (...any) => any; + } +} diff --git a/ui/store/actions.test.js b/ui/store/actions.test.js index 105ff34c8..cac4f79a3 100644 --- a/ui/store/actions.test.js +++ b/ui/store/actions.test.js @@ -45,6 +45,10 @@ describe('Actions', () => { background = sinon.createStubInstance(MetaMaskController, { getState: sinon.stub().callsFake((cb) => cb(null, baseMockState)), }); + + background.signMessage = sinon.stub(); + background.signPersonalMessage = sinon.stub(); + background.signTypedMessage = sinon.stub(); }); describe('#tryUnlockMetamask', () => { diff --git a/yarn.lock b/yarn.lock index 11acd8dea..3f1224f76 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4026,6 +4026,21 @@ __metadata: languageName: node linkType: hard +"@metamask/message-manager@npm:^2.0.0": + version: 2.0.0 + resolution: "@metamask/message-manager@npm:2.0.0" + dependencies: + "@metamask/base-controller": ^2.0.0 + "@metamask/controller-utils": ^3.0.0 + "@types/uuid": ^8.3.0 + eth-sig-util: ^3.0.0 + ethereumjs-util: ^7.0.10 + jsonschema: ^1.2.4 + uuid: ^8.3.2 + checksum: f130b5c58fbbb5ccd69da32cfa43839a09dec906974c6d0f6e2d46f15d1a872c564b0c880aac2979b7ffc8d00bcf328bf1989cb133cc41bc612e1e6e16ef9ef5 + languageName: node + linkType: hard + "@metamask/metamask-eth-abis@npm:3.0.0, @metamask/metamask-eth-abis@npm:^3.0.0": version: 3.0.0 resolution: "@metamask/metamask-eth-abis@npm:3.0.0" @@ -24282,6 +24297,7 @@ __metadata: "@metamask/jazzicon": ^2.0.0 "@metamask/key-tree": ^7.0.0 "@metamask/logo": ^3.1.1 + "@metamask/message-manager": ^2.0.0 "@metamask/metamask-eth-abis": ^3.0.0 "@metamask/notification-controller": ^1.0.0 "@metamask/obs-store": ^5.0.0 @@ -24460,7 +24476,6 @@ __metadata: jsdom: ^11.2.0 json-rpc-engine: ^6.1.0 json-rpc-middleware-stream: ^4.2.1 - jsonschema: ^1.2.4 junit-report-merger: ^4.0.0 koa: ^2.7.0 labeled-stream-splicer: ^2.0.2