From 3d3a1774b3d3fcd6919426b3829762b275100158 Mon Sep 17 00:00:00 2001 From: Esteban MIno Date: Thu, 29 Nov 2018 22:16:55 -0300 Subject: [PATCH 01/18] TypedMessageManager with signTypedMessageV3 data type validation missing --- package-lock.json | 104 +++++++- package.json | 1 + src/TypedMessageManager.test.ts | 442 ++++++++++++++++++++++++++++++++ src/TypedMessageManager.ts | 379 +++++++++++++++++++++++++++ src/util.test.ts | 139 ++++++++++ src/util.ts | 58 ++++- 6 files changed, 1110 insertions(+), 13 deletions(-) create mode 100644 src/TypedMessageManager.test.ts create mode 100644 src/TypedMessageManager.ts diff --git a/package-lock.json b/package-lock.json index b19098657ea..e25371a90f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1485,6 +1485,11 @@ "resolved": "https://registry.npmjs.org/base-x/-/base-x-1.1.0.tgz", "integrity": "sha1-QtPXF0dPnqAiB/bRqh9CaRPut6w=" }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -1656,6 +1661,15 @@ "node-int64": "^0.4.0" } }, + "buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", + "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, "buffer-from": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz", @@ -2558,6 +2572,25 @@ "loglevel": "^1.5.0", "obs-store": "^2.4.1", "promise-filter": "^1.1.0" + }, + "dependencies": { + "eth-sig-util": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", + "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", + "requires": { + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", + "ethereumjs-util": "^5.1.1" + } + }, + "ethereumjs-abi": { + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", + "requires": { + "bn.js": "^4.10.0", + "ethereumjs-util": "^5.0.0" + } + } } }, "eth-phishing-detect": { @@ -2578,21 +2611,22 @@ } }, "eth-sig-util": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", - "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-2.1.0.tgz", + "integrity": "sha512-JRKmq1zytYoOuAj8llYiGlRGSlWrQ0jGGh9+YPhELfmMP1PD/dkwq2kzMoB8pRF6sEgZojQfSasswto3xsKFvw==", "requires": { - "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", - "ethereumjs-util": "^5.1.1" + "buffer": "^5.2.1", + "elliptic": "^6.4.0", + "ethereumjs-abi": "0.6.5", + "ethereumjs-util": "^5.1.1", + "tweetnacl": "^1.0.0", + "tweetnacl-util": "^0.15.0" }, "dependencies": { - "ethereumjs-abi": { - "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", - "requires": { - "bn.js": "^4.10.0", - "ethereumjs-util": "^5.0.0" - } + "tweetnacl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.0.tgz", + "integrity": "sha1-cT2LgY2kIGh0C/aDhtBHnmb8ins=" } } }, @@ -2679,6 +2713,14 @@ "resolved": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.2.0.tgz", "integrity": "sha512-XOnAR/3rntJgbCdGhqdaLIxDLWKLmsZOGhHdBKadEr6gEnJLH52k93Ou+TUdFaPN3hJc3isBZBal3U/XZ15abA==" }, + "ethereumjs-abi": { + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", + "requires": { + "bn.js": "^4.10.0", + "ethereumjs-util": "^5.0.0" + } + }, "ethereumjs-vm": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/ethereumjs-vm/-/ethereumjs-vm-2.3.4.tgz", @@ -2721,6 +2763,17 @@ "tape": "^4.4.0", "xhr": "^2.2.0", "xtend": "^4.0.1" + }, + "dependencies": { + "eth-sig-util": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", + "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", + "requires": { + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", + "ethereumjs-util": "^5.1.1" + } + } } } } @@ -4186,6 +4239,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", + "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==" + }, "immediate": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.2.3.tgz", @@ -9191,6 +9249,11 @@ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", "optional": true }, + "tweetnacl-util": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.0.tgz", + "integrity": "sha1-RXbBzuXi1j0gf+5S8boCgZSAvHU=" + }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -9580,6 +9643,23 @@ "pify": "^2.3.0", "tape": "^4.6.3" } + }, + "eth-sig-util": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", + "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", + "requires": { + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", + "ethereumjs-util": "^5.1.1" + } + }, + "ethereumjs-abi": { + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", + "requires": { + "bn.js": "^4.10.0", + "ethereumjs-util": "^5.0.0" + } } } }, diff --git a/package.json b/package.json index b9d8bb89f55..4ba61fd7d67 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "eth-keyring-controller": "^4.0.0", "eth-phishing-detect": "^1.1.13", "eth-query": "^2.1.2", + "eth-sig-util": "^2.1.0", "ethereumjs-util": "^5.2.0", "ethereumjs-wallet": "0.6.0", "ethjs-query": "^0.3.8", diff --git a/src/TypedMessageManager.test.ts b/src/TypedMessageManager.test.ts new file mode 100644 index 00000000000..9faee83605b --- /dev/null +++ b/src/TypedMessageManager.test.ts @@ -0,0 +1,442 @@ +import TypedMessageManager from './TypedMessageManager'; + +describe('TypedMessageManager', () => { + it('should set default state', () => { + const controller = new TypedMessageManager(); + expect(controller.state).toEqual({ unapprovedMessages: {}, unapprovedMessagesCount: 0 }); + }); + + it('should set default config', () => { + const controller = new TypedMessageManager(); + expect(controller.config).toEqual({}); + }); + + it('should add a valid message', async () => { + const controller = new TypedMessageManager(); + const messageId = '1'; + const from = '0x0123'; + const messageTime = Date.now(); + const messageStatus = 'unapproved'; + const messageType = 'eth_signTypedData'; + const messageData = [ + { + name: 'Message', + type: 'string', + value: 'Hi, Alice!' + }, + { + name: 'A number', + type: 'uint32', + value: '1337' + } + ]; + controller.addMessage({ + id: messageId, + messageParams: { + data: messageData, + from + }, + status: messageStatus, + time: messageTime, + type: messageType + }); + const message = controller.getMessage(messageId); + expect(message).not.toBe(undefined); + if (message) { + expect(message.id).toBe(messageId); + expect(message.messageParams.from).toBe(from); + expect(message.messageParams.data).toBe(messageData); + expect(message.time).toBe(messageTime); + expect(message.status).toBe(messageStatus); + expect(message.type).toBe(messageType); + } + }); + + it('should reject a message', () => { + return new Promise(async (resolve) => { + const controller = new TypedMessageManager(); + const from = '0xc38bf1ad06ef69f0c04e29dbeb4152b4175f0a8d'; + const version = 'V1'; + const messageData = [ + { + name: 'Message', + type: 'string', + value: 'Hi, Alice!' + }, + { + name: 'A number', + type: 'uint32', + value: '1337' + } + ]; + const result = controller.addUnapprovedMessageAsync( + { + data: messageData, + from + }, + version + ); + const unapprovedMessages = controller.getUnapprovedMessages(); + const keys = Object.keys(unapprovedMessages); + controller.hub.once(`${keys[0]}:finished`, () => { + expect(unapprovedMessages[keys[0]].messageParams.from).toBe(from); + expect(unapprovedMessages[keys[0]].status).toBe('rejected'); + }); + controller.rejectMessage(keys[0]); + result.catch((error) => { + expect(error.message).toContain('User denied message signature'); + resolve(); + }); + }); + }); + + it('should sign a message', () => { + return new Promise(async (resolve) => { + const controller = new TypedMessageManager(); + const from = '0xc38bf1ad06ef69f0c04e29dbeb4152b4175f0a8d'; + const version = 'V1'; + const rawSig = '0x5f7a0'; + const messageData = [ + { + name: 'Message', + type: 'string', + value: 'Hi, Alice!' + }, + { + name: 'A number', + type: 'uint32', + value: '1337' + } + ]; + const result = controller.addUnapprovedMessageAsync( + { + data: messageData, + from + }, + version + ); + const unapprovedMessages = controller.getUnapprovedMessages(); + const keys = Object.keys(unapprovedMessages); + controller.hub.once(`${keys[0]}:finished`, () => { + expect(unapprovedMessages[keys[0]].messageParams.from).toBe(from); + expect(unapprovedMessages[keys[0]].status).toBe('signed'); + }); + controller.setMessageStatusSigned(keys[0], rawSig); + result.then((sig) => { + expect(sig).toEqual(rawSig); + resolve(); + }); + }); + }); + + it('should reject a message', () => { + return new Promise(async (resolve) => { + const controller = new TypedMessageManager(); + const from = '0xc38bf1ad06ef69f0c04e29dbeb4152b4175f0a8d'; + const version = 'V1'; + const messageData = [ + { + name: 'Message', + type: 'string', + value: 'Hi, Alice!' + }, + { + name: 'A number', + type: 'uint32', + value: '1337' + } + ]; + const result = controller.addUnapprovedMessageAsync( + { + data: messageData, + from + }, + version + ); + const unapprovedMessages = controller.getUnapprovedMessages(); + const keys = Object.keys(unapprovedMessages); + controller.hub.once(`${keys[0]}:finished`, () => { + expect(unapprovedMessages[keys[0]].messageParams.from).toBe(from); + expect(unapprovedMessages[keys[0]].status).toBe('errored'); + }); + controller.setMessageStatusErrored(keys[0], 'error message'); + result.catch((error) => { + expect(error.message).toContain('MetaMask Message Signature: error message'); + resolve(); + }); + }); + }); + + it('should throw when unapproved finishes', () => { + return new Promise(async (resolve) => { + const controller = new TypedMessageManager(); + const from = '0xc38bf1ad06ef69f0c04e29dbeb4152b4175f0a8d'; + const version = 'V1'; + const messageData = [ + { + name: 'Message', + type: 'string', + value: 'Hi, Alice!' + }, + { + name: 'A number', + type: 'uint32', + value: '1337' + } + ]; + const result = controller.addUnapprovedMessageAsync( + { + data: messageData, + from + }, + version + ); + const unapprovedMessages = controller.getUnapprovedMessages(); + const keys = Object.keys(unapprovedMessages); + controller.hub.emit(`${keys[0]}:finished`, unapprovedMessages[keys[0]]); + result.catch((error) => { + expect(error.message).toContain('Unknown problem'); + resolve(); + }); + }); + }); + + it('should add a valid unapproved message', async () => { + const controller = new TypedMessageManager(); + const messageStatus = 'unapproved'; + const messageType = 'eth_signTypedData'; + const version = 'version'; + const messageData = [ + { + name: 'Message', + type: 'string', + value: 'Hi, Alice!' + }, + { + name: 'A number', + type: 'uint32', + value: '1337' + } + ]; + const messageParams = { + data: messageData, + from: '0xfoO' + }; + const originalRequest = { origin: 'origin' }; + const messageId = controller.addUnapprovedMessage(messageParams, version, originalRequest); + expect(messageId).not.toBe(undefined); + const message = controller.getMessage(messageId); + if (message) { + expect(message.messageParams.from).toBe(messageParams.from); + expect(message.messageParams.data).toBe(messageParams.data); + expect(message.time).not.toBe(undefined); + expect(message.status).toBe(messageStatus); + expect(message.type).toBe(messageType); + } + }); + + it('should throw when adding invalid legacy typed message', () => { + const from = '0xc38bf1ad06ef69f0c04e29dbeb4152b4175f0a8d'; + const messageData = '0x879'; + const version = 'V1'; + return new Promise(async (resolve) => { + const controller = new TypedMessageManager(); + try { + await controller.addUnapprovedMessageAsync( + { + data: messageData, + from + }, + version + ); + } catch (error) { + expect(error.message).toContain('Invalid message "data":'); + resolve(); + } + }); + }); + + it('should throw when adding invalid typed message', () => { + const from = '0xc38bf1ad06ef69f0c04e29dbeb4152b4175f0a8d'; + const messageData = [ + { + name: 'Message', + type: 'string', + value: 'Hi, Alice!' + }, + { + name: 'A number', + type: 'uint32', + value: '1337' + } + ]; + const version = 'V3'; + return new Promise(async (resolve) => { + const controller = new TypedMessageManager(); + try { + await controller.addUnapprovedMessageAsync( + { + data: messageData, + from + }, + version + ); + } catch (error) { + expect(error.message).toContain('Invalid message "data":'); + resolve(); + } + }); + }); + + it('should get correct unapproved messages', () => { + const firstMessageData = [ + { + name: 'Message', + type: 'string', + value: 'Hi, Alice!' + }, + { + name: 'A number', + type: 'uint32', + value: '1337' + } + ]; + const secondMessageData = [ + { + name: 'Message', + type: 'string', + value: 'Hi, Alice!' + }, + { + name: 'A number', + type: 'uint32', + value: '1337' + } + ]; + const firstMessage = { + id: '1', + messageParams: { from: '0x1', data: firstMessageData }, + status: 'unapproved', + time: 123, + type: 'eth_signTypedData' + }; + const secondMessage = { + id: '2', + messageParams: { from: '0x1', data: secondMessageData }, + status: 'unapproved', + time: 123, + type: 'eth_signTypedData' + }; + const controller = new TypedMessageManager(); + controller.addMessage(firstMessage); + controller.addMessage(secondMessage); + expect(controller.getUnapprovedMessagesCount()).toEqual(2); + expect(controller.getUnapprovedMessages()).toEqual({ + [firstMessage.id]: firstMessage, + [secondMessage.id]: secondMessage + }); + }); + + it('should approve typed message', async () => { + const controller = new TypedMessageManager(); + const messageData = [ + { + name: 'Message', + type: 'string', + value: 'Hi, Alice!' + }, + { + name: 'A number', + type: 'uint32', + value: '1337' + } + ]; + const firstMessage = { from: '0xfoO', data: messageData }; + const version = 'V1'; + const messageId = controller.addUnapprovedMessage(firstMessage, version); + const messageParams = await controller.approveMessage({ ...firstMessage, metamaskId: messageId, version }); + const message = controller.getMessage(messageId); + expect(messageParams).toEqual(firstMessage); + expect(message).not.toBe(undefined); + if (message) { + expect(message.status).toEqual('approved'); + } + }); + + it('should set message status signed', () => { + const controller = new TypedMessageManager(); + const messageData = [ + { + name: 'Message', + type: 'string', + value: 'Hi, Alice!' + }, + { + name: 'A number', + type: 'uint32', + value: '1337' + } + ]; + const firstMessage = { from: '0xfoO', data: messageData }; + const version = 'V1'; + const rawSig = '0x5f7a0'; + const messageId = controller.addUnapprovedMessage(firstMessage, version); + controller.setMessageStatusSigned(messageId, rawSig); + const message = controller.getMessage(messageId); + expect(message).not.toBe(undefined); + if (message) { + expect(message.rawSig).toEqual(rawSig); + expect(message.status).toEqual('signed'); + } + }); + + it('should reject message', () => { + const controller = new TypedMessageManager(); + const messageData = [ + { + name: 'Message', + type: 'string', + value: 'Hi, Alice!' + }, + { + name: 'A number', + type: 'uint32', + value: '1337' + } + ]; + const firstMessage = { from: '0xfoO', data: messageData }; + const version = 'V1'; + const messageId = controller.addUnapprovedMessage(firstMessage, version); + controller.rejectMessage(messageId); + const message = controller.getMessage(messageId); + expect(message).not.toBe(undefined); + if (message) { + expect(message.status).toEqual('rejected'); + } + }); + + it('should set message status errored', () => { + const controller = new TypedMessageManager(); + const messageData = [ + { + name: 'Message', + type: 'string', + value: 'Hi, Alice!' + }, + { + name: 'A number', + type: 'uint32', + value: '1337' + } + ]; + const firstMessage = { from: '0xfoO', data: messageData }; + const version = 'V1'; + const messageId = controller.addUnapprovedMessage(firstMessage, version); + controller.setMessageStatusErrored(messageId, 'errored'); + const message = controller.getMessage(messageId); + expect(message).not.toBe(undefined); + if (message) { + expect(message.status).toEqual('errored'); + } + }); +}); diff --git a/src/TypedMessageManager.ts b/src/TypedMessageManager.ts new file mode 100644 index 00000000000..9c6abea3ed3 --- /dev/null +++ b/src/TypedMessageManager.ts @@ -0,0 +1,379 @@ +import { EventEmitter } from 'events'; +import BaseController, { BaseConfig, BaseState } from './BaseController'; +import { validateTypedSignMessageV3Data, validateTypedSignMessageV1Data } from './util'; +import NetworkController from './NetworkController'; + +const random = require('uuid/v1'); + +/** + * @type TypedMessage + * + * Represents and contains data about an 'eth_signTypedData' type signature request. + * These are created when a signature for an eth_signTypedData call is requested. + * + * @property id - An id to track and identify the message object + * @property error - Error corresponding to eth_signTypedData error in failure case + * @property messageParams - The parameters to pass to the eth_signTypedData method once + * the signature request is approved + * @property type - The json-prc signing method for which a signature request has been made. + * A 'TypedMessage' which always has a 'eth_signTypedData' type + * @property rawSig - Raw data of the signature request + */ +export interface TypedMessage { + id: string; + error?: string; + messageParams: TypedMessageParams; + time: number; + status: string; + type: string; + rawSig?: string; +} + +/** + * @type TypedMessageParams + * + * Represents the parameters to pass to the eth_signTypedData method once the signature request is approved. + * + * @property data - A hex string conversion of the raw buffer data of the signature request + * @property from - Address to sign this message from + * @property origin? - Added for request origin identification + */ +export interface TypedMessageParams { + data: object[] | string; + from: string; + origin?: string; +} + +/** + * @type TypedMessageParamsMetamask + * + * Represents the parameters to pass to the personal_sign method once the signature request is approved + * plus data added by MetaMask. + * + * @property metamaskId - Added for tracking and identification within MetaMask + * @property data - A hex string conversion of the raw buffer data of the signature request + * @property from - Address to sign this message from + * @property origin? - Added for request origin identification + * @property version - Compatibility version EIP712 + */ +export interface TypedMessageParamsMetamask { + metamaskId: string; + data: object[] | string; + error?: string; + from: string; + origin?: string; + version: string; +} + +/** + * @type OriginalRequest + * + * Represents the original request object for adding a message. + * + * @property origin? - Is it is specified, represents the origin + */ +export interface OriginalRequest { + origin?: string; +} + +/** + * @type PersonalMessageManagerState + * + * Message Manager state + * + * @property unapprovedMessages - A collection of all Messages in the 'unapproved' state + * @property unapprovedMessagesCount - The count of all Messages in this.memStore.unapprobedMessages + */ +export interface TypedMessageManagerState extends BaseState { + unapprovedMessages: { [key: string]: TypedMessage }; + unapprovedMessagesCount: number; +} + +/** + * Controller in charge of managing - storing, adding, removing, updating - TypedMessages. + */ +export class TypedMessageManager extends BaseController { + private messages: TypedMessage[]; + + /** + * Saves the unapproved TypedMessages, and their count to state + * + */ + private saveMessageList() { + const unapprovedMessages = this.getUnapprovedMessages(); + const unapprovedMessagesCount = this.getUnapprovedMessagesCount(); + this.update({ unapprovedMessages, unapprovedMessagesCount }); + this.hub.emit('updateBadge'); + } + + /** + * Updates the status of a TypedMessage in this.messages + * + * @param messageId - The id of the TypedMessage to update + * @param status - The new status of the TypedMessage + */ + private setMessageStatus(messageId: string, status: string) { + const message = this.getMessage(messageId); + /* istanbul ignore if */ + if (!message) { + throw new Error(`TypedMessageManager - Message not found for id: ${messageId}.`); + } + message.status = status; + this.updateMessage(message); + this.hub.emit(`${messageId}:${status}`, message); + if (status === 'rejected' || status === 'signed' || status === 'errored') { + this.hub.emit(`${messageId}:finished`, message); + } + } + + /** + * Sets a TypedMessage in this.messages to the passed TypedMessage if the ids are equal. + * Then saves the unapprovedMessage list to storage + * + * @param message - A TypedMessage that will replace an existing TypedMessage (with the id) in this.messages + */ + private updateMessage(message: TypedMessage) { + const index = this.messages.findIndex((msg) => message.id === msg.id); + /* istanbul ignore next */ + if (index !== -1) { + this.messages[index] = message; + } + this.saveMessageList(); + } + + /** + * EventEmitter instance used to listen to specific message events + */ + hub = new EventEmitter(); + + /** + * Name of this controller used during composition + */ + name = 'TypedMessageManager'; + + /** + * List of required sibling controllers this controller needs to function + */ + requiredControllers = ['NetworkController']; + + /** + * Creates a TypedMessageManager instance + * + * @param config - Initial options used to configure this controller + * @param state - Initial state to set on this controller + */ + constructor(config?: Partial, state?: Partial) { + super(config, state); + this.defaultState = { + unapprovedMessages: {}, + unapprovedMessagesCount: 0 + }; + this.messages = []; + this.initialize(); + } + + /** + * A getter for the number of 'unapproved' TypedMessages in this.messages + * + * @returns - The number of 'unapproved' TypedMessages in this.messages + * + */ + getUnapprovedMessagesCount() { + return Object.keys(this.getUnapprovedMessages()).length; + } + + /** + * A getter for the 'unapproved' TypedMessages in state messages + * + * @returns - An index of TypedMessage ids to TypedMessages, for all 'unapproved' TypedMessages in this.messages + * + */ + getUnapprovedMessages() { + return this.messages + .filter((message) => message.status === 'unapproved') + .reduce((result: { [key: string]: TypedMessage }, message: TypedMessage) => { + result[message.id] = message; + return result; + }, {}) as { [key: string]: TypedMessage }; + } + + /** + * Creates a new TypedMessage with an 'unapproved' status using the passed messageParams. + * this.addMessage is called to add the new TypedMessage to this.messages, and to save the unapproved TypedMessages. + * + * @param messageParams - The params for the eth_signTypedData call to be made after the message is approved + * @param version - Compatibility version EIP712 + * @param req? - The original request object possibly containing the origin + * @returns - Promise resolving to the raw data of the signature request + */ + addUnapprovedMessageAsync( + messageParams: TypedMessageParams, + version: string, + req?: OriginalRequest + ): Promise { + return new Promise((resolve, reject) => { + const network = this.context.NetworkController as NetworkController; + /* istanbul ignore next */ + const currentNetworkID = network ? network.state.network : '1'; + const chainId = parseInt(currentNetworkID, undefined); + if (version === 'V1') { + validateTypedSignMessageV1Data(messageParams); + } + if (version === 'V3') { + validateTypedSignMessageV3Data(messageParams, chainId); + } + const messageId = this.addUnapprovedMessage(messageParams, version, req); + this.hub.once(`${messageId}:finished`, (data: TypedMessage) => { + switch (data.status) { + case 'signed': + return resolve(data.rawSig); + case 'rejected': + return reject(new Error('MetaMask Personal Message Signature: User denied message signature.')); + case 'errored': + return reject(new Error(`MetaMask Message Signature: ${data.error}`)); + default: + return reject( + new Error( + `MetaMask Personal Message Signature: Unknown problem: ${JSON.stringify(messageParams)}` + ) + ); + } + }); + }); + } + + /** + * Creates a new TypedMessage with an 'unapproved' status using the passed messageParams. + * this.addMessage is called to add the new TypedMessage to this.messages, and to save the + * unapproved TypedMessages. + * + * @param messageParams - The params for the eth_signTypedData call to be made after the message + * is approved + * @param version - Compatibility version EIP712 + * @param req? - The original request object possibly containing the origin + * @returns - The id of the newly created TypedMessage + */ + addUnapprovedMessage(messageParams: TypedMessageParams, version: string, req?: OriginalRequest) { + const messageId = random(); + const messageParamsMetamask = { ...messageParams, metamaskId: messageId, version }; + if (req) { + messageParams.origin = req.origin; + } + const messageData: TypedMessage = { + id: messageId, + messageParams, + status: 'unapproved', + time: Date.now(), + type: 'eth_signTypedData' + }; + this.addMessage(messageData); + this.hub.emit(`unapprovedMessage`, messageParamsMetamask); + return messageId; + } + + /** + * Adds a passed TypedMessage to this.messages, and calls this.saveMessageList() to save + * the unapproved TypedMessages from that list to this.messages. + * + * @param {Message} message The Message to add to this.messages + * + */ + addMessage(message: TypedMessage) { + this.messages.push(message); + this.saveMessageList(); + } + + /** + * Returns a specified TypedMessage. + * + * @param messageId - The id of the TypedMessage to get + * @returns - The TypedMessage with the id that matches the passed messageId, or undefined + * if no Message has that id. + * + */ + getMessage(messageId: string) { + return this.messages.find((message) => message.id === messageId); + } + + /** + * Approves a TypedMessage. Sets the message status via a call to this.setMessageStatusApproved, + * and returns a promise with any the message params modified for proper signing. + * + * @param messageParams - The messageParams to be used when personal_sign is called, + * plus data added by MetaMask + * @returns - Promise resolving to the messageParams with the metamaskId property removed + */ + approveMessage(messageParams: TypedMessageParamsMetamask): Promise { + this.setMessageStatusApproved(messageParams.metamaskId); + return this.prepMessageForSigning(messageParams); + } + + /** + * Sets a TypedMessage status to 'approved' via a call to this.setMessageStatus. + * + * @param messageId - The id of the TypedMessage to approve + */ + setMessageStatusApproved(messageId: string) { + this.setMessageStatus(messageId, 'approved'); + } + + /** + * Sets a TypedMessage status to 'errored' via a call to this.setMessageStatus. + * + * @param messageId - The id of the TypedMessage to error + * @param error - The error to be included in TypedMessage + */ + setMessageStatusErrored(messageId: string, error: string) { + const message = this.getMessage(messageId); + /* istanbul ignore if */ + if (!message) { + return; + } + message.error = error; + this.updateMessage(message); + this.setMessageStatus(messageId, 'errored'); + } + + /** + * Sets a TypedMessage status to 'signed' via a call to this.setMessageStatus and updates + * that TypedMessage in this.messages by adding the raw signature data of the signature + * request to the TypedMessage. + * + * @param messageId - The id of the TypedMessage to sign + * @param rawSig - The raw data of the signature request + */ + setMessageStatusSigned(messageId: string, rawSig: string) { + const message = this.getMessage(messageId); + /* istanbul ignore if */ + if (!message) { + return; + } + message.rawSig = rawSig; + this.updateMessage(message); + this.setMessageStatus(messageId, 'signed'); + } + + /** + * Removes the metamaskId and version property from passed messageParams and returns a promise which + * resolves the updated messageParams + * + * @param messageParams - The messageParams to modify + * @returns - Promise resolving to the messageParams with the metamaskId and version properties removed + */ + prepMessageForSigning(messageParams: TypedMessageParamsMetamask): Promise { + delete messageParams.metamaskId; + delete messageParams.version; + return Promise.resolve(messageParams); + } + + /** + * Sets a TypedMessage status to 'rejected' via a call to this.setMessageStatus. + * + * @param messageId - The id of the TypedMessage to reject. + */ + rejectMessage(messageId: string) { + this.setMessageStatus(messageId, 'rejected'); + } +} + +export default TypedMessageManager; diff --git a/src/util.test.ts b/src/util.test.ts index cb3f578c851..0ea2745ee3a 100644 --- a/src/util.test.ts +++ b/src/util.test.ts @@ -185,4 +185,143 @@ describe('util', () => { ).toThrow(); }); }); + + describe('validateTypedMessageDataV1', () => { + it('should throw if no from address legacy', () => { + expect(() => + util.validateTypedSignMessageV1Data({ + data: [] + } as any) + ).toThrow('Invalid "from" address:'); + }); + + it('should throw if invalid from address', () => { + expect(() => + util.validateTypedSignMessageV1Data({ + data: [], + from: '3244e191f1b4903970224322180f1fbbc415696b' + } as any) + ).toThrow('Invalid "from" address:'); + }); + + it('should throw if invalid type from address', () => { + expect(() => + util.validateTypedSignMessageV1Data({ + data: [], + from: 123 + } as any) + ).toThrow('Invalid "from" address:'); + }); + + it('should throw if incorrect data', () => { + expect(() => + util.validateTypedSignMessageV1Data({ + data: '0x879a05', + from: '0x3244e191f1b4903970224322180f1fbbc415696b' + } as any) + ).toThrow('Invalid message "data":'); + }); + + it('should throw if no data', () => { + expect(() => + util.validateTypedSignMessageV1Data({ + data: '0x879a05', + from: '0x3244e191f1b4903970224322180f1fbbc415696b' + } as any) + ).toThrow('Invalid message "data":'); + }); + + it('should throw if invalid type data', () => { + expect(() => + util.validateTypedSignMessageV1Data({ + data: [], + from: '0x3244e191f1b4903970224322180f1fbbc415696b' + } as any) + ).toThrow('Expected EIP712 typed data.'); + }); + }); + + describe('validateTypedMessageDataV3', () => { + it('should throw if no from address', () => { + expect(() => + util.validateTypedSignMessageV3Data( + { + data: '0x879a05' + } as any, + 1 + ) + ).toThrow('Invalid "from" address:'); + }); + + it('should throw if invalid from address', () => { + expect(() => + util.validateTypedSignMessageV3Data( + { + data: '0x879a05', + from: '3244e191f1b4903970224322180f1fbbc415696b' + } as any, + 1 + ) + ).toThrow('Invalid "from" address:'); + }); + + it('should throw if invalid type from address', () => { + expect(() => + util.validateTypedSignMessageV3Data( + { + data: '0x879a05', + from: 123 + } as any, + 1 + ) + ).toThrow('Invalid "from" address:'); + }); + + it('should throw if array data', () => { + expect(() => + util.validateTypedSignMessageV3Data( + { + data: [], + from: '0x3244e191f1b4903970224322180f1fbbc415696b' + } as any, + 1 + ) + ).toThrow('Invalid message "data":'); + }); + + it('should throw if no array data', () => { + expect(() => + util.validateTypedSignMessageV3Data( + { + from: '0x3244e191f1b4903970224322180f1fbbc415696b' + } as any, + 1 + ) + ).toThrow('Invalid message "data":'); + }); + + it('should throw if no json valid data', () => { + expect(() => + util.validateTypedSignMessageV3Data( + { + data: 'uh oh', + from: '0x3244e191f1b4903970224322180f1fbbc415696b' + } as any, + 1 + ) + ).toThrow('Data must be passed as a valid JSON string.'); + }); + + it('should throw if data not in typed message schema', () => { + expect(() => + util.validateTypedSignMessageV3Data( + { + data: '{"greetings":"I am Alice"}', + from: '0x3244e191f1b4903970224322180f1fbbc415696b' + } as any, + 1 + ) + ).toThrow('Data must conform to EIP-712 schema.'); + }); + }); }); diff --git a/src/util.ts b/src/util.ts index c07cbc8be60..ff4f14e6911 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,5 +1,7 @@ import { Transaction } from './TransactionController'; import { MessageParams } from './PersonalMessageManager'; +import { TypedMessageParams } from './TypedMessageManager'; +const sigUtil = require('eth-sig-util'); const { addHexPrefix, BN, isValidAddress, stripHexPrefix, bufferToHex } = require('ethereumjs-util'); const hexRe = /^[0-9A-Fa-f]+$/g; @@ -185,6 +187,58 @@ export function validatePersonalSignMessageData(messageData: MessageParams) { } } +/** + * Validates a TypedMessageParams object for required properties and throws in + * the event of any validation error for eth_signTypedMessage_V1. + * + * @param messageData - TypedMessageParams object to validate + * @param activeChainId - Active chain id + */ +export function validateTypedSignMessageV1Data(messageData: TypedMessageParams) { + if (!messageData.from || typeof messageData.from !== 'string' || !isValidAddress(messageData.from)) { + throw new Error(`Invalid "from" address: ${messageData.from} must be a valid string.`); + } + if (!messageData.data || !Array.isArray(messageData.data)) { + throw new Error(`Invalid message "data": ${messageData.data} must be a valid array.`); + } + try { + sigUtil.typedSignatureHash(messageData.data); + } catch (e) { + throw new Error(`Expected EIP712 typed data.`); + } +} + +/** + * Validates a TypedMessageParams object for required properties and throws in + * the event of any validation error for eth_signTypedMessage_V3. + * + * @param messageData - TypedMessageParams object to validate + * @param activeChainId - Active chain id + */ +export function validateTypedSignMessageV3Data(messageData: TypedMessageParams, activeChainId: number) { + if (!messageData.from || typeof messageData.from !== 'string' || !isValidAddress(messageData.from)) { + throw new Error(`Invalid "from" address: ${messageData.from} must be a valid string.`); + } + if (!messageData.data || typeof messageData.data !== 'string') { + throw new Error(`Invalid message "data": ${messageData.data} must be a valid array.`); + } + let data; + try { + data = JSON.parse(messageData.data); + } catch (e) { + throw new Error('Data must be passed as a valid JSON string.'); + } + try { + // jsonschema.validate(data, sigUtil.TYPED_MESSAGE_SCHEMA) + } catch (e) { + throw new Error('Data must conform to EIP-712 schema. See https://git.io/fNtcx.'); + } + const chainId = data.domain.chainId; + if (chainId !== activeChainId) { + throw new Error(`Provided chainId (${chainId}) must match the active chainId (${activeChainId})`); + } +} + /** * Modifies collectible images URI in case is necessary * @@ -212,5 +266,7 @@ export default { manageCollectibleImage, normalizeTransaction, safelyExecute, - validateTransaction + validateTransaction, + validateTypedSignMessageV1Data, + validateTypedSignMessageV3Data }; From 46d8cd1931dd69a3af96b5d94b6fe1a752e84c2c Mon Sep 17 00:00:00 2001 From: Esteban MIno Date: Fri, 30 Nov 2018 16:58:57 -0300 Subject: [PATCH 02/18] add signTypedMessageV3 data validation tests --- package-lock.json | 5 +++++ package.json | 1 + src/util.test.ts | 26 ++++++++++++++++++++++++++ src/util.ts | 8 ++++---- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index e25371a90f7..7cb520dd7a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5805,6 +5805,11 @@ "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" }, + "jsonschema": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.4.tgz", + "integrity": "sha512-lz1nOH69GbsVHeVgEdvyavc/33oymY1AZwtePMiMj4HZPMbP5OIKK3zT9INMWjwua/V4Z4yq7wSlBbSG+g4AEw==" + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", diff --git a/package.json b/package.json index 4ba61fd7d67..c17222e55d4 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "human-standard-collectible-abi": "^1.0.2", "human-standard-token-abi": "^2.0.0", "isomorphic-fetch": "^2.2.1", + "jsonschema": "^1.2.4", "percentile": "^1.2.1", "uuid": "^3.3.2", "web3": "^0.20.7", diff --git a/src/util.test.ts b/src/util.test.ts index 0ea2745ee3a..4bbf259ea8f 100644 --- a/src/util.test.ts +++ b/src/util.test.ts @@ -242,6 +242,8 @@ describe('util', () => { }); describe('validateTypedMessageDataV3', () => { + const dataTyped = + '{"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 Mail","version":"1","chainId":1,"verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"},"message":{"from":{"name":"Cow","wallet":"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"},"to":{"name":"Bob","wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"},"contents":"Hello, Bob!"}}'; it('should throw if no from address', () => { expect(() => util.validateTypedSignMessageV3Data( @@ -323,5 +325,29 @@ describe('util', () => { ) ).toThrow('Data must conform to EIP-712 schema.'); }); + + it('should throw if signing in a different chainId', () => { + expect(() => + util.validateTypedSignMessageV3Data( + { + data: dataTyped, + from: '0x3244e191f1b4903970224322180f1fbbc415696b' + } as any, + 2 + ) + ).toThrow('Provided chainId (1) must match the active chainId (2)'); + }); + + it('should not throw if data is correct', () => { + expect(() => + util.validateTypedSignMessageV3Data( + { + data: dataTyped, + from: '0x3244e191f1b4903970224322180f1fbbc415696b' + } as any, + 1 + ) + ).not.toThrow(); + }); }); }); diff --git a/src/util.ts b/src/util.ts index ff4f14e6911..93f83942534 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,7 +1,8 @@ import { Transaction } from './TransactionController'; import { MessageParams } from './PersonalMessageManager'; -import { TypedMessageParams } from './TypedMessageManager'; +import { TypedMessageParams, TypedData } from './TypedMessageManager'; const sigUtil = require('eth-sig-util'); +const jsonschema = require('jsonschema'); const { addHexPrefix, BN, isValidAddress, stripHexPrefix, bufferToHex } = require('ethereumjs-util'); const hexRe = /^[0-9A-Fa-f]+$/g; @@ -228,9 +229,8 @@ export function validateTypedSignMessageV3Data(messageData: TypedMessageParams, } catch (e) { throw new Error('Data must be passed as a valid JSON string.'); } - try { - // jsonschema.validate(data, sigUtil.TYPED_MESSAGE_SCHEMA) - } catch (e) { + const validation = jsonschema.validate(data, sigUtil.TYPED_MESSAGE_SCHEMA); + if (validation.errors.length > 0) { throw new Error('Data must conform to EIP-712 schema. See https://git.io/fNtcx.'); } const chainId = data.domain.chainId; From 4ad299572e22e9881209d0db80f19c27e5af5602 Mon Sep 17 00:00:00 2001 From: Esteban MIno Date: Fri, 30 Nov 2018 19:27:42 -0300 Subject: [PATCH 03/18] rename util methods for sign typed data --- package-lock.json | 76 ++++++++++++++++++++++++++++---------- src/TypedMessageManager.ts | 6 +-- src/util.test.ts | 30 +++++++-------- src/util.ts | 10 ++--- 4 files changed, 80 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7cb520dd7a3..5b3c3b9803b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1499,10 +1499,6 @@ "tweetnacl": "^0.14.3" } }, - "bignumber.js": { - "version": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934", - "from": "git+https://github.com/frozeman/bignumber.js-nolookahead.git" - }, "binary-extensions": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", @@ -2509,13 +2505,12 @@ "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-2.0.1.tgz", "integrity": "sha512-lxHZOQspexk3DaGj4RBbWy4C/qNOWRnxpaJzNnYD3WEmC8shcJ4tHs7Xv878rzvILfJnSFSCCiKQhng1m80oBQ==", "requires": { - "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", "ethereumjs-util": "^5.1.1" }, "dependencies": { "ethereumjs-abi": { "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", "requires": { "bn.js": "^4.10.0", "ethereumjs-util": "^5.0.0" @@ -2579,8 +2574,17 @@ "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", "requires": { - "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", "ethereumjs-util": "^5.1.1" + }, + "dependencies": { + "ethereumjs-abi": { + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", + "requires": { + "bn.js": "^4.10.0", + "ethereumjs-util": "^5.0.0" + } + } } }, "ethereumjs-abi": { @@ -2648,13 +2652,12 @@ "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-2.0.1.tgz", "integrity": "sha512-lxHZOQspexk3DaGj4RBbWy4C/qNOWRnxpaJzNnYD3WEmC8shcJ4tHs7Xv878rzvILfJnSFSCCiKQhng1m80oBQ==", "requires": { - "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", "ethereumjs-util": "^5.1.1" }, "dependencies": { "ethereumjs-abi": { "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", "requires": { "bn.js": "^4.10.0", "ethereumjs-util": "^5.0.0" @@ -2770,9 +2773,16 @@ "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", "requires": { - "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", "ethereumjs-util": "^5.1.1" } + }, + "ethereumjs-abi": { + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", + "requires": { + "bn.js": "^4.10.0", + "ethereumjs-util": "^5.0.0" + } } } } @@ -3347,13 +3357,15 @@ "version": "1.0.0", "resolved": false, "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "resolved": false, "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3370,19 +3382,22 @@ "version": "1.1.0", "resolved": false, "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "resolved": false, "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "resolved": false, "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3513,7 +3528,8 @@ "version": "2.0.3", "resolved": false, "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -3527,6 +3543,7 @@ "resolved": false, "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3543,6 +3560,7 @@ "resolved": false, "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3551,13 +3569,15 @@ "version": "0.0.8", "resolved": false, "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "resolved": false, "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -3578,6 +3598,7 @@ "resolved": false, "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3666,7 +3687,8 @@ "version": "1.0.1", "resolved": false, "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3680,6 +3702,7 @@ "resolved": false, "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -3817,6 +3840,7 @@ "resolved": false, "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -9600,11 +9624,16 @@ "resolved": "https://registry.npmjs.org/web3/-/web3-0.20.7.tgz", "integrity": "sha512-VU6/DSUX93d1fCzBz7WP/SGCQizO1rKZi4Px9j/3yRyfssHyFcZamMw2/sj4E8TlfMXONvZLoforR8B4bRoyTQ==", "requires": { - "bignumber.js": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934", "crypto-js": "^3.1.4", "utf8": "^2.1.1", "xhr2-cookies": "^1.1.0", "xmlhttprequest": "*" + }, + "dependencies": { + "bignumber.js": { + "version": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934", + "from": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934" + } } }, "web3-provider-engine": { @@ -9654,8 +9683,17 @@ "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", "requires": { - "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", "ethereumjs-util": "^5.1.1" + }, + "dependencies": { + "ethereumjs-abi": { + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", + "requires": { + "bn.js": "^4.10.0", + "ethereumjs-util": "^5.0.0" + } + } } }, "ethereumjs-abi": { diff --git a/src/TypedMessageManager.ts b/src/TypedMessageManager.ts index 9c6abea3ed3..a73a59b436e 100644 --- a/src/TypedMessageManager.ts +++ b/src/TypedMessageManager.ts @@ -1,6 +1,6 @@ import { EventEmitter } from 'events'; import BaseController, { BaseConfig, BaseState } from './BaseController'; -import { validateTypedSignMessageV3Data, validateTypedSignMessageV1Data } from './util'; +import { validateTypedSignMessageDataV3, validateTypedSignMessageDataV1 } from './util'; import NetworkController from './NetworkController'; const random = require('uuid/v1'); @@ -217,10 +217,10 @@ export class TypedMessageManager extends BaseController { diff --git a/src/util.test.ts b/src/util.test.ts index 4bbf259ea8f..dadbf9c9c6d 100644 --- a/src/util.test.ts +++ b/src/util.test.ts @@ -189,7 +189,7 @@ describe('util', () => { describe('validateTypedMessageDataV1', () => { it('should throw if no from address legacy', () => { expect(() => - util.validateTypedSignMessageV1Data({ + util.validateTypedSignMessageDataV1({ data: [] } as any) ).toThrow('Invalid "from" address:'); @@ -197,7 +197,7 @@ describe('util', () => { it('should throw if invalid from address', () => { expect(() => - util.validateTypedSignMessageV1Data({ + util.validateTypedSignMessageDataV1({ data: [], from: '3244e191f1b4903970224322180f1fbbc415696b' } as any) @@ -206,7 +206,7 @@ describe('util', () => { it('should throw if invalid type from address', () => { expect(() => - util.validateTypedSignMessageV1Data({ + util.validateTypedSignMessageDataV1({ data: [], from: 123 } as any) @@ -215,7 +215,7 @@ describe('util', () => { it('should throw if incorrect data', () => { expect(() => - util.validateTypedSignMessageV1Data({ + util.validateTypedSignMessageDataV1({ data: '0x879a05', from: '0x3244e191f1b4903970224322180f1fbbc415696b' } as any) @@ -224,7 +224,7 @@ describe('util', () => { it('should throw if no data', () => { expect(() => - util.validateTypedSignMessageV1Data({ + util.validateTypedSignMessageDataV1({ data: '0x879a05', from: '0x3244e191f1b4903970224322180f1fbbc415696b' } as any) @@ -233,7 +233,7 @@ describe('util', () => { it('should throw if invalid type data', () => { expect(() => - util.validateTypedSignMessageV1Data({ + util.validateTypedSignMessageDataV1({ data: [], from: '0x3244e191f1b4903970224322180f1fbbc415696b' } as any) @@ -246,7 +246,7 @@ describe('util', () => { '{"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 Mail","version":"1","chainId":1,"verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"},"message":{"from":{"name":"Cow","wallet":"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"},"to":{"name":"Bob","wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"},"contents":"Hello, Bob!"}}'; it('should throw if no from address', () => { expect(() => - util.validateTypedSignMessageV3Data( + util.validateTypedSignMessageDataV3( { data: '0x879a05' } as any, @@ -257,7 +257,7 @@ describe('util', () => { it('should throw if invalid from address', () => { expect(() => - util.validateTypedSignMessageV3Data( + util.validateTypedSignMessageDataV3( { data: '0x879a05', from: '3244e191f1b4903970224322180f1fbbc415696b' @@ -269,7 +269,7 @@ describe('util', () => { it('should throw if invalid type from address', () => { expect(() => - util.validateTypedSignMessageV3Data( + util.validateTypedSignMessageDataV3( { data: '0x879a05', from: 123 @@ -281,7 +281,7 @@ describe('util', () => { it('should throw if array data', () => { expect(() => - util.validateTypedSignMessageV3Data( + util.validateTypedSignMessageDataV3( { data: [], from: '0x3244e191f1b4903970224322180f1fbbc415696b' @@ -293,7 +293,7 @@ describe('util', () => { it('should throw if no array data', () => { expect(() => - util.validateTypedSignMessageV3Data( + util.validateTypedSignMessageDataV3( { from: '0x3244e191f1b4903970224322180f1fbbc415696b' } as any, @@ -304,7 +304,7 @@ describe('util', () => { it('should throw if no json valid data', () => { expect(() => - util.validateTypedSignMessageV3Data( + util.validateTypedSignMessageDataV3( { data: 'uh oh', from: '0x3244e191f1b4903970224322180f1fbbc415696b' @@ -316,7 +316,7 @@ describe('util', () => { it('should throw if data not in typed message schema', () => { expect(() => - util.validateTypedSignMessageV3Data( + util.validateTypedSignMessageDataV3( { data: '{"greetings":"I am Alice"}', from: '0x3244e191f1b4903970224322180f1fbbc415696b' @@ -328,7 +328,7 @@ describe('util', () => { it('should throw if signing in a different chainId', () => { expect(() => - util.validateTypedSignMessageV3Data( + util.validateTypedSignMessageDataV3( { data: dataTyped, from: '0x3244e191f1b4903970224322180f1fbbc415696b' @@ -340,7 +340,7 @@ describe('util', () => { it('should not throw if data is correct', () => { expect(() => - util.validateTypedSignMessageV3Data( + util.validateTypedSignMessageDataV3( { data: dataTyped, from: '0x3244e191f1b4903970224322180f1fbbc415696b' diff --git a/src/util.ts b/src/util.ts index 93f83942534..fbc95756523 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,6 +1,6 @@ import { Transaction } from './TransactionController'; import { MessageParams } from './PersonalMessageManager'; -import { TypedMessageParams, TypedData } from './TypedMessageManager'; +import { TypedMessageParams } from './TypedMessageManager'; const sigUtil = require('eth-sig-util'); const jsonschema = require('jsonschema'); const { addHexPrefix, BN, isValidAddress, stripHexPrefix, bufferToHex } = require('ethereumjs-util'); @@ -195,7 +195,7 @@ export function validatePersonalSignMessageData(messageData: MessageParams) { * @param messageData - TypedMessageParams object to validate * @param activeChainId - Active chain id */ -export function validateTypedSignMessageV1Data(messageData: TypedMessageParams) { +export function validateTypedSignMessageDataV1(messageData: TypedMessageParams) { if (!messageData.from || typeof messageData.from !== 'string' || !isValidAddress(messageData.from)) { throw new Error(`Invalid "from" address: ${messageData.from} must be a valid string.`); } @@ -216,7 +216,7 @@ export function validateTypedSignMessageV1Data(messageData: TypedMessageParams) * @param messageData - TypedMessageParams object to validate * @param activeChainId - Active chain id */ -export function validateTypedSignMessageV3Data(messageData: TypedMessageParams, activeChainId: number) { +export function validateTypedSignMessageDataV3(messageData: TypedMessageParams, activeChainId: number) { if (!messageData.from || typeof messageData.from !== 'string' || !isValidAddress(messageData.from)) { throw new Error(`Invalid "from" address: ${messageData.from} must be a valid string.`); } @@ -267,6 +267,6 @@ export default { normalizeTransaction, safelyExecute, validateTransaction, - validateTypedSignMessageV1Data, - validateTypedSignMessageV3Data + validateTypedSignMessageDataV1, + validateTypedSignMessageDataV3 }; From c90c154de9aa736d839ef1b432717c00dc2ee25f Mon Sep 17 00:00:00 2001 From: Esteban MIno Date: Fri, 30 Nov 2018 19:33:02 -0300 Subject: [PATCH 04/18] update doc --- src/TypedMessageManager.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/TypedMessageManager.ts b/src/TypedMessageManager.ts index a73a59b436e..d720ed35f60 100644 --- a/src/TypedMessageManager.ts +++ b/src/TypedMessageManager.ts @@ -51,7 +51,7 @@ export interface TypedMessageParams { * plus data added by MetaMask. * * @property metamaskId - Added for tracking and identification within MetaMask - * @property data - A hex string conversion of the raw buffer data of the signature request + * @property data - A hex string conversion of the raw buffer or array of objects of data of the signature request * @property from - Address to sign this message from * @property origin? - Added for request origin identification * @property version - Compatibility version EIP712 @@ -77,12 +77,12 @@ export interface OriginalRequest { } /** - * @type PersonalMessageManagerState + * @type TypedMessageManagerState * - * Message Manager state + * Typed Message Manager state * * @property unapprovedMessages - A collection of all Messages in the 'unapproved' state - * @property unapprovedMessagesCount - The count of all Messages in this.memStore.unapprobedMessages + * @property unapprovedMessagesCount - The count of all Messages in this.unapprovedMessages */ export interface TypedMessageManagerState extends BaseState { unapprovedMessages: { [key: string]: TypedMessage }; @@ -212,14 +212,14 @@ export class TypedMessageManager extends BaseController { return new Promise((resolve, reject) => { - const network = this.context.NetworkController as NetworkController; - /* istanbul ignore next */ - const currentNetworkID = network ? network.state.network : '1'; - const chainId = parseInt(currentNetworkID, undefined); if (version === 'V1') { validateTypedSignMessageDataV1(messageParams); } if (version === 'V3') { + const network = this.context.NetworkController as NetworkController; + /* istanbul ignore next */ + const currentNetworkID = network ? network.state.network : '1'; + const chainId = parseInt(currentNetworkID, undefined); validateTypedSignMessageDataV3(messageParams, chainId); } const messageId = this.addUnapprovedMessage(messageParams, version, req); @@ -228,13 +228,13 @@ export class TypedMessageManager extends BaseController Date: Fri, 30 Nov 2018 21:10:41 -0300 Subject: [PATCH 05/18] add signTypedMessage --- src/KeyringController.ts | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/KeyringController.ts b/src/KeyringController.ts index 43a1ca1d4da..9ab954a0aec 100644 --- a/src/KeyringController.ts +++ b/src/KeyringController.ts @@ -3,7 +3,7 @@ import BaseController, { BaseConfig, BaseState, Listener } from './BaseControlle import PreferencesController from './PreferencesController'; import { Transaction } from './TransactionController'; import { MessageParams } from './PersonalMessageManager'; - +const sigUtil = require('eth-sig-util'); const { toChecksumAddress } = require('ethereumjs-util'); const Keyring = require('eth-keyring-controller'); const Mutex = require('await-semaphore').Mutex; @@ -249,7 +249,7 @@ export class KeyringController extends BaseController } /** - * Signs message by calling down into a specific keyring + * Signs personal message by calling down into a specific keyring * * @param messageParams - MessageParams object to sign * @returns - Promise resolving to a signed message string @@ -258,6 +258,28 @@ export class KeyringController extends BaseController return this.keyring.signPersonalMessage(messageParams); } + /** + * Signs typed message by calling down into a specific keyring + * + * @param messageParams - MessageParams object to sign + * @param version - Compatibility version EIP712 + * @returns - Promise resolving to a signed message string or an error if any + */ + signTypedMessage(messageParams: MessageParams, version: string) { + try { + const address = sigUtil.normalize(messageParams.from); + const privKey = this.exportAccount(address); + switch (version) { + case 'V1': + return sigUtil.signTypedDataLegacy(privKey, { data: messageParams.data }); + case 'V3': + return sigUtil.signTypedData(privKey, { data: messageParams.data }); + } + } catch (error) { + return error; + } + } + /** * Signs a transaction by calling down into a specific keyring * From 91302149e55391151e97a5be224a5c4ad01200bd Mon Sep 17 00:00:00 2001 From: Esteban MIno Date: Fri, 30 Nov 2018 21:35:36 -0300 Subject: [PATCH 06/18] const naming changes --- src/KeyringController.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/KeyringController.ts b/src/KeyringController.ts index 9ab954a0aec..8d89f5ef6e5 100644 --- a/src/KeyringController.ts +++ b/src/KeyringController.ts @@ -268,14 +268,15 @@ export class KeyringController extends BaseController signTypedMessage(messageParams: MessageParams, version: string) { try { const address = sigUtil.normalize(messageParams.from); - const privKey = this.exportAccount(address); + const privateKey = this.exportAccount(address); switch (version) { case 'V1': - return sigUtil.signTypedDataLegacy(privKey, { data: messageParams.data }); + return sigUtil.signTypedDataLegacy(privateKey, { data: messageParams.data }); case 'V3': - return sigUtil.signTypedData(privKey, { data: messageParams.data }); + return sigUtil.signTypedData(privateKey, { data: messageParams.data }); } } catch (error) { + /* istanbul ignore next */ return error; } } From 50bfa25a657fc42d3873ee50d07f482acb62ebd1 Mon Sep 17 00:00:00 2001 From: Esteban MIno Date: Fri, 30 Nov 2018 21:37:46 -0300 Subject: [PATCH 07/18] update test --- src/TypedMessageManager.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TypedMessageManager.test.ts b/src/TypedMessageManager.test.ts index 9faee83605b..6776e813844 100644 --- a/src/TypedMessageManager.test.ts +++ b/src/TypedMessageManager.test.ts @@ -161,7 +161,7 @@ describe('TypedMessageManager', () => { }); controller.setMessageStatusErrored(keys[0], 'error message'); result.catch((error) => { - expect(error.message).toContain('MetaMask Message Signature: error message'); + expect(error.message).toContain('MetaMask Typed Message Signature: error message'); resolve(); }); }); From 4f19000508264b9081a29179ad1774226f81ad73 Mon Sep 17 00:00:00 2001 From: Esteban MIno Date: Sat, 1 Dec 2018 21:08:58 -0300 Subject: [PATCH 08/18] signTypedData working as expected --- src/KeyringController.ts | 9 +++++---- src/TypedMessageManager.ts | 13 +------------ src/index.ts | 1 + 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/KeyringController.ts b/src/KeyringController.ts index 8d89f5ef6e5..e5a1ec7abae 100644 --- a/src/KeyringController.ts +++ b/src/KeyringController.ts @@ -265,15 +265,16 @@ export class KeyringController extends BaseController * @param version - Compatibility version EIP712 * @returns - Promise resolving to a signed message string or an error if any */ - signTypedMessage(messageParams: MessageParams, version: string) { + async signTypedMessage(messageParams: MessageParams, version: string) { try { const address = sigUtil.normalize(messageParams.from); - const privateKey = this.exportAccount(address); + const privateKey = await this.exportAccount(address); + const privateKeyBuffer = ethUtil.toBuffer(ethUtil.addHexPrefix(privateKey)); switch (version) { case 'V1': - return sigUtil.signTypedDataLegacy(privateKey, { data: messageParams.data }); + return sigUtil.signTypedDataLegacy(privateKeyBuffer, { data: messageParams.data }); case 'V3': - return sigUtil.signTypedData(privateKey, { data: messageParams.data }); + return sigUtil.signTypedData(privateKeyBuffer, { data: messageParams.data }); } } catch (error) { /* istanbul ignore next */ diff --git a/src/TypedMessageManager.ts b/src/TypedMessageManager.ts index d720ed35f60..dca80ad7c1f 100644 --- a/src/TypedMessageManager.ts +++ b/src/TypedMessageManager.ts @@ -2,7 +2,7 @@ import { EventEmitter } from 'events'; import BaseController, { BaseConfig, BaseState } from './BaseController'; import { validateTypedSignMessageDataV3, validateTypedSignMessageDataV1 } from './util'; import NetworkController from './NetworkController'; - +import { OriginalRequest } from './PersonalMessageManager'; const random = require('uuid/v1'); /** @@ -65,17 +65,6 @@ export interface TypedMessageParamsMetamask { version: string; } -/** - * @type OriginalRequest - * - * Represents the original request object for adding a message. - * - * @property origin? - Is it is specified, represents the origin - */ -export interface OriginalRequest { - origin?: string; -} - /** * @type TypedMessageManagerState * diff --git a/src/index.ts b/src/index.ts index ba4e9cc9d50..97325a5d7c5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,4 +19,5 @@ export * from './ShapeShiftController'; export * from './TokenBalancesController'; export * from './TokenRatesController'; export * from './TransactionController'; +export * from './TypedMessageManager'; export { util }; From 950b9c849ea13b148886cf5ca2a22a7e44d6ba65 Mon Sep 17 00:00:00 2001 From: Esteban MIno Date: Mon, 3 Dec 2018 20:03:56 -0300 Subject: [PATCH 09/18] use data object to signTypedData V3 --- src/KeyringController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/KeyringController.ts b/src/KeyringController.ts index e5a1ec7abae..4e37334ac06 100644 --- a/src/KeyringController.ts +++ b/src/KeyringController.ts @@ -274,7 +274,7 @@ export class KeyringController extends BaseController case 'V1': return sigUtil.signTypedDataLegacy(privateKeyBuffer, { data: messageParams.data }); case 'V3': - return sigUtil.signTypedData(privateKeyBuffer, { data: messageParams.data }); + return sigUtil.signTypedData(privateKeyBuffer, { data: JSON.parse(messageParams.data) }); } } catch (error) { /* istanbul ignore next */ From 8caea2d275fda0c823648778985c894f11df1d42 Mon Sep 17 00:00:00 2001 From: Esteban MIno Date: Mon, 3 Dec 2018 23:16:30 -0300 Subject: [PATCH 10/18] throw error when signTypedData fails --- src/KeyringController.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/KeyringController.ts b/src/KeyringController.ts index 4e37334ac06..13ae06c8c60 100644 --- a/src/KeyringController.ts +++ b/src/KeyringController.ts @@ -277,8 +277,7 @@ export class KeyringController extends BaseController return sigUtil.signTypedData(privateKeyBuffer, { data: JSON.parse(messageParams.data) }); } } catch (error) { - /* istanbul ignore next */ - return error; + throw new Error('Keyring Controller signTypedMessage: ' + error); } } From c9ca8559a59e79e3091b53a25eede1a082e70b2b Mon Sep 17 00:00:00 2001 From: Esteban MIno Date: Tue, 4 Dec 2018 12:35:32 -0300 Subject: [PATCH 11/18] improve doc --- src/TypedMessageManager.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/TypedMessageManager.ts b/src/TypedMessageManager.ts index dca80ad7c1f..206ef6bce0d 100644 --- a/src/TypedMessageManager.ts +++ b/src/TypedMessageManager.ts @@ -34,7 +34,8 @@ export interface TypedMessage { * * Represents the parameters to pass to the eth_signTypedData method once the signature request is approved. * - * @property data - A hex string conversion of the raw buffer data of the signature request + * @property data - A hex string conversion of the raw buffer or an object containing data of the signature + * request depending on version * @property from - Address to sign this message from * @property origin? - Added for request origin identification */ @@ -47,11 +48,12 @@ export interface TypedMessageParams { /** * @type TypedMessageParamsMetamask * - * Represents the parameters to pass to the personal_sign method once the signature request is approved + * Represents the parameters to pass to the eth_signTypedData method once the signature request is approved * plus data added by MetaMask. * * @property metamaskId - Added for tracking and identification within MetaMask - * @property data - A hex string conversion of the raw buffer or array of objects of data of the signature request + * @property data - A hex string conversion of the raw buffer or an object containing data of the signature + * request depending on version * @property from - Address to sign this message from * @property origin? - Added for request origin identification * @property version - Compatibility version EIP712 @@ -70,8 +72,8 @@ export interface TypedMessageParamsMetamask { * * Typed Message Manager state * - * @property unapprovedMessages - A collection of all Messages in the 'unapproved' state - * @property unapprovedMessagesCount - The count of all Messages in this.unapprovedMessages + * @property unapprovedMessages - A collection of all TypedMessages in the 'unapproved' state + * @property unapprovedMessagesCount - The count of all TypedMessages in this.unapprovedMessages */ export interface TypedMessageManagerState extends BaseState { unapprovedMessages: { [key: string]: TypedMessage }; @@ -277,7 +279,7 @@ export class TypedMessageManager extends BaseController Date: Tue, 4 Dec 2018 12:43:49 -0300 Subject: [PATCH 12/18] reuse const typedMessage on typedMessageManager tests --- src/TypedMessageManager.test.ts | 156 +++++--------------------------- 1 file changed, 23 insertions(+), 133 deletions(-) diff --git a/src/TypedMessageManager.test.ts b/src/TypedMessageManager.test.ts index 6776e813844..b853549c154 100644 --- a/src/TypedMessageManager.test.ts +++ b/src/TypedMessageManager.test.ts @@ -1,5 +1,16 @@ import TypedMessageManager from './TypedMessageManager'; - +const typedMessage = [ + { + name: 'Message', + type: 'string', + value: 'Hi, Alice!' + }, + { + name: 'A number', + type: 'uint32', + value: '1337' + } +]; describe('TypedMessageManager', () => { it('should set default state', () => { const controller = new TypedMessageManager(); @@ -18,18 +29,7 @@ describe('TypedMessageManager', () => { const messageTime = Date.now(); const messageStatus = 'unapproved'; const messageType = 'eth_signTypedData'; - const messageData = [ - { - name: 'Message', - type: 'string', - value: 'Hi, Alice!' - }, - { - name: 'A number', - type: 'uint32', - value: '1337' - } - ]; + const messageData = typedMessage; controller.addMessage({ id: messageId, messageParams: { @@ -57,18 +57,7 @@ describe('TypedMessageManager', () => { const controller = new TypedMessageManager(); const from = '0xc38bf1ad06ef69f0c04e29dbeb4152b4175f0a8d'; const version = 'V1'; - const messageData = [ - { - name: 'Message', - type: 'string', - value: 'Hi, Alice!' - }, - { - name: 'A number', - type: 'uint32', - value: '1337' - } - ]; + const messageData = typedMessage; const result = controller.addUnapprovedMessageAsync( { data: messageData, @@ -96,18 +85,7 @@ describe('TypedMessageManager', () => { const from = '0xc38bf1ad06ef69f0c04e29dbeb4152b4175f0a8d'; const version = 'V1'; const rawSig = '0x5f7a0'; - const messageData = [ - { - name: 'Message', - type: 'string', - value: 'Hi, Alice!' - }, - { - name: 'A number', - type: 'uint32', - value: '1337' - } - ]; + const messageData = typedMessage; const result = controller.addUnapprovedMessageAsync( { data: messageData, @@ -134,18 +112,7 @@ describe('TypedMessageManager', () => { const controller = new TypedMessageManager(); const from = '0xc38bf1ad06ef69f0c04e29dbeb4152b4175f0a8d'; const version = 'V1'; - const messageData = [ - { - name: 'Message', - type: 'string', - value: 'Hi, Alice!' - }, - { - name: 'A number', - type: 'uint32', - value: '1337' - } - ]; + const messageData = typedMessage; const result = controller.addUnapprovedMessageAsync( { data: messageData, @@ -172,18 +139,7 @@ describe('TypedMessageManager', () => { const controller = new TypedMessageManager(); const from = '0xc38bf1ad06ef69f0c04e29dbeb4152b4175f0a8d'; const version = 'V1'; - const messageData = [ - { - name: 'Message', - type: 'string', - value: 'Hi, Alice!' - }, - { - name: 'A number', - type: 'uint32', - value: '1337' - } - ]; + const messageData = typedMessage; const result = controller.addUnapprovedMessageAsync( { data: messageData, @@ -206,18 +162,7 @@ describe('TypedMessageManager', () => { const messageStatus = 'unapproved'; const messageType = 'eth_signTypedData'; const version = 'version'; - const messageData = [ - { - name: 'Message', - type: 'string', - value: 'Hi, Alice!' - }, - { - name: 'A number', - type: 'uint32', - value: '1337' - } - ]; + const messageData = typedMessage; const messageParams = { data: messageData, from: '0xfoO' @@ -258,18 +203,7 @@ describe('TypedMessageManager', () => { it('should throw when adding invalid typed message', () => { const from = '0xc38bf1ad06ef69f0c04e29dbeb4152b4175f0a8d'; - const messageData = [ - { - name: 'Message', - type: 'string', - value: 'Hi, Alice!' - }, - { - name: 'A number', - type: 'uint32', - value: '1337' - } - ]; + const messageData = typedMessage; const version = 'V3'; return new Promise(async (resolve) => { const controller = new TypedMessageManager(); @@ -339,18 +273,7 @@ describe('TypedMessageManager', () => { it('should approve typed message', async () => { const controller = new TypedMessageManager(); - const messageData = [ - { - name: 'Message', - type: 'string', - value: 'Hi, Alice!' - }, - { - name: 'A number', - type: 'uint32', - value: '1337' - } - ]; + const messageData = typedMessage; const firstMessage = { from: '0xfoO', data: messageData }; const version = 'V1'; const messageId = controller.addUnapprovedMessage(firstMessage, version); @@ -365,18 +288,7 @@ describe('TypedMessageManager', () => { it('should set message status signed', () => { const controller = new TypedMessageManager(); - const messageData = [ - { - name: 'Message', - type: 'string', - value: 'Hi, Alice!' - }, - { - name: 'A number', - type: 'uint32', - value: '1337' - } - ]; + const messageData = typedMessage; const firstMessage = { from: '0xfoO', data: messageData }; const version = 'V1'; const rawSig = '0x5f7a0'; @@ -392,18 +304,7 @@ describe('TypedMessageManager', () => { it('should reject message', () => { const controller = new TypedMessageManager(); - const messageData = [ - { - name: 'Message', - type: 'string', - value: 'Hi, Alice!' - }, - { - name: 'A number', - type: 'uint32', - value: '1337' - } - ]; + const messageData = typedMessage; const firstMessage = { from: '0xfoO', data: messageData }; const version = 'V1'; const messageId = controller.addUnapprovedMessage(firstMessage, version); @@ -417,18 +318,7 @@ describe('TypedMessageManager', () => { it('should set message status errored', () => { const controller = new TypedMessageManager(); - const messageData = [ - { - name: 'Message', - type: 'string', - value: 'Hi, Alice!' - }, - { - name: 'A number', - type: 'uint32', - value: '1337' - } - ]; + const messageData = typedMessage; const firstMessage = { from: '0xfoO', data: messageData }; const version = 'V1'; const messageId = controller.addUnapprovedMessage(firstMessage, version); From a3731beb27e52b67ec995cb88331186d5a1044a5 Mon Sep 17 00:00:00 2001 From: Esteban MIno Date: Wed, 5 Dec 2018 20:02:50 -0300 Subject: [PATCH 13/18] refactor PersonalMessageManager using base MessageManager --- src/MessageManager.ts | 271 ++++++++++++++++++++++++++++++++++ src/PersonalMessageManager.ts | 260 +------------------------------- src/TypedMessageManager.ts | 6 +- 3 files changed, 279 insertions(+), 258 deletions(-) create mode 100644 src/MessageManager.ts diff --git a/src/MessageManager.ts b/src/MessageManager.ts new file mode 100644 index 00000000000..a001b7953fd --- /dev/null +++ b/src/MessageManager.ts @@ -0,0 +1,271 @@ +import { EventEmitter } from 'events'; +import BaseController, { BaseConfig, BaseState } from './BaseController'; + +/** + * @type OriginalRequest + * + * Represents the original request object for adding a message. + * + * @property origin? - Is it is specified, represents the origin + */ +export interface OriginalRequest { + origin?: string; +} + +/** + * @type Message + * + * Represents and contains data about a 'personal_sign' type signature request. + * These are created when a signature for an personal_sign call is requested. + * + * @property id - An id to track and identify the message object + * @property messageParams - The parameters to pass to the personal_sign method once the signature request is approved + * @property type - The json-prc signing method for which a signature request has been made. + * A 'Message' which always has a 'personal_sign' type + * @property rawSig - Raw data of the signature request + */ +export interface Message { + id: string; + messageParams: MessageParams; + time: number; + status: string; + type: string; + rawSig?: string; +} + +/** + * @type MessageParams + * + * Represents the parameters to pass to the signing method once the signature request is approved. + * + * @property from - Address to sign this message from + * @property origin? - Added for request origin identification + */ +export interface MessageParams { + from: string; + origin?: string; +} + +/** + * @type MessageParamsMetamask + * + * Represents the parameters to pass to the personal_sign method once the signature request is approved + * plus data added by MetaMask. + * + * @property metamaskId - Added for tracking and identification within MetaMask + * @property data - A hex string conversion of the raw buffer data of the signature request + * @property from - Address to sign this message from + * @property origin? - Added for request origin identification + */ +export interface MessageParamsMetamask extends MessageParams { + metamaskId: string; +} + +/** + * @type MessageManagerState + * + * Message Manager state + * + * @property unapprovedMessages - A collection of all Messages in the 'unapproved' state + * @property unapprovedMessagesCount - The count of all Messages in this.unapprovedMessages + */ +export interface MessageManagerState extends BaseState { + unapprovedMessages: { [key: string]: M }; + unapprovedMessagesCount: number; +} + +/** + * Controller in charge of managing - storing, adding, removing, updating - Messages. + */ +export class MessageManager< + M extends Message, + P extends MessageParams, + PM extends MessageParamsMetamask +> extends BaseController> { + protected messages: M[]; + + /** + * Saves the unapproved messages, and their count to state + * + */ + protected saveMessageList() { + const unapprovedMessages = this.getUnapprovedMessages(); + const unapprovedMessagesCount = this.getUnapprovedMessagesCount(); + this.update({ unapprovedMessages, unapprovedMessagesCount }); + this.hub.emit('updateBadge'); + } + + /** + * Updates the status of a Message in this.messages + * + * @param messageId - The id of the Message to update + * @param status - The new status of the Message + */ + protected setMessageStatus(messageId: string, status: string) { + const message = this.getMessage(messageId); + /* istanbul ignore if */ + if (!message) { + throw new Error(`${this.context[name]}- Message not found for id: ${messageId}.`); + } + message.status = status; + this.updateMessage(message); + this.hub.emit(`${messageId}:${status}`, message); + if (status === 'rejected' || status === 'signed' || status === 'errored') { + this.hub.emit(`${messageId}:finished`, message); + } + } + + /** + * Sets a Message in this.messages to the passed Message if the ids are equal. + * Then saves the unapprovedMessage list to storage + * + * @param message - A Message that will replace an existing Message (with the id) in this.messages + */ + protected updateMessage(message: M) { + const index = this.messages.findIndex((msg) => message.id === msg.id); + /* istanbul ignore next */ + if (index !== -1) { + this.messages[index] = message; + } + this.saveMessageList(); + } + + /** + * EventEmitter instance used to listen to specific message events + */ + hub = new EventEmitter(); + + /** + * Name of this controller used during composition + */ + name = 'MessageManager'; + + /** + * Creates a MessageManager instance + * + * @param config - Initial options used to configure this controller + * @param state - Initial state to set on this controller + */ + constructor(config?: Partial, state?: Partial>) { + super(config, state); + this.defaultState = { + unapprovedMessages: {}, + unapprovedMessagesCount: 0 + }; + this.messages = []; + this.initialize(); + } + + /** + * A getter for the number of 'unapproved' Messages in this.messages + * + * @returns - The number of 'unapproved' Messages in this.messages + * + */ + getUnapprovedMessagesCount() { + return Object.keys(this.getUnapprovedMessages()).length; + } + + /** + * A getter for the 'unapproved' Messages in state messages + * + * @returns - An index of Message ids to Messages, for all 'unapproved' Messages in this.messages + * + */ + getUnapprovedMessages() { + return this.messages + .filter((message) => message.status === 'unapproved') + .reduce((result: { [key: string]: M }, message: M) => { + result[message.id] = message; + return result; + }, {}) as { [key: string]: M }; + } + + /** + * Adds a passed Message to this.messages, and calls this.saveMessageList() to save + * the unapproved Messages from that list to this.messages. + * + * @param {Message} message The Message to add to this.messages + * + */ + addMessage(message: M) { + this.messages.push(message); + this.saveMessageList(); + } + + /** + * Returns a specified Message. + * + * @param messageId - The id of the Message to get + * @returns - The Message with the id that matches the passed messageId, or undefined + * if no Message has that id. + * + */ + getMessage(messageId: string) { + return this.messages.find((message) => message.id === messageId); + } + + /** + * Approves a Message. Sets the message status via a call to this.setMessageStatusApproved, + * and returns a promise with any the message params modified for proper signing. + * + * @param messageParams - The messageParams to be used when personal_sign is called, + * plus data added by MetaMask + * @returns - Promise resolving to the messageParams with the metamaskId property removed + */ + approveMessage(messageParams: PM): Promise

{ + this.setMessageStatusApproved(messageParams.metamaskId); + return this.prepMessageForSigning(messageParams); + } + + /** + * Sets a Message status to 'approved' via a call to this.setMessageStatus. + * + * @param messageId - The id of the Message to approve + */ + setMessageStatusApproved(messageId: string) { + this.setMessageStatus(messageId, 'approved'); + } + + /** + * Sets a Message status to 'signed' via a call to this.setMessageStatus and updates + * that Message in this.messages by adding the raw signature data of the signature + * request to the Message. + * + * @param messageId - The id of the Message to sign + * @param rawSig - The raw data of the signature request + */ + setMessageStatusSigned(messageId: string, rawSig: string) { + const message = this.getMessage(messageId); + /* istanbul ignore if */ + if (!message) { + return; + } + message.rawSig = rawSig; + this.updateMessage(message); + this.setMessageStatus(messageId, 'signed'); + } + + /** + * Removes the metamaskId property from passed messageParams and returns a promise which + * resolves the updated messageParams + * + * @param messageParams - The messageParams to modify + * @returns - Promise resolving to the messageParams with the metamaskId property removed + */ + prepMessageForSigning(messageParams: PM): Promise

{ + delete messageParams.metamaskId; + return Promise.resolve(messageParams); + } + + /** + * Sets a Message status to 'rejected' via a call to this.setMessageStatus. + * + * @param messageId - The id of the Message to reject. + */ + rejectMessage(messageId: string) { + this.setMessageStatus(messageId, 'rejected'); + } +} + +export default MessageManager; diff --git a/src/PersonalMessageManager.ts b/src/PersonalMessageManager.ts index 3c059687e43..8eb347a8ae4 100644 --- a/src/PersonalMessageManager.ts +++ b/src/PersonalMessageManager.ts @@ -1,31 +1,9 @@ -import { EventEmitter } from 'events'; -import BaseController, { BaseConfig, BaseState } from './BaseController'; import { validatePersonalSignMessageData, normalizeMessageData } from './util'; +import MessageManager, { Message, MessageParams, MessageParamsMetamask, OriginalRequest } from './MessageManager'; const random = require('uuid/v1'); /** - * @type Message - * - * Represents and contains data about a 'personal_sign' type signature request. - * These are created when a signature for an personal_sign call is requested. - * - * @property id - An id to track and identify the message object - * @property messageParams - The parameters to pass to the personal_sign method once the signature request is approved - * @property type - The json-prc signing method for which a signature request has been made. - * A 'Message' which always has a 'personal_sign' type - * @property rawSig - Raw data of the signature request - */ -export interface Message { - id: string; - messageParams: MessageParams; - time: number; - status: string; - type: string; - rawSig?: string; -} - -/** - * @type MessageParams + * @type PersonalMessageParams * * Represents the parameters to pass to the personal_sign method once the signature request is approved. * @@ -33,157 +11,19 @@ export interface Message { * @property from - Address to sign this message from * @property origin? - Added for request origin identification */ -export interface MessageParams { +export interface PersonalMessageParams extends MessageParams { data: string; - from: string; - origin?: string; -} - -/** - * @type MessageParamsMetamask - * - * Represents the parameters to pass to the personal_sign method once the signature request is approved - * plus data added by MetaMask. - * - * @property metamaskId - Added for tracking and identification within MetaMask - * @property data - A hex string conversion of the raw buffer data of the signature request - * @property from - Address to sign this message from - * @property origin? - Added for request origin identification - */ -export interface MessageParamsMetamask { - metamaskId: string; - data: string; - from: string; - origin?: string; -} - -/** - * @type OriginalRequest - * - * Represents the original request object for adding a message. - * - * @property origin? - Is it is specified, represents the origin - */ -export interface OriginalRequest { - origin?: string; -} - -/** - * @type PersonalMessageManagerState - * - * Message Manager state - * - * @property unapprovedMessages - A collection of all Messages in the 'unapproved' state - * @property unapprovedMessagesCount - The count of all Messages in this.memStore.unapprobedMessages - */ -export interface PersonalMessageManagerState extends BaseState { - unapprovedMessages: { [key: string]: Message }; - unapprovedMessagesCount: number; } /** * Controller in charge of managing - storing, adding, removing, updating - Messages. */ -export class PersonalMessageManager extends BaseController { - private messages: Message[]; - - /** - * Saves the unapproved messages, and their count to state - * - */ - private saveMessageList() { - const unapprovedMessages = this.getUnapprovedMessages(); - const unapprovedMessagesCount = this.getUnapprovedMessagesCount(); - this.update({ unapprovedMessages, unapprovedMessagesCount }); - this.hub.emit('updateBadge'); - } - - /** - * Updates the status of a Message in this.messages - * - * @param messageId - The id of the Messsage to update - * @param status - The new status of the Message - */ - private setMessageStatus(messageId: string, status: string) { - const message = this.getMessage(messageId); - /* istanbul ignore if */ - if (!message) { - throw new Error(`PersonalMessageManager - Message not found for id: ${messageId}.`); - } - message.status = status; - this.updateMessage(message); - this.hub.emit(`${messageId}:${status}`, message); - if (status === 'rejected' || status === 'signed') { - this.hub.emit(`${messageId}:finished`, message); - } - } - - /** - * Sets a Message in this.messages to the passed Message if the ids are equal. - * Then saves the unapprovedMessage list to storage - * - * @param message - A Message that will replace an existing Message (with the id) in this.messages - */ - private updateMessage(message: Message) { - const index = this.messages.findIndex((msg) => message.id === msg.id); - /* istanbul ignore next */ - if (index !== -1) { - this.messages[index] = message; - } - this.saveMessageList(); - } - - /** - * EventEmitter instance used to listen to specific message events - */ - hub = new EventEmitter(); - +export class PersonalMessageManager extends MessageManager { /** * Name of this controller used during composition */ name = 'PersonalMessageManager'; - /** - * Creates a PersonalMessageManager instance - * - * @param config - Initial options used to configure this controller - * @param state - Initial state to set on this controller - */ - constructor(config?: Partial, state?: Partial) { - super(config, state); - this.defaultState = { - unapprovedMessages: {}, - unapprovedMessagesCount: 0 - }; - this.messages = []; - this.initialize(); - } - - /** - * A getter for the number of 'unapproved' Messages in this.messages - * - * @returns - The number of 'unapproved' Messages in this.messages - * - */ - getUnapprovedMessagesCount() { - return Object.keys(this.getUnapprovedMessages()).length; - } - - /** - * A getter for the 'unapproved' Messages in state messages - * - * @returns - An index of Message ids to Messages, for all 'unapproved' Messages in this.messages - * - */ - getUnapprovedMessages() { - return this.messages - .filter((message) => message.status === 'unapproved') - .reduce((result: { [key: string]: Message }, message: Message) => { - result[message.id] = message; - return result; - }, {}) as { [key: string]: Message }; - } - /** * Creates a new Message with an 'unapproved' status using the passed messageParams. * this.addMessage is called to add the new Message to this.messages, and to save the unapproved Messages. @@ -192,7 +32,7 @@ export class PersonalMessageManager extends BaseController { + addUnapprovedMessageAsync(messageParams: PersonalMessageParams, req?: OriginalRequest): Promise { return new Promise((resolve, reject) => { validatePersonalSignMessageData(messageParams); const messageId = this.addUnapprovedMessage(messageParams, req); @@ -223,13 +63,11 @@ export class PersonalMessageManager extends BaseController message.id === messageId); - } - - /** - * Approves a Message. Sets the message status via a call to this.setMessageStatusApproved, - * and returns a promise with any the message params modified for proper signing. - * - * @param messageParams - The messageParams to be used when personal_sign is called, - * plus data added by MetaMask - * @returns - Promise resolving to the messageParams with the metamaskId property removed - */ - approveMessage(messageParams: MessageParamsMetamask): Promise { - this.setMessageStatusApproved(messageParams.metamaskId); - return this.prepMessageForSigning(messageParams); - } - - /** - * Sets a Message status to 'approved' via a call to this.setMessageStatus. - * - * @param messageId - The id of the Message to approve - */ - setMessageStatusApproved(messageId: string) { - this.setMessageStatus(messageId, 'approved'); - } - - /** - * Sets a Message status to 'signed' via a call to this.setMessageStatus and updates - * that Message in this.messages by adding the raw signature data of the signature - * request to the Message. - * - * @param messageId - The id of the Message to sign - * @param rawSig - The raw data of the signature request - */ - setMessageStatusSigned(messageId: string, rawSig: string) { - const message = this.getMessage(messageId); - /* istanbul ignore if */ - if (!message) { - return; - } - message.rawSig = rawSig; - this.updateMessage(message); - this.setMessageStatus(messageId, 'signed'); - } - - /** - * Removes the metamaskId property from passed messageParams and returns a promise which - * resolves the updated messageParams - * - * @param messageParams - The messageParams to modify - * @returns - Promise resolving to the messageParams with the metamaskId property removed - */ - prepMessageForSigning(messageParams: MessageParamsMetamask): Promise { - delete messageParams.metamaskId; - return Promise.resolve(messageParams); - } - - /** - * Sets a Message status to 'rejected' via a call to this.setMessageStatus. - * - * @param messageId - The id of the Message to reject. - */ - rejectMessage(messageId: string) { - this.setMessageStatus(messageId, 'rejected'); - } } export default PersonalMessageManager; diff --git a/src/TypedMessageManager.ts b/src/TypedMessageManager.ts index 206ef6bce0d..a4641abfa0d 100644 --- a/src/TypedMessageManager.ts +++ b/src/TypedMessageManager.ts @@ -54,16 +54,14 @@ export interface TypedMessageParams { * @property metamaskId - Added for tracking and identification within MetaMask * @property data - A hex string conversion of the raw buffer or an object containing data of the signature * request depending on version + * @property error? - Added for message errored * @property from - Address to sign this message from * @property origin? - Added for request origin identification * @property version - Compatibility version EIP712 */ -export interface TypedMessageParamsMetamask { +export interface TypedMessageParamsMetamask extends TypedMessage { metamaskId: string; - data: object[] | string; error?: string; - from: string; - origin?: string; version: string; } From 16480fa0f0b775a4cc0032fee62ad71b7737c851 Mon Sep 17 00:00:00 2001 From: Esteban MIno Date: Wed, 5 Dec 2018 20:26:19 -0300 Subject: [PATCH 14/18] refactor TypedMessageManager using base MessageManager --- src/MessageManager.ts | 10 +- src/PersonalMessageManager.ts | 51 +++++++++- src/TypedMessageManager.ts | 186 ++-------------------------------- 3 files changed, 57 insertions(+), 190 deletions(-) diff --git a/src/MessageManager.ts b/src/MessageManager.ts index a001b7953fd..032a7a1bcce 100644 --- a/src/MessageManager.ts +++ b/src/MessageManager.ts @@ -19,14 +19,12 @@ export interface OriginalRequest { * These are created when a signature for an personal_sign call is requested. * * @property id - An id to track and identify the message object - * @property messageParams - The parameters to pass to the personal_sign method once the signature request is approved * @property type - The json-prc signing method for which a signature request has been made. * A 'Message' which always has a 'personal_sign' type * @property rawSig - Raw data of the signature request */ export interface Message { id: string; - messageParams: MessageParams; time: number; status: string; type: string; @@ -53,7 +51,6 @@ export interface MessageParams { * plus data added by MetaMask. * * @property metamaskId - Added for tracking and identification within MetaMask - * @property data - A hex string conversion of the raw buffer data of the signature request * @property from - Address to sign this message from * @property origin? - Added for request origin identification */ @@ -77,7 +74,7 @@ export interface MessageManagerState extends BaseState { /** * Controller in charge of managing - storing, adding, removing, updating - Messages. */ -export class MessageManager< +export abstract class MessageManager< M extends Message, P extends MessageParams, PM extends MessageParamsMetamask @@ -253,10 +250,7 @@ export class MessageManager< * @param messageParams - The messageParams to modify * @returns - Promise resolving to the messageParams with the metamaskId property removed */ - prepMessageForSigning(messageParams: PM): Promise

{ - delete messageParams.metamaskId; - return Promise.resolve(messageParams); - } + abstract prepMessageForSigning(messageParams: PM): Promise

; /** * Sets a Message status to 'rejected' via a call to this.setMessageStatus. diff --git a/src/PersonalMessageManager.ts b/src/PersonalMessageManager.ts index 8eb347a8ae4..e9970464b5c 100644 --- a/src/PersonalMessageManager.ts +++ b/src/PersonalMessageManager.ts @@ -2,6 +2,22 @@ import { validatePersonalSignMessageData, normalizeMessageData } from './util'; import MessageManager, { Message, MessageParams, MessageParamsMetamask, OriginalRequest } from './MessageManager'; const random = require('uuid/v1'); +/** + * @type Message + * + * Represents and contains data about a 'personal_sign' type signature request. + * These are created when a signature for an personal_sign call is requested. + * + * @property id - An id to track and identify the message object + * @property messageParams - The parameters to pass to the personal_sign method once the signature request is approved + * @property type - The json-prc signing method for which a signature request has been made. + * A 'Message' which always has a 'personal_sign' type + * @property rawSig - Raw data of the signature request + */ +export interface PersonalMessage extends Message { + messageParams: PersonalMessageParams; +} + /** * @type PersonalMessageParams * @@ -15,10 +31,29 @@ export interface PersonalMessageParams extends MessageParams { data: string; } +/** + * @type MessageParamsMetamask + * + * Represents the parameters to pass to the personal_sign method once the signature request is approved + * plus data added by MetaMask. + * + * @property metamaskId - Added for tracking and identification within MetaMask + * @property data - A hex string conversion of the raw buffer data of the signature request + * @property from - Address to sign this message from + * @property origin? - Added for request origin identification + */ +export interface PersonalMessageParamsMetamask extends MessageParamsMetamask { + data: string; +} + /** * Controller in charge of managing - storing, adding, removing, updating - Messages. */ -export class PersonalMessageManager extends MessageManager { +export class PersonalMessageManager extends MessageManager< + PersonalMessage, + PersonalMessageParams, + PersonalMessageParamsMetamask +> { /** * Name of this controller used during composition */ @@ -69,7 +104,7 @@ export class PersonalMessageManager extends MessageManager { + delete messageParams.metamaskId; + return Promise.resolve(messageParams); + } } export default PersonalMessageManager; diff --git a/src/TypedMessageManager.ts b/src/TypedMessageManager.ts index a4641abfa0d..d8e4e739bca 100644 --- a/src/TypedMessageManager.ts +++ b/src/TypedMessageManager.ts @@ -2,7 +2,7 @@ import { EventEmitter } from 'events'; import BaseController, { BaseConfig, BaseState } from './BaseController'; import { validateTypedSignMessageDataV3, validateTypedSignMessageDataV1 } from './util'; import NetworkController from './NetworkController'; -import { OriginalRequest } from './PersonalMessageManager'; +import MessageManager, { Message, MessageParams, MessageParamsMetamask, OriginalRequest } from './MessageManager'; const random = require('uuid/v1'); /** @@ -19,8 +19,7 @@ const random = require('uuid/v1'); * A 'TypedMessage' which always has a 'eth_signTypedData' type * @property rawSig - Raw data of the signature request */ -export interface TypedMessage { - id: string; +export interface TypedMessage extends Message { error?: string; messageParams: TypedMessageParams; time: number; @@ -39,10 +38,8 @@ export interface TypedMessage { * @property from - Address to sign this message from * @property origin? - Added for request origin identification */ -export interface TypedMessageParams { +export interface TypedMessageParams extends MessageParams { data: object[] | string; - from: string; - origin?: string; } /** @@ -59,82 +56,17 @@ export interface TypedMessageParams { * @property origin? - Added for request origin identification * @property version - Compatibility version EIP712 */ -export interface TypedMessageParamsMetamask extends TypedMessage { +export interface TypedMessageParamsMetamask extends MessageParamsMetamask { + data: object[] | string; metamaskId: string; error?: string; version: string; } -/** - * @type TypedMessageManagerState - * - * Typed Message Manager state - * - * @property unapprovedMessages - A collection of all TypedMessages in the 'unapproved' state - * @property unapprovedMessagesCount - The count of all TypedMessages in this.unapprovedMessages - */ -export interface TypedMessageManagerState extends BaseState { - unapprovedMessages: { [key: string]: TypedMessage }; - unapprovedMessagesCount: number; -} - /** * Controller in charge of managing - storing, adding, removing, updating - TypedMessages. */ -export class TypedMessageManager extends BaseController { - private messages: TypedMessage[]; - - /** - * Saves the unapproved TypedMessages, and their count to state - * - */ - private saveMessageList() { - const unapprovedMessages = this.getUnapprovedMessages(); - const unapprovedMessagesCount = this.getUnapprovedMessagesCount(); - this.update({ unapprovedMessages, unapprovedMessagesCount }); - this.hub.emit('updateBadge'); - } - - /** - * Updates the status of a TypedMessage in this.messages - * - * @param messageId - The id of the TypedMessage to update - * @param status - The new status of the TypedMessage - */ - private setMessageStatus(messageId: string, status: string) { - const message = this.getMessage(messageId); - /* istanbul ignore if */ - if (!message) { - throw new Error(`TypedMessageManager - Message not found for id: ${messageId}.`); - } - message.status = status; - this.updateMessage(message); - this.hub.emit(`${messageId}:${status}`, message); - if (status === 'rejected' || status === 'signed' || status === 'errored') { - this.hub.emit(`${messageId}:finished`, message); - } - } - - /** - * Sets a TypedMessage in this.messages to the passed TypedMessage if the ids are equal. - * Then saves the unapprovedMessage list to storage - * - * @param message - A TypedMessage that will replace an existing TypedMessage (with the id) in this.messages - */ - private updateMessage(message: TypedMessage) { - const index = this.messages.findIndex((msg) => message.id === msg.id); - /* istanbul ignore next */ - if (index !== -1) { - this.messages[index] = message; - } - this.saveMessageList(); - } - - /** - * EventEmitter instance used to listen to specific message events - */ - hub = new EventEmitter(); - +export class TypedMessageManager extends MessageManager { /** * Name of this controller used during composition */ @@ -145,47 +77,6 @@ export class TypedMessageManager extends BaseController, state?: Partial) { - super(config, state); - this.defaultState = { - unapprovedMessages: {}, - unapprovedMessagesCount: 0 - }; - this.messages = []; - this.initialize(); - } - - /** - * A getter for the number of 'unapproved' TypedMessages in this.messages - * - * @returns - The number of 'unapproved' TypedMessages in this.messages - * - */ - getUnapprovedMessagesCount() { - return Object.keys(this.getUnapprovedMessages()).length; - } - - /** - * A getter for the 'unapproved' TypedMessages in state messages - * - * @returns - An index of TypedMessage ids to TypedMessages, for all 'unapproved' TypedMessages in this.messages - * - */ - getUnapprovedMessages() { - return this.messages - .filter((message) => message.status === 'unapproved') - .reduce((result: { [key: string]: TypedMessage }, message: TypedMessage) => { - result[message.id] = message; - return result; - }, {}) as { [key: string]: TypedMessage }; - } - /** * Creates a new TypedMessage with an 'unapproved' status using the passed messageParams. * this.addMessage is called to add the new TypedMessage to this.messages, and to save the unapproved TypedMessages. @@ -260,52 +151,6 @@ export class TypedMessageManager extends BaseController message.id === messageId); - } - - /** - * Approves a TypedMessage. Sets the message status via a call to this.setMessageStatusApproved, - * and returns a promise with any the message params modified for proper signing. - * - * @param messageParams - The messageParams to be used when 'eth_signTypedData' is called, - * plus data added by MetaMask - * @returns - Promise resolving to the messageParams with the metamaskId property removed - */ - approveMessage(messageParams: TypedMessageParamsMetamask): Promise { - this.setMessageStatusApproved(messageParams.metamaskId); - return this.prepMessageForSigning(messageParams); - } - - /** - * Sets a TypedMessage status to 'approved' via a call to this.setMessageStatus. - * - * @param messageId - The id of the TypedMessage to approve - */ - setMessageStatusApproved(messageId: string) { - this.setMessageStatus(messageId, 'approved'); - } - /** * Sets a TypedMessage status to 'errored' via a call to this.setMessageStatus. * @@ -323,25 +168,6 @@ export class TypedMessageManager extends BaseController Date: Wed, 5 Dec 2018 20:48:58 -0300 Subject: [PATCH 15/18] add MessageManager tests --- src/MessageManager.test.ts | 174 +++++++++++++++++++++++++++++++++++++ src/TypedMessageManager.ts | 11 --- 2 files changed, 174 insertions(+), 11 deletions(-) create mode 100644 src/MessageManager.test.ts diff --git a/src/MessageManager.test.ts b/src/MessageManager.test.ts new file mode 100644 index 00000000000..03b95fa5bd7 --- /dev/null +++ b/src/MessageManager.test.ts @@ -0,0 +1,174 @@ +import MessageManager from './MessageManager'; +import { TypedMessage, TypedMessageParams, TypedMessageParamsMetamask } from './TypedMessageManager'; + +class TestManager extends MessageManager { + prepMessageForSigning(messageParams: TypedMessageParamsMetamask): Promise { + delete messageParams.metamaskId; + delete messageParams.version; + return Promise.resolve(messageParams); + } +} +const typedMessage = [ + { + name: 'Message', + type: 'string', + value: 'Hi, Alice!' + }, + { + name: 'A number', + type: 'uint32', + value: '1337' + } +]; +const messageId = '1'; +const from = '0x0123'; +const messageTime = Date.now(); +const messageStatus = 'unapproved'; +const messageType = 'eth_signTypedData'; +const messageData = typedMessage; + +describe('TestManager', () => { + it('should set default state', () => { + const controller = new TestManager(); + expect(controller.state).toEqual({ unapprovedMessages: {}, unapprovedMessagesCount: 0 }); + }); + + it('should set default config', () => { + const controller = new TestManager(); + expect(controller.config).toEqual({}); + }); + + it('should add a valid message', async () => { + const controller = new TestManager(); + controller.addMessage({ + id: messageId, + messageParams: { + data: typedMessage, + from + }, + status: messageStatus, + time: messageTime, + type: messageType + }); + const message = controller.getMessage(messageId); + expect(message).not.toBe(undefined); + if (message) { + expect(message.id).toBe(messageId); + expect(message.messageParams.from).toBe(from); + expect(message.messageParams.data).toBe(messageData); + expect(message.time).toBe(messageTime); + expect(message.status).toBe(messageStatus); + expect(message.type).toBe(messageType); + } + }); + + it('should reject a message', () => { + const controller = new TestManager(); + controller.addMessage({ + id: messageId, + messageParams: { + data: typedMessage, + from + }, + status: messageStatus, + time: messageTime, + type: messageType + }); + controller.rejectMessage(messageId); + const message = controller.getMessage(messageId); + expect(message).not.toBe(undefined); + if (message) { + expect(message.status).toBe('rejected'); + } + }); + + it('should sign a message', () => { + const controller = new TestManager(); + controller.addMessage({ + id: messageId, + messageParams: { + data: typedMessage, + from + }, + status: messageStatus, + time: messageTime, + type: messageType + }); + controller.setMessageStatusSigned(messageId, 'rawSig'); + const message = controller.getMessage(messageId); + expect(message).not.toBe(undefined); + if (message) { + expect(message.status).toBe('signed'); + expect(message.rawSig).toBe('rawSig'); + } + }); + + it('should get correct unapproved messages', () => { + const firstMessageData = [ + { + name: 'Message', + type: 'string', + value: 'Hi, Alice!' + }, + { + name: 'A number', + type: 'uint32', + value: '1337' + } + ]; + const secondMessageData = [ + { + name: 'Message', + type: 'string', + value: 'Hi, Alice!' + }, + { + name: 'A number', + type: 'uint32', + value: '1337' + } + ]; + const firstMessage = { + id: '1', + messageParams: { from: '0x1', data: firstMessageData }, + status: 'unapproved', + time: 123, + type: 'eth_signTypedData' + }; + const secondMessage = { + id: '2', + messageParams: { from: '0x1', data: secondMessageData }, + status: 'unapproved', + time: 123, + type: 'eth_signTypedData' + }; + const controller = new TestManager(); + controller.addMessage(firstMessage); + controller.addMessage(secondMessage); + expect(controller.getUnapprovedMessagesCount()).toEqual(2); + expect(controller.getUnapprovedMessages()).toEqual({ + [firstMessage.id]: firstMessage, + [secondMessage.id]: secondMessage + }); + }); + + it('should approve typed message', async () => { + const controller = new TestManager(); + const firstMessage = { from: '0xfoO', data: typedMessage }; + const version = 'V1'; + controller.addMessage({ + id: messageId, + messageParams: firstMessage, + status: messageStatus, + time: messageTime, + type: messageType + }); + const messageParams = await controller.approveMessage({ ...firstMessage, metamaskId: messageId, version }); + const message = controller.getMessage(messageId); + expect(messageParams).toEqual(firstMessage); + expect(message).not.toBe(undefined); + if (message) { + expect(message.status).toEqual('approved'); + } + }); +}); diff --git a/src/TypedMessageManager.ts b/src/TypedMessageManager.ts index d8e4e739bca..d9b0a7449b3 100644 --- a/src/TypedMessageManager.ts +++ b/src/TypedMessageManager.ts @@ -1,5 +1,3 @@ -import { EventEmitter } from 'events'; -import BaseController, { BaseConfig, BaseState } from './BaseController'; import { validateTypedSignMessageDataV3, validateTypedSignMessageDataV1 } from './util'; import NetworkController from './NetworkController'; import MessageManager, { Message, MessageParams, MessageParamsMetamask, OriginalRequest } from './MessageManager'; @@ -180,15 +178,6 @@ export class TypedMessageManager extends MessageManager Date: Wed, 5 Dec 2018 20:51:30 -0300 Subject: [PATCH 16/18] fiz util import --- src/util.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/util.ts b/src/util.ts index fbc95756523..bdaa7259063 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,5 +1,5 @@ import { Transaction } from './TransactionController'; -import { MessageParams } from './PersonalMessageManager'; +import { PersonalMessageParams } from './PersonalMessageManager'; import { TypedMessageParams } from './TypedMessageManager'; const sigUtil = require('eth-sig-util'); const jsonschema = require('jsonschema'); @@ -174,12 +174,12 @@ export function normalizeMessageData(data: string) { } /** - * Validates a MessageParams object for required properties and throws in + * Validates a PersonalMessageParams object for required properties and throws in * the event of any validation error. * - * @param messageData - MessageParams object to validate + * @param messageData - PersonalMessageParams object to validate */ -export function validatePersonalSignMessageData(messageData: MessageParams) { +export function validatePersonalSignMessageData(messageData: PersonalMessageParams) { if (!messageData.from || typeof messageData.from !== 'string' || !isValidAddress(messageData.from)) { throw new Error(`Invalid "from" address: ${messageData.from} must be a valid string.`); } From 0b29f2156251d317deb39ffdc857f4ed82b2349f Mon Sep 17 00:00:00 2001 From: Esteban MIno Date: Wed, 5 Dec 2018 21:33:42 -0300 Subject: [PATCH 17/18] fix TypedMessageParams on keyring controller --- src/KeyringController.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/KeyringController.ts b/src/KeyringController.ts index 13ae06c8c60..1a7881b491e 100644 --- a/src/KeyringController.ts +++ b/src/KeyringController.ts @@ -2,7 +2,8 @@ import 'isomorphic-fetch'; import BaseController, { BaseConfig, BaseState, Listener } from './BaseController'; import PreferencesController from './PreferencesController'; import { Transaction } from './TransactionController'; -import { MessageParams } from './PersonalMessageManager'; +import { PersonalMessageParams } from './PersonalMessageManager'; +import { TypedMessageParams } from './TypedMessageManager'; const sigUtil = require('eth-sig-util'); const { toChecksumAddress } = require('ethereumjs-util'); const Keyring = require('eth-keyring-controller'); @@ -241,31 +242,31 @@ export class KeyringController extends BaseController /** * Signs message by calling down into a specific keyring * - * @param messageParams - MessageParams object to sign + * @param messageParams - PersonalMessageParams object to sign * @returns - Promise resolving to a signed message string */ - signMessage(messageParams: MessageParams) { + signMessage(messageParams: PersonalMessageParams) { return this.keyring.signMessage(messageParams); } /** * Signs personal message by calling down into a specific keyring * - * @param messageParams - MessageParams object to sign + * @param messageParams - PersonalMessageParams object to sign * @returns - Promise resolving to a signed message string */ - signPersonalMessage(messageParams: MessageParams) { + signPersonalMessage(messageParams: PersonalMessageParams) { return this.keyring.signPersonalMessage(messageParams); } /** * Signs typed message by calling down into a specific keyring * - * @param messageParams - MessageParams object to sign + * @param messageParams - TypedMessageParams object to sign * @param version - Compatibility version EIP712 * @returns - Promise resolving to a signed message string or an error if any */ - async signTypedMessage(messageParams: MessageParams, version: string) { + async signTypedMessage(messageParams: TypedMessageParams, version: string) { try { const address = sigUtil.normalize(messageParams.from); const privateKey = await this.exportAccount(address); @@ -274,7 +275,7 @@ export class KeyringController extends BaseController case 'V1': return sigUtil.signTypedDataLegacy(privateKeyBuffer, { data: messageParams.data }); case 'V3': - return sigUtil.signTypedData(privateKeyBuffer, { data: JSON.parse(messageParams.data) }); + return sigUtil.signTypedData(privateKeyBuffer, { data: JSON.parse(messageParams.data as string) }); } } catch (error) { throw new Error('Keyring Controller signTypedMessage: ' + error); From 9a96a1567d683170f6ebf3f0e6abdbf463978643 Mon Sep 17 00:00:00 2001 From: Esteban MIno Date: Thu, 6 Dec 2018 13:20:40 -0300 Subject: [PATCH 18/18] update docs --- src/MessageManager.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/MessageManager.ts b/src/MessageManager.ts index 032a7a1bcce..91def651fd0 100644 --- a/src/MessageManager.ts +++ b/src/MessageManager.ts @@ -15,12 +15,12 @@ export interface OriginalRequest { /** * @type Message * - * Represents and contains data about a 'personal_sign' type signature request. + * Represents and contains data about a signing type signature request. * These are created when a signature for an personal_sign call is requested. * * @property id - An id to track and identify the message object * @property type - The json-prc signing method for which a signature request has been made. - * A 'Message' which always has a 'personal_sign' type + * A 'Message' which always has a signing type * @property rawSig - Raw data of the signature request */ export interface Message { @@ -47,7 +47,7 @@ export interface MessageParams { /** * @type MessageParamsMetamask * - * Represents the parameters to pass to the personal_sign method once the signature request is approved + * Represents the parameters to pass to the signing method once the signature request is approved * plus data added by MetaMask. * * @property metamaskId - Added for tracking and identification within MetaMask @@ -206,7 +206,7 @@ export abstract class MessageManager< * Approves a Message. Sets the message status via a call to this.setMessageStatusApproved, * and returns a promise with any the message params modified for proper signing. * - * @param messageParams - The messageParams to be used when personal_sign is called, + * @param messageParams - The messageParams to be used when signing method is called, * plus data added by MetaMask * @returns - Promise resolving to the messageParams with the metamaskId property removed */