@colony/purser-metamask
Version:
A javascript library to interact with a Metamask based Ethereum wallet
371 lines (340 loc) • 13.1 kB
JavaScript
import _objectSpread from "@babel/runtime/helpers/esm/objectSpread";
import _regeneratorRuntime from "@babel/runtime/regenerator";
import _asyncToGenerator from "@babel/runtime/helpers/esm/asyncToGenerator";
import _classCallCheck from "@babel/runtime/helpers/esm/classCallCheck";
import _createClass from "@babel/runtime/helpers/esm/createClass";
import isEqual from 'lodash.isequal';
import { warning } from '@colony/purser-core/utils';
import { recoverPublicKey as recoverPublicKeyHelper, userInputValidator } from '@colony/purser-core/helpers';
import { addressValidator, hexSequenceValidator } from '@colony/purser-core/validators';
import { hexSequenceNormalizer } from '@colony/purser-core/normalizers';
import { DESCRIPTORS, HEX_HASH_TYPE, REQUIRED_PROPS } from '@colony/purser-core/defaults';
import { TYPE_SOFTWARE, SUBTYPE_METAMASK } from '@colony/purser-core/types';
import { signTransaction, signMessage, verifyMessage } from './staticMethods';
import { methodCaller, setStateEventObserver } from './helpers';
import { validateMetamaskState } from './validators';
import { signMessage as signMessageMethodLink } from './methodLinks';
import { PUBLICKEY_RECOVERY_MESSAGE, STD_ERRORS } from './defaults';
import { MetamaskWallet as messages, staticMethods as staticMethodsMessages } from './messages';
var SETTERS = DESCRIPTORS.SETTERS,
GETTERS = DESCRIPTORS.GETTERS,
GENERIC_PROPS = DESCRIPTORS.GENERIC_PROPS,
WALLET_PROPS = 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;
_classCallCheck(this, MetamaskWallet);
/*
* Validate the address that's coming in from Metamask
*/
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: TYPE_SOFTWARE
}, GENERIC_PROPS),
subtype: Object.assign({}, {
value: SUBTYPE_METAMASK
}, GENERIC_PROPS),
sign: Object.assign({}, {
value: function () {
var _value = _asyncToGenerator(
/*#__PURE__*/
_regeneratorRuntime.mark(function _callee(transactionObject) {
return _regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
/*
* Validate the trasaction's object input
*/
userInputValidator({
firstArgument: transactionObject
});
return _context.abrupt("return", 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 = _asyncToGenerator(
/*#__PURE__*/
_regeneratorRuntime.mark(function _callee2() {
var messageObject,
_args2 = arguments;
return _regeneratorRuntime.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
*/
userInputValidator({
firstArgument: messageObject,
requiredOr: REQUIRED_PROPS.SIGN_MESSAGE
});
return _context2.abrupt("return", 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 = _asyncToGenerator(
/*#__PURE__*/
_regeneratorRuntime.mark(function _callee3(messageVerificationObject) {
return _regeneratorRuntime.wrap(function _callee3$(_context3) {
while (1) {
switch (_context3.prev = _context3.next) {
case 0:
/*
* Validate the trasaction's object input
*/
userInputValidator({
firstArgument: messageVerificationObject,
requiredAll: REQUIRED_PROPS.VERIFY_MESSAGE
});
return _context3.abrupt("return", verifyMessage(_objectSpread({
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.
*/
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 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.
*/
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 (!isEqual(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.cannotObserve);
}
/*
* Public Key Getter
*/
/* eslint-disable-next-line class-methods-use-this */
_createClass(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 = _asyncToGenerator(
/*#__PURE__*/
_regeneratorRuntime.mark(function _callee4(currentAddress) {
return _regeneratorRuntime.wrap(function _callee4$(_context4) {
while (1) {
switch (_context4.prev = _context4.next) {
case 0:
return _context4.abrupt("return", 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
*/
signMessageMethodLink(
/*
* Ensure the hex string has the `0x` prefix
*/
hexSequenceNormalizer(
/*
* We could really do with default Flow types for Buffer...
*/
/* $FlowFixMe */
Buffer.from(PUBLICKEY_RECOVERY_MESSAGE).toString(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
*/
hexSequenceValidator(signature);
var recoveredPublicKey = recoverPublicKeyHelper({
message: PUBLICKEY_RECOVERY_MESSAGE,
signature: signature
});
/*
* Add the `0x` prefix to the recovered public key
*/
var normalizedPublicKey = 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(STD_ERRORS.CANCEL_MSG_SIGN)) {
return warning(staticMethodsMessages.cancelMessageSign);
}
throw new Error(error.message);
}
});
});
}, messages.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
*/
export { MetamaskWallet as default };
Object.defineProperties(MetamaskWallet.prototype, {
publicKey: GETTERS
});