UNPKG

@colony/purser-metamask

Version:

A javascript library to interact with a Metamask based Ethereum wallet

371 lines (340 loc) 13.1 kB
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 });