@colony/purser-metamask
Version:
A javascript library to interact with a Metamask based Ethereum wallet
396 lines (346 loc) • 13.7 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread"));
var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _lodash = _interopRequireDefault(require("lodash.isequal"));
var _utils = require("@colony/purser-core/utils");
var _helpers = require("@colony/purser-core/helpers");
var _validators = require("@colony/purser-core/validators");
var _normalizers = require("@colony/purser-core/normalizers");
var _defaults = require("@colony/purser-core/defaults");
var _types = require("@colony/purser-core/types");
var _staticMethods = require("./staticMethods");
var _helpers2 = require("./helpers");
var _validators2 = require("./validators");
var _methodLinks = require("./methodLinks");
var _defaults2 = require("./defaults");
var _messages = require("./messages");
var SETTERS = _defaults.DESCRIPTORS.SETTERS,
GETTERS = _defaults.DESCRIPTORS.GETTERS,
GENERIC_PROPS = _defaults.DESCRIPTORS.GENERIC_PROPS,
WALLET_PROPS = _defaults.DESCRIPTORS.WALLET_PROPS;
/*
* "Private" (internal) variable(s).
*/
var state = {};
var internalPublicKey;
var MetamaskWallet =
/*#__PURE__*/
function () {
/*
* `publicKey` prop is a getter
*/
/*
* @TODO Add specific Flow type
*
* See the core generic wallet for this, since that will implement them.
* This will just use the ones declared there.
*/
function MetamaskWallet(_ref) {
var _this = this;
var address = _ref.address;
(0, _classCallCheck2.default)(this, MetamaskWallet);
/*
* Validate the address that's coming in from Metamask
*/
(0, _validators.addressValidator)(address);
Object.defineProperties(this, {
/*
* The initial address is set when `open()`-ing the wallet, but after that
* it's updated via the Metamask state change observer.
*
* This way, we keep it in sync with the changes from Metamask's UI
*/
address: Object.assign({}, {
value: address
}, SETTERS),
type: Object.assign({}, {
value: _types.TYPE_SOFTWARE
}, GENERIC_PROPS),
subtype: Object.assign({}, {
value: _types.SUBTYPE_METAMASK
}, GENERIC_PROPS),
sign: Object.assign({}, {
value: function () {
var _value = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee(transactionObject) {
return _regenerator.default.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
/*
* Validate the trasaction's object input
*/
(0, _helpers.userInputValidator)({
firstArgument: transactionObject
});
return _context.abrupt("return", (0, _staticMethods.signTransaction)(Object.assign({}, transactionObject, {
from: _this.address
})));
case 2:
case "end":
return _context.stop();
}
}
}, _callee, this);
}));
return function value(_x) {
return _value.apply(this, arguments);
};
}()
}, WALLET_PROPS),
signMessage: Object.assign({}, {
value: function () {
var _value2 = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee2() {
var messageObject,
_args2 = arguments;
return _regenerator.default.wrap(function _callee2$(_context2) {
while (1) {
switch (_context2.prev = _context2.next) {
case 0:
messageObject = _args2.length > 0 && _args2[0] !== undefined ? _args2[0] : {};
/*
* Validate the trasaction's object input
*/
(0, _helpers.userInputValidator)({
firstArgument: messageObject,
requiredOr: _defaults.REQUIRED_PROPS.SIGN_MESSAGE
});
return _context2.abrupt("return", (0, _staticMethods.signMessage)({
currentAddress: _this.address,
message: messageObject.message,
messageData: messageObject.messageData
}));
case 3:
case "end":
return _context2.stop();
}
}
}, _callee2, this);
}));
return function value() {
return _value2.apply(this, arguments);
};
}()
}, WALLET_PROPS),
verifyMessage: Object.assign({}, {
value: function () {
var _value3 = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee3(messageVerificationObject) {
return _regenerator.default.wrap(function _callee3$(_context3) {
while (1) {
switch (_context3.prev = _context3.next) {
case 0:
/*
* Validate the trasaction's object input
*/
(0, _helpers.userInputValidator)({
firstArgument: messageVerificationObject,
requiredAll: _defaults.REQUIRED_PROPS.VERIFY_MESSAGE
});
return _context3.abrupt("return", (0, _staticMethods.verifyMessage)((0, _objectSpread2.default)({
currentAddress: _this.address
}, messageVerificationObject)));
case 2:
case "end":
return _context3.stop();
}
}
}, _callee3, this);
}));
return function value(_x2) {
return _value3.apply(this, arguments);
};
}()
}, WALLET_PROPS)
});
/*
* We must check for the Metamask injected in-page proxy every time we
* try to access it. This is because something can change it from the time
* of last detection until now.
*
* So we must ensure, again, that we have a state update event to hook
* our update method onto.
*/
(0, _helpers2.methodCaller)(
/*
* @TODO Move into own (non-anonymous) method
* This way we could better test it
*
* Set the state change observer
*
* This tracks updates Metamask's states and updates the local address
* value if that changes in the UI
*/
function () {
return (0, _helpers2.setStateEventObserver)(
/*
* @TODO Move into own (non-anonymous) method
* This way we could better test it
*/
function (newState) {
try {
/*
* Validate the state object that's coming in.
* It should have all the props needed for us to work with.
*
* If they aren't there, it means that either Metamask is locked,
* or somebody tampered with them.
*/
(0, _validators2.validateMetamaskState)(newState);
/*
* We only update the values if the state has changed.
* (We're using lodash here to deep compare the two state objects)
*/
if (!(0, _lodash.default)(state, newState)) {
state = newState;
_this.address = newState.selectedAddress;
/*
* Reset the saved public key, as the address now changed
*/
internalPublicKey = undefined;
return true;
}
return false;
} catch (caughtError) {
/*
* We don't want to throw or stop execution, so in the case that the
* state doesn't validate, and update and silently return `false`.
*/
return false;
}
});
}, _messages.MetamaskWallet.cannotObserve);
}
/*
* Public Key Getter
*/
/* eslint-disable-next-line class-methods-use-this */
(0, _createClass2.default)(MetamaskWallet, [{
key: "publicKey",
get: function get() {
/*
* We can't memoize the getter (as we do in most other such getters)
*
* This is because the address could change at any time leaving us with a
* stale value for the public key, as there is no way (currently) to invalidate
* this value.
*/
if (internalPublicKey) {
return Promise.resolve(internalPublicKey);
}
return MetamaskWallet.recoverPublicKey(this.address);
}
/**
* Recover the public key from a signed message.
* Sign a message, and use that signature to recover the (R), (S) signature
* components, along with the reco(V)ery param. We then use those values to
* recover, set internally, and return the public key.
*
* @method recoverPublicKey
*
* @param {string} currentAddress The current selected address.
* Note the we don't need to validate this here since it comes from a trusted
* source: the class constructor.
*
* @return {Promise} The recovered public key (for the currently selected addresss)
*/
}], [{
key: "recoverPublicKey",
value: function () {
var _recoverPublicKey = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee4(currentAddress) {
return _regenerator.default.wrap(function _callee4$(_context4) {
while (1) {
switch (_context4.prev = _context4.next) {
case 0:
return _context4.abrupt("return", (0, _helpers2.methodCaller)(
/*
* @TODO Move into own (non-anonymous) method
* This way we could better test it
*/
function () {
return new Promise(function (resolve) {
/*
* Sign the message. This will prompt the user via Metamask's UI
*/
(0, _methodLinks.signMessage)(
/*
* Ensure the hex string has the `0x` prefix
*/
(0, _normalizers.hexSequenceNormalizer)(
/*
* We could really do with default Flow types for Buffer...
*/
/* $FlowFixMe */
Buffer.from(_defaults2.PUBLICKEY_RECOVERY_MESSAGE).toString(_defaults.HEX_HASH_TYPE)), currentAddress,
/*
* @TODO Move into own (non-anonymous) method
* This way we could better test it
*/
function (error, signature) {
try {
/*
* Validate that the signature is in the correct format
*/
(0, _validators.hexSequenceValidator)(signature);
var recoveredPublicKey = (0, _helpers.recoverPublicKey)({
message: _defaults2.PUBLICKEY_RECOVERY_MESSAGE,
signature: signature
});
/*
* Add the `0x` prefix to the recovered public key
*/
var normalizedPublicKey = (0, _normalizers.hexSequenceNormalizer)(recoveredPublicKey);
/*
* Also set the internal public key
*/
internalPublicKey = normalizedPublicKey;
return resolve(normalizedPublicKey);
} catch (caughtError) {
/*
* Don't throw an Error if the user just cancels signing the message.
* This is normal UX, not an exception
*/
if (error.message.includes(_defaults2.STD_ERRORS.CANCEL_MSG_SIGN)) {
return (0, _utils.warning)(_messages.staticMethods.cancelMessageSign);
}
throw new Error(error.message);
}
});
});
}, _messages.MetamaskWallet.cannotGetPublicKey));
case 1:
case "end":
return _context4.stop();
}
}
}, _callee4, this);
}));
return function recoverPublicKey(_x3) {
return _recoverPublicKey.apply(this, arguments);
};
}()
}]);
return MetamaskWallet;
}();
/*
* We need to use `defineProperties` to make props enumerable.
* When adding them via a `Class` getter/setter it will prevent that by default
*/
exports.default = MetamaskWallet;
Object.defineProperties(MetamaskWallet.prototype, {
publicKey: GETTERS
});