@colony/purser-metamask
Version:
A javascript library to interact with a Metamask based Ethereum wallet
555 lines (474 loc) • 19.7 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.verifyMessage = exports.verifyMessageCallback = exports.signMessage = exports.signMessageCallback = exports.signTransaction = exports.signTransactionCallback = exports.getTransaction = void 0;
var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));
var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
var _ethereumjsTx = require("ethereumjs-tx");
var _bn = _interopRequireDefault(require("bn.js"));
var _awaitTransactionMined = require("await-transaction-mined");
var _utils = require("@colony/purser-core/utils");
var _validators = require("@colony/purser-core/validators");
var _normalizers = require("@colony/purser-core/normalizers");
var _helpers = require("@colony/purser-core/helpers");
var _defaults = require("@colony/purser-core/defaults");
var _helpers2 = require("./helpers");
var _methodLinks = require("./methodLinks");
var _defaults2 = require("./defaults");
var _messages = require("./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
*/
var getTransaction =
/*#__PURE__*/
function () {
var _ref = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee(transactionHash) {
var receiptPromise, transaction;
return _regenerator.default.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
receiptPromise = (0, _awaitTransactionMined.awaitTx)(global.web3, transactionHash, {
blocksToWait: 1
});
_context.next = 3;
return (0, _methodLinks.getTransaction)(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", (0, _methodLinks.getTransaction)(transactionHash));
case 9:
case "end":
return _context.stop();
}
}
}, _callee, this);
}));
return function getTransaction(_x) {
return _ref.apply(this, arguments);
};
}();
exports.getTransaction = getTransaction;
var signTransactionCallback = function signTransactionCallback(chainId, resolve, reject) {
return (
/*#__PURE__*/
function () {
var _ref2 = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee2(error, transactionHash) {
var normalizedTransactionHash, _ref3, gas, signedGasPrice, signedData, nonce, r, s, signedTo, v, signedValue, signedTransaction, serializedSignedTransaction, normalizedSignedTransaction;
return _regenerator.default.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(_defaults2.STD_ERRORS.CANCEL_TX_SIGN)) {
_context2.next = 4;
break;
}
throw new Error(_messages.staticMethods.cancelTransactionSign);
case 4:
throw new Error(error.message);
case 5:
/*
* Validate that the signature hash is in the correct format
*/
(0, _validators.hexSequenceValidator)(transactionHash);
/*
* Add the `0x` prefix to the signed transaction hash
*/
normalizedTransactionHash = (0, _normalizers.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 _ethereumjsTx.Transaction({
data: signedData,
gasLimit: new _bn.default(gas),
gasPrice: new _bn.default(signedGasPrice),
nonce: new _bn.default(nonce),
r: r,
s: s,
to: signedTo,
v: v,
value: new _bn.default(signedValue)
}, (0, _helpers.getChainDefinition)(chainId));
serializedSignedTransaction = signedTransaction.serialize().toString(_defaults.HEX_HASH_TYPE);
normalizedSignedTransaction = (0, _normalizers.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
*/
exports.signTransactionCallback = signTransactionCallback;
var signTransaction =
/*#__PURE__*/
function () {
var _ref4 = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee3() {
var _ref5,
from,
manualNonce,
transactionObject,
_transactionObjectVal,
chainId,
gasPrice,
gasLimit,
to,
value,
inputData,
_args3 = arguments;
return _regenerator.default.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 = (0, _objectWithoutProperties2.default)(_ref5, ["from", "nonce"]);
_transactionObjectVal = (0, _helpers.transactionObjectValidator)(transactionObject), chainId = _transactionObjectVal.chainId, gasPrice = _transactionObjectVal.gasPrice, gasLimit = _transactionObjectVal.gasLimit, to = _transactionObjectVal.to, value = _transactionObjectVal.value, inputData = _transactionObjectVal.inputData;
(0, _validators.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) {
(0, _validators.safeIntegerValidator)(manualNonce);
(0, _utils.warning)(_messages.staticMethods.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", (0, _helpers2.methodCaller)(
/*
* @TODO Move into own (non-anonymous) method
* This way we could better test it
*/
function () {
return new Promise(function (resolve, reject) {
return (0, _methodLinks.signTransaction)(Object.assign({}, {
from: (0, _normalizers.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: (0, _normalizers.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: (0, _normalizers.addressNormalizer)(to)
} : {}), signTransactionCallback(chainId, resolve, reject));
});
}, _messages.staticMethods.cannotSendTransaction));
case 5:
case "end":
return _context3.stop();
}
}
}, _callee3, this);
}));
return function signTransaction() {
return _ref4.apply(this, arguments);
};
}();
exports.signTransaction = signTransaction;
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(_defaults2.STD_ERRORS.CANCEL_MSG_SIGN)) {
throw new Error(_messages.staticMethods.cancelMessageSign);
}
throw new Error(error.message);
}
/*
* Validate that the signature is in the correct format
*/
(0, _validators.hexSequenceValidator)(messageSignature);
/*
* Add the `0x` prefix to the message's signature
*/
var normalizedSignature = (0, _normalizers.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`)
*/
exports.signMessageCallback = signMessageCallback;
var signMessage =
/*#__PURE__*/
function () {
var _ref6 = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee4() {
var _ref7,
currentAddress,
message,
messageData,
toSign,
_args4 = arguments;
return _regenerator.default.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;
(0, _validators.addressValidator)(currentAddress);
toSign = (0, _helpers.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", (0, _helpers2.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
*/
(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(toSign).toString(_defaults.HEX_HASH_TYPE)), currentAddress, signMessageCallback(resolve, reject));
});
}, _messages.staticMethods.cannotSignMessage));
case 4:
case "end":
return _context4.stop();
}
}
}, _callee4, this);
}));
return function signMessage() {
return _ref6.apply(this, arguments);
};
}();
exports.signMessage = signMessage;
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
*/
(0, _validators.addressValidator)(recoveredAddress);
/*
* Add the `0x` prefix to the recovered address
*/
var normalizedRecoveredAddress = (0, _normalizers.addressNormalizer)(recoveredAddress);
/*
* Add the `0x` prefix to the current address
*/
var normalizedCurrentAddress = (0, _normalizers.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`)
*/
exports.verifyMessageCallback = verifyMessageCallback;
var verifyMessage =
/*#__PURE__*/
function () {
var _ref8 = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee5() {
var _ref9,
currentAddress,
messageVerificationObject,
_messageVerificationO,
message,
signature,
_args5 = arguments;
return _regenerator.default.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 = (0, _objectWithoutProperties2.default)(_ref9, ["currentAddress"]);
/*
* Validate the current address
*/
(0, _validators.addressValidator)(currentAddress);
/*
* Validate the rest of the pros using the core helper
*/
_messageVerificationO = (0, _helpers.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", (0, _helpers2.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
*/
(0, _methodLinks.verifyMessage)(message,
/*
* Ensure the signature has the `0x` prefix
*/
(0, _normalizers.hexSequenceNormalizer)(signature), verifyMessageCallback(currentAddress, resolve, reject));
});
}, _messages.staticMethods.cannotSignMessage));
case 4:
case "end":
return _context5.stop();
}
}
}, _callee5, this);
}));
return function verifyMessage() {
return _ref8.apply(this, arguments);
};
}();
exports.verifyMessage = verifyMessage;