sparrow-controllers
Version:
Collection of platform-agnostic modules for creating secure data models for cryptocurrency wallets
381 lines • 15.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ApprovalController = void 0;
const eth_rpc_errors_1 = require("eth-rpc-errors");
const nanoid_1 = require("nanoid");
const BaseControllerV2_1 = require("../BaseControllerV2");
const controllerName = 'ApprovalController';
const stateMetadata = {
pendingApprovals: { persist: false, anonymous: true },
pendingApprovalCount: { persist: false, anonymous: false },
};
const getAlreadyPendingMessage = (origin, type) => `Request of type '${type}' already pending for origin ${origin}. Please wait.`;
const getDefaultState = () => {
return {
pendingApprovals: {},
pendingApprovalCount: 0,
};
};
/**
* Controller for managing requests that require user approval.
*
* Enables limiting the number of pending requests by origin and type, counting
* pending requests, and more.
*
* Adding a request returns a promise that resolves or rejects when the request
* is approved or denied, respectively.
*/
class ApprovalController extends BaseControllerV2_1.BaseController {
/**
* Construct an Approval controller.
*
* @param options - The controller options.
* @param options.showApprovalRequest - Function for opening the UI such that
* the request can be displayed to the user.
* @param options.messenger - The restricted controller messenger for the Approval controller.
* @param options.state - The initial controller state.
*/
constructor({ messenger, showApprovalRequest, state = {}, }) {
super({
name: controllerName,
metadata: stateMetadata,
messenger,
state: Object.assign(Object.assign({}, getDefaultState()), state),
});
this._approvals = new Map();
this._origins = new Map();
this._showApprovalRequest = showApprovalRequest;
this.registerMessageHandlers();
}
/**
* Constructor helper for registering this controller's messaging system
* actions.
*/
registerMessageHandlers() {
this.messagingSystem.registerActionHandler(`${controllerName}:clearRequests`, this.clear.bind(this));
this.messagingSystem.registerActionHandler(`${controllerName}:addRequest`, (opts, shouldShowRequest) => {
if (shouldShowRequest) {
return this.addAndShowApprovalRequest(opts);
}
return this.add(opts);
});
this.messagingSystem.registerActionHandler(`${controllerName}:hasRequest`, this.has.bind(this));
this.messagingSystem.registerActionHandler(`${controllerName}:acceptRequest`, this.accept.bind(this));
this.messagingSystem.registerActionHandler(`${controllerName}:rejectRequest`, this.reject.bind(this));
}
/**
* Adds an approval request per the given arguments, calls the show approval
* request function, and returns the associated approval promise.
*
* There can only be one approval per origin and type. An error is thrown if
* attempting to add an invalid or duplicate request.
*
* @param opts - Options bag.
* @param opts.id - The id of the approval request. A random id will be
* generated if none is provided.
* @param opts.origin - The origin of the approval request.
* @param opts.type - The type associated with the approval request.
* @param opts.requestData - Additional data associated with the request,
* if any.
* @returns The approval promise.
*/
addAndShowApprovalRequest(opts) {
const promise = this._add(opts.origin, opts.type, opts.id, opts.requestData);
this._showApprovalRequest();
return promise;
}
/**
* Adds an approval request per the given arguments and returns the approval
* promise.
*
* There can only be one approval per origin and type. An error is thrown if
* attempting to add an invalid or duplicate request.
*
* @param opts - Options bag.
* @param opts.id - The id of the approval request. A random id will be
* generated if none is provided.
* @param opts.origin - The origin of the approval request.
* @param opts.type - The type associated with the approval request.
* @param opts.requestData - Additional data associated with the request,
* if any.
* @returns The approval promise.
*/
add(opts) {
return this._add(opts.origin, opts.type, opts.id, opts.requestData);
}
/**
* Gets the info for the approval request with the given id.
*
* @param id - The id of the approval request.
* @returns The approval request data associated with the id.
*/
get(id) {
return this.state.pendingApprovals[id];
}
/**
* Gets the number of pending approvals, by origin and/or type.
*
* If only `origin` is specified, all approvals for that origin will be
* counted, regardless of type.
* If only `type` is specified, all approvals for that type will be counted,
* regardless of origin.
* If both `origin` and `type` are specified, 0 or 1 will be returned.
*
* @param opts - The approval count options.
* @param opts.origin - An approval origin.
* @param opts.type - The type of the approval request.
* @returns The current approval request count for the given origin and/or
* type.
*/
getApprovalCount(opts = {}) {
var _a, _b;
if (!opts.origin && !opts.type) {
throw new Error('Must specify origin, type, or both.');
}
const { origin, type: _type } = opts;
if (origin && _type) {
return Number(Boolean((_a = this._origins.get(origin)) === null || _a === void 0 ? void 0 : _a.has(_type)));
}
if (origin) {
return ((_b = this._origins.get(origin)) === null || _b === void 0 ? void 0 : _b.size) || 0;
}
// Only "type" was specified
let count = 0;
for (const approval of Object.values(this.state.pendingApprovals)) {
if (approval.type === _type) {
count += 1;
}
}
return count;
}
/**
* Get the total count of all pending approval requests for all origins.
*
* @returns The total pending approval request count.
*/
getTotalApprovalCount() {
return this.state.pendingApprovalCount;
}
/**
* Checks if there's a pending approval request per the given parameters.
* At least one parameter must be specified. An error will be thrown if the
* parameters are invalid.
*
* If `id` is specified, all other parameters will be ignored.
* If `id` is not specified, the method will check for requests that match
* all of the specified parameters.
*
* @param opts - Options bag.
* @param opts.id - The ID to check for.
* @param opts.origin - The origin to check for.
* @param opts.type - The type to check for.
* @returns `true` if a matching approval is found, and `false` otherwise.
*/
has(opts = {}) {
var _a;
const { id, origin, type: _type } = opts;
if (id) {
if (typeof id !== 'string') {
throw new Error('May not specify non-string id.');
}
return this._approvals.has(id);
}
if (_type && typeof _type !== 'string') {
throw new Error('May not specify non-string type.');
}
if (origin) {
if (typeof origin !== 'string') {
throw new Error('May not specify non-string origin.');
}
// Check origin and type pair if type also specified
if (_type) {
return Boolean((_a = this._origins.get(origin)) === null || _a === void 0 ? void 0 : _a.has(_type));
}
return this._origins.has(origin);
}
if (_type) {
for (const approval of Object.values(this.state.pendingApprovals)) {
if (approval.type === _type) {
return true;
}
}
return false;
}
throw new Error('Must specify a valid combination of id, origin, and type.');
}
/**
* Resolves the promise of the approval with the given id, and deletes the
* approval. Throws an error if no such approval exists.
*
* @param id - The id of the approval request.
* @param value - The value to resolve the approval promise with.
*/
accept(id, value) {
this._deleteApprovalAndGetCallbacks(id).resolve(value);
}
/**
* Rejects the promise of the approval with the given id, and deletes the
* approval. Throws an error if no such approval exists.
*
* @param id - The id of the approval request.
* @param error - The error to reject the approval promise with.
*/
reject(id, error) {
this._deleteApprovalAndGetCallbacks(id).reject(error);
}
/**
* Rejects and deletes all approval requests.
*
* @param rejectionError - The EthereumRpcError to reject the approval
* requests with.
*/
clear(rejectionError) {
for (const id of this._approvals.keys()) {
this.reject(id, rejectionError);
}
this._origins.clear();
this.update(() => getDefaultState());
}
/**
* Implementation of add operation.
*
* @param origin - The origin of the approval request.
* @param type - The type associated with the approval request.
* @param id - The id of the approval request.
* @param requestData - The request data associated with the approval request.
* @returns The approval promise.
*/
_add(origin, type, id = (0, nanoid_1.nanoid)(), requestData) {
var _a;
this._validateAddParams(id, origin, type, requestData);
if ((_a = this._origins.get(origin)) === null || _a === void 0 ? void 0 : _a.has(type)) {
throw eth_rpc_errors_1.ethErrors.rpc.resourceUnavailable(getAlreadyPendingMessage(origin, type));
}
// add pending approval
return new Promise((resolve, reject) => {
this._approvals.set(id, { resolve, reject });
this._addPendingApprovalOrigin(origin, type);
this._addToStore(id, origin, type, requestData);
});
}
/**
* Validates parameters to the add method.
*
* @param id - The id of the approval request.
* @param origin - The origin of the approval request.
* @param type - The type associated with the approval request.
* @param requestData - The request data associated with the approval request.
*/
_validateAddParams(id, origin, type, requestData) {
let errorMessage = null;
if (!id || typeof id !== 'string') {
errorMessage = 'Must specify non-empty string id.';
}
else if (this._approvals.has(id)) {
errorMessage = `Approval request with id '${id}' already exists.`;
}
else if (!origin || typeof origin !== 'string') {
errorMessage = 'Must specify non-empty string origin.';
}
else if (!type || typeof type !== 'string') {
errorMessage = 'Must specify non-empty string type.';
}
else if (requestData &&
(typeof requestData !== 'object' || Array.isArray(requestData))) {
errorMessage = 'Request data must be a plain object if specified.';
}
if (errorMessage) {
throw eth_rpc_errors_1.ethErrors.rpc.internal(errorMessage);
}
}
/**
* Adds an entry to _origins.
* Performs no validation.
*
* @param origin - The origin of the approval request.
* @param type - The type associated with the approval request.
*/
_addPendingApprovalOrigin(origin, type) {
const originSet = this._origins.get(origin) || new Set();
originSet.add(type);
if (!this._origins.has(origin)) {
this._origins.set(origin, originSet);
}
}
/**
* Adds an entry to the store.
* Performs no validation.
*
* @param id - The id of the approval request.
* @param origin - The origin of the approval request.
* @param type - The type associated with the approval request.
* @param requestData - The request data associated with the approval request.
*/
_addToStore(id, origin, type, requestData) {
const approval = {
id,
origin,
type,
time: Date.now(),
requestData: requestData || null,
};
this.update((draftState) => {
// Typecast: ts(2589)
draftState.pendingApprovals[id] = approval;
draftState.pendingApprovalCount = Object.keys(draftState.pendingApprovals).length;
});
}
/**
* Deletes the approval with the given id. The approval promise must be
* resolved or reject before this method is called.
* Deletion is an internal operation because approval state is solely
* managed by this controller.
*
* @param id - The id of the approval request to be deleted.
*/
_delete(id) {
this._approvals.delete(id);
// This method is only called after verifying that the approval with the
// specified id exists.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { origin, type } = this.state.pendingApprovals[id];
this._origins.get(origin).delete(type);
if (this._isEmptyOrigin(origin)) {
this._origins.delete(origin);
}
this.update((draftState) => {
delete draftState.pendingApprovals[id];
draftState.pendingApprovalCount = Object.keys(draftState.pendingApprovals).length;
});
}
/**
* Gets the approval callbacks for the given id, deletes the entry, and then
* returns the callbacks for promise resolution.
* Throws an error if no approval is found for the given id.
*
* @param id - The id of the approval request.
* @returns The promise callbacks associated with the approval request.
*/
_deleteApprovalAndGetCallbacks(id) {
const callbacks = this._approvals.get(id);
if (!callbacks) {
throw new Error(`Approval request with id '${id}' not found.`);
}
this._delete(id);
return callbacks;
}
/**
* Checks whether there are any approvals associated with the given
* origin.
*
* @param origin - The origin to check.
* @returns True if the origin has no approvals, false otherwise.
*/
_isEmptyOrigin(origin) {
var _a;
return !((_a = this._origins.get(origin)) === null || _a === void 0 ? void 0 : _a.size);
}
}
exports.ApprovalController = ApprovalController;
exports.default = ApprovalController;
//# sourceMappingURL=ApprovalController.js.map