Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: message manager #160

Merged
merged 7 commits into from
Oct 17, 2019
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export * from './BaseController';
export * from './ComposableController';
export * from './assets/CurrencyRateController';
export * from './keyring/KeyringController';
export * from './message-manager/PersonalMessageManager';
export * from './message-manager/MessageManager';
export * from './network/NetworkController';
export * from './network/NetworkStatusController';
export * from './third-party/PhishingController';
Expand All @@ -19,5 +19,6 @@ export * from './third-party/ShapeShiftController';
export * from './assets/TokenBalancesController';
export * from './assets/TokenRatesController';
export * from './transaction/TransactionController';
export * from './message-manager/PersonalMessageManager';
export * from './message-manager/TypedMessageManager';
export { util };
265 changes: 265 additions & 0 deletions src/message-manager/AbstractMessageManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
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 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 signing type
* @property rawSig - Raw data of the signature request
*/
export interface AbstractMessage {
id: string;
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 AbstractMessageParams {
from: string;
origin?: string;
}

/**
* @type MessageParamsMetamask
*
* 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
* @property from - Address to sign this message from
* @property origin? - Added for request origin identification
*/
export interface AbstractMessageParamsMetamask extends AbstractMessageParams {
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<M extends AbstractMessage> extends BaseState {
unapprovedMessages: { [key: string]: M };
unapprovedMessagesCount: number;
}

/**
* Controller in charge of managing - storing, adding, removing, updating - Messages.
*/
export abstract class AbstractMessageManager<
M extends AbstractMessage,
P extends AbstractMessageParams,
PM extends AbstractMessageParamsMetamask
> extends BaseController<BaseConfig, MessageManagerState<M>> {
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 = 'AbstractMessageManager';

/**
* Creates an AbstractMessageManager instance
*
* @param config - Initial options used to configure this controller
* @param state - Initial state to set on this controller
*/
constructor(config?: Partial<BaseConfig>, state?: Partial<MessageManagerState<M>>) {
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 signing method is called,
* plus data added by MetaMask
* @returns - Promise resolving to the messageParams with the metamaskId property removed
*/
approveMessage(messageParams: PM): Promise<P> {
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
*/
abstract prepMessageForSigning(messageParams: PM): Promise<P>;

/**
* 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 AbstractMessageManager;
Loading