@colony/purser-metamask
Version:
A javascript library to interact with a Metamask based Ethereum wallet
512 lines (461 loc) • 18.5 kB
JavaScript
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);
};
}();