UNPKG

@colony/purser-metamask

Version:

A javascript library to interact with a Metamask based Ethereum wallet

512 lines (461 loc) 18.5 kB
import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties"; import _regeneratorRuntime from "@babel/runtime/regenerator"; import _asyncToGenerator from "@babel/runtime/helpers/esm/asyncToGenerator"; import { Transaction as EthereumTx } from 'ethereumjs-tx'; import BigNumber from 'bn.js'; import { awaitTx } from 'await-transaction-mined'; import { warning } from '@colony/purser-core/utils'; import { hexSequenceValidator, addressValidator, safeIntegerValidator } from '@colony/purser-core/validators'; import { addressNormalizer, hexSequenceNormalizer } from '@colony/purser-core/normalizers'; import { transactionObjectValidator, messageVerificationObjectValidator, messageOrDataValidator, getChainDefinition } from '@colony/purser-core/helpers'; import { HEX_HASH_TYPE } from '@colony/purser-core/defaults'; import { methodCaller } from './helpers'; import { getTransaction as getTransactionMethodLink, signTransaction as signTransactionMethodLink, signMessage as signMessageMethodLink, verifyMessage as verifyMessageMethodLink } from './methodLinks'; import { STD_ERRORS } from './defaults'; import { staticMethods as messages } from './messages'; /** * Get a transaction, with a workaround for some providers not returning * a pending transaction. * * If the transaction was not immediately returned, it's possible that * Infura is being used, and it isn't responding to `eth_getTransaction` * in the expected way (i.e. it isn't returning anything because the * transaction is not yet confirmed). * * This method uses a web3 0.20.x-compatible means of waiting for the * transaction to be confirmed (which will resolve to the receipt, * or reject if the transaction could not be confirmed. * * This can probably be removed when MetaMask has its own workaround. * See https://github.com/MetaMask/metamask-extension/issues/6704 */ export var getTransaction = /*#__PURE__*/ function () { var _ref = _asyncToGenerator( /*#__PURE__*/ _regeneratorRuntime.mark(function _callee(transactionHash) { var receiptPromise, transaction; return _regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: receiptPromise = awaitTx(global.web3, transactionHash, { blocksToWait: 1 }); _context.next = 3; return getTransactionMethodLink(transactionHash); case 3: transaction = _context.sent; if (!transaction) { _context.next = 6; break; } return _context.abrupt("return", transaction); case 6: _context.next = 8; return receiptPromise; case 8: return _context.abrupt("return", getTransactionMethodLink(transactionHash)); case 9: case "end": return _context.stop(); } } }, _callee, this); })); return function getTransaction(_x) { return _ref.apply(this, arguments); }; }(); export var signTransactionCallback = function signTransactionCallback(chainId, resolve, reject) { return ( /*#__PURE__*/ function () { var _ref2 = _asyncToGenerator( /*#__PURE__*/ _regeneratorRuntime.mark(function _callee2(error, transactionHash) { var normalizedTransactionHash, _ref3, gas, signedGasPrice, signedData, nonce, r, s, signedTo, v, signedValue, signedTransaction, serializedSignedTransaction, normalizedSignedTransaction; return _regeneratorRuntime.wrap(function _callee2$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: _context2.prev = 0; if (!error) { _context2.next = 5; break; } if (!error.message.includes(STD_ERRORS.CANCEL_TX_SIGN)) { _context2.next = 4; break; } throw new Error(messages.cancelTransactionSign); case 4: throw new Error(error.message); case 5: /* * Validate that the signature hash is in the correct format */ hexSequenceValidator(transactionHash); /* * Add the `0x` prefix to the signed transaction hash */ normalizedTransactionHash = hexSequenceNormalizer(transactionHash); /* * Get signed transaction object with transaction hash using Web3 * Include signature + any values MetaMask may have changed. */ _context2.next = 9; return getTransaction(normalizedTransactionHash); case 9: _ref3 = _context2.sent; gas = _ref3.gas; signedGasPrice = _ref3.gasPrice; signedData = _ref3.input; nonce = _ref3.nonce; r = _ref3.r; s = _ref3.s; signedTo = _ref3.to; v = _ref3.v; signedValue = _ref3.value; /* * RLP encode (to hex string) with ethereumjs-tx, prefix with * `0x` and return. Convert to BN all the numbers-as-strings. */ signedTransaction = new EthereumTx({ data: signedData, gasLimit: new BigNumber(gas), gasPrice: new BigNumber(signedGasPrice), nonce: new BigNumber(nonce), r: r, s: s, to: signedTo, v: v, value: new BigNumber(signedValue) }, getChainDefinition(chainId)); serializedSignedTransaction = signedTransaction.serialize().toString(HEX_HASH_TYPE); normalizedSignedTransaction = hexSequenceNormalizer(serializedSignedTransaction); return _context2.abrupt("return", resolve(normalizedSignedTransaction)); case 25: _context2.prev = 25; _context2.t0 = _context2["catch"](0); return _context2.abrupt("return", reject(_context2.t0)); case 28: case "end": return _context2.stop(); } } }, _callee2, this, [[0, 25]]); })); return function (_x2, _x3) { return _ref2.apply(this, arguments); }; }() ); }; /** * Sign (and send) a transaction object and return the serialized signature (as a hex string) * * @TODO Refactor to only sign the transaction * This is only after Metamask will allow us that functionality (see below) * * Metamask doesn't currently allow us to sign a transaction without also broadcasting it to * the network. See this issue for context: * https://github.com/MetaMask/metamask-extension/issues/3475 * * @method signTransaction * * @param {string} from the sender address (provided by the Wallet instance) * @param {bigNumber} gasPrice gas price for the transaction in WEI (as an instance of bigNumber), defaults to 9000000000 (9 GWEI) * @param {bigNumber} gasLimit gas limit for the transaction (as an instance of bigNumber), defaults to 21000 * @param {number} nonce the nonce to use for the transaction (as a number) * @param {string} to the address to which to the transaction is sent * @param {bigNumber} value the value of the transaction in WEI (as an instance of bigNumber), defaults to 1 * @param {string} inputData data appended to the transaction (as a `hex` string) * * All the above params are sent in as props of an object. * * @return {Promise<string>} the hex signature string */ export var signTransaction = /*#__PURE__*/ function () { var _ref4 = _asyncToGenerator( /*#__PURE__*/ _regeneratorRuntime.mark(function _callee3() { var _ref5, from, manualNonce, transactionObject, _transactionObjectVal, chainId, gasPrice, gasLimit, to, value, inputData, _args3 = arguments; return _regeneratorRuntime.wrap(function _callee3$(_context3) { while (1) { switch (_context3.prev = _context3.next) { case 0: _ref5 = _args3.length > 0 && _args3[0] !== undefined ? _args3[0] : {}, from = _ref5.from, manualNonce = _ref5.nonce, transactionObject = _objectWithoutProperties(_ref5, ["from", "nonce"]); _transactionObjectVal = transactionObjectValidator(transactionObject), chainId = _transactionObjectVal.chainId, gasPrice = _transactionObjectVal.gasPrice, gasLimit = _transactionObjectVal.gasLimit, to = _transactionObjectVal.to, value = _transactionObjectVal.value, inputData = _transactionObjectVal.inputData; addressValidator(from); /* * Metamask auto-sets the nonce based on the next one available. You can manually * override it, but it's best to omit it. * * So we only validate if there is one, otherwise we just pass undefined * to the transaction object. * * We also notify (in dev mode) the user about not setting the nonce. */ if (manualNonce) { safeIntegerValidator(manualNonce); warning(messages.dontSetNonce); } /* * 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. */ return _context3.abrupt("return", methodCaller( /* * @TODO Move into own (non-anonymous) method * This way we could better test it */ function () { return new Promise(function (resolve, reject) { return signTransactionMethodLink(Object.assign({}, { from: addressNormalizer(from), /* * We don't need to normalize these three values since Metamask accepts * number values directly, so we don't need to convert them to hex */ value: value.toString(), gas: gasLimit.toString(), gasPrice: gasPrice.toString(), data: hexSequenceNormalizer(inputData), chainId: chainId, /* * Most likely this value is `undefined`, but that is good (see above) */ nonce: manualNonce }, /* * Only send (and normalize) the destination address if one was * provided in the initial transaction object. */ to ? { to: addressNormalizer(to) } : {}), signTransactionCallback(chainId, resolve, reject)); }); }, messages.cannotSendTransaction)); case 5: case "end": return _context3.stop(); } } }, _callee3, this); })); return function signTransaction() { return _ref4.apply(this, arguments); }; }(); export var signMessageCallback = function signMessageCallback(resolve, reject) { return function (error, messageSignature) { try { if (error) { /* * If the user cancels signing the message we still throw, * but we customize the message */ if (error.message.includes(STD_ERRORS.CANCEL_MSG_SIGN)) { throw new Error(messages.cancelMessageSign); } throw new Error(error.message); } /* * Validate that the signature is in the correct format */ hexSequenceValidator(messageSignature); /* * Add the `0x` prefix to the message's signature */ var normalizedSignature = hexSequenceNormalizer(messageSignature); return resolve(normalizedSignature); } catch (caughtError) { return reject(caughtError); } }; }; /** * Sign a message and return the signature. Useful for verifying identities. * * @method signMessage * * @param {string} currentAddress The current selected address (in the UI) * @param {string} message the message you want to sign * @param {any} messageData the message data (hex string or UInt8Array) you want to sign * * All the above params are sent in as props of an {object. * * @return {Promise<string>} The signed message `hex` string (wrapped inside a `Promise`) */ export var signMessage = /*#__PURE__*/ function () { var _ref6 = _asyncToGenerator( /*#__PURE__*/ _regeneratorRuntime.mark(function _callee4() { var _ref7, currentAddress, message, messageData, toSign, _args4 = arguments; return _regeneratorRuntime.wrap(function _callee4$(_context4) { while (1) { switch (_context4.prev = _context4.next) { case 0: _ref7 = _args4.length > 0 && _args4[0] !== undefined ? _args4[0] : {}, currentAddress = _ref7.currentAddress, message = _ref7.message, messageData = _ref7.messageData; addressValidator(currentAddress); toSign = messageOrDataValidator({ message: message, messageData: messageData }); /* * 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. */ 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, reject) { /* * 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(toSign).toString(HEX_HASH_TYPE)), currentAddress, signMessageCallback(resolve, reject)); }); }, messages.cannotSignMessage)); case 4: case "end": return _context4.stop(); } } }, _callee4, this); })); return function signMessage() { return _ref6.apply(this, arguments); }; }(); export var verifyMessageCallback = function verifyMessageCallback(currentAddress, resolve, reject) { return function (error, recoveredAddress) { try { if (error) { throw new Error(error.message); } /* * Validate that the recovered address is correct */ addressValidator(recoveredAddress); /* * Add the `0x` prefix to the recovered address */ var normalizedRecoveredAddress = addressNormalizer(recoveredAddress); /* * Add the `0x` prefix to the current address */ var normalizedCurrentAddress = addressNormalizer(currentAddress); return resolve(normalizedRecoveredAddress === normalizedCurrentAddress); } catch (caughtError) { return reject(caughtError); } }; }; /** * Verify a signed message. Useful for verifying identity. (In conjunction with `signMessage`) * * @method verifyMessage * * @param {string} message The message to verify if it was signed correctly * @param {string} signature The message signature as a `hex` string (you usually get this via `signMessage`) * @param {string} currentAddress The current selected address (in the UI) * * All the above params are sent in as props of an object. * * @return {Promise<boolean>} A boolean to indicate if the message/signature pair are valid (wrapped inside a `Promise`) */ export var verifyMessage = /*#__PURE__*/ function () { var _ref8 = _asyncToGenerator( /*#__PURE__*/ _regeneratorRuntime.mark(function _callee5() { var _ref9, currentAddress, messageVerificationObject, _messageVerificationO, message, signature, _args5 = arguments; return _regeneratorRuntime.wrap(function _callee5$(_context5) { while (1) { switch (_context5.prev = _context5.next) { case 0: _ref9 = _args5.length > 0 && _args5[0] !== undefined ? _args5[0] : {}, currentAddress = _ref9.currentAddress, messageVerificationObject = _objectWithoutProperties(_ref9, ["currentAddress"]); /* * Validate the current address */ addressValidator(currentAddress); /* * Validate the rest of the pros using the core helper */ _messageVerificationO = messageVerificationObjectValidator(messageVerificationObject), message = _messageVerificationO.message, signature = _messageVerificationO.signature; /* * 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. */ return _context5.abrupt("return", methodCaller( /* * @TODO Move into own (non-anonymous) method * This way we could better test it */ function () { return new Promise(function (resolve, reject) { /* * Verify the message */ verifyMessageMethodLink(message, /* * Ensure the signature has the `0x` prefix */ hexSequenceNormalizer(signature), verifyMessageCallback(currentAddress, resolve, reject)); }); }, messages.cannotSignMessage)); case 4: case "end": return _context5.stop(); } } }, _callee5, this); })); return function verifyMessage() { return _ref8.apply(this, arguments); }; }();