@colony/purser-core
Version:
A collection of helpers, utils, validators and normalizers to assist the individual purser modules
484 lines (433 loc) • 16.8 kB
JavaScript
import _typeof from "@babel/runtime/helpers/esm/typeof";
import { hashPersonalMessage, ecrecover } from 'ethereumjs-util';
import Common from 'ethereumjs-common';
import { safeIntegerValidator, bigNumberValidator, addressValidator, hexSequenceValidator, messageValidator, messageDataValidator } from './validators';
import { hexSequenceNormalizer, recoveryParamNormalizer } from './normalizers';
import { bigNumber, warning } from './utils';
import { helpers as helperMessages } from './messages';
import { CHAIN_IDS, HARDFORKS, HEX_HASH_TYPE, NETWORK_NAMES, PATH, TRANSACTION } from './defaults';
/**
* Serialize an derivation path object's props into it's string counterpart
*
* @method derivationPathSerializer
*
* @param {number} purpose path purpose
* @param {number} coinType path coin type (and network)
* @param {number} account path account number
* @param {number} change path change number
* @param {number} addressIndex address index (no default since it should be manually added)
*
* See the defaults file for some more information regarding the format of the
* Ethereum deviation path.
*
* All the above params are sent in as props of an {DerivationPathObjectType} object.
*
* @return {string} The serialized path
*/
export var derivationPathSerializer = function derivationPathSerializer() {
var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
_ref$purpose = _ref.purpose,
purpose = _ref$purpose === void 0 ? PATH.PURPOSE : _ref$purpose,
_ref$coinType = _ref.coinType,
coinType = _ref$coinType === void 0 ? PATH.COIN_MAINNET : _ref$coinType,
_ref$account = _ref.account,
account = _ref$account === void 0 ? PATH.ACCOUNT : _ref$account,
change = _ref.change,
addressIndex = _ref.addressIndex;
var DELIMITER = PATH.DELIMITER;
var hasChange = change || change === 0;
var hasAddressIndex = addressIndex || addressIndex === 0;
return (
/*
* It's using a template in the last spot, eslint just donesn't recognizes it...
*/
/* eslint-disable-next-line prefer-template */
"".concat(PATH.HEADER_KEY, "/").concat(purpose) + "".concat(DELIMITER).concat(coinType) + "".concat(DELIMITER).concat(account) + "".concat(DELIMITER) +
/*
* We're already checking if the change and address index has a value, so
* we're not coercing `undefined`.
*
* Flow is overreacting again...
*/
/* $FlowFixMe */
"".concat(hasChange ? change : '') + (
/* $FlowFixMe */
hasChange && hasAddressIndex ? "/".concat(addressIndex) : '')
);
};
/**
* Recover a public key from a message and the signature of that message.
*
* @NOTE Further optimization
*
* This can be further optimized by writing our own recovery mechanism since we already
* do most of the cleanup, checking and coversions.
*
* All that is left to do is to use `secp256k1` to convert and recover the public key
* from the signature points (components).
*
* But since most of our dependencies already use `ethereumjs-util` under the hood anyway,
* it's easier just use it as well.
*
* @method recoverPublicKey
*
* @param {string} message The message string to hash for the signature verification procedure
* @param {string} signature The signature to recover the private key from, as a `hex` string
*
* All the above params are sent in as props of an {MessageVerificationObjectType} object.
*
* @return {String} The recovered public key.
*/
export var recoverPublicKey = function recoverPublicKey() {
var _ref2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
message = _ref2.message,
signature = _ref2.signature;
var messages = helperMessages.verifyMessageSignature;
var signatureBuffer = Buffer.from(
/* $FlowFixMe */
hexSequenceNormalizer(signature.toLowerCase(), false), HEX_HASH_TYPE);
/*
* It should be 65 bits in legth:
* - 32 for the (R) point (component)
* - 32 for the (S) point (component)
* - 1 for the reco(V)ery param
*/
if (signatureBuffer.length !== 65) {
throw new Error(messages.wrongLength);
}
/*
* The recovery param is the the 64th bit of the signature Buffer
*/
var recoveryParam = recoveryParamNormalizer(signatureBuffer[64]);
var rComponent = signatureBuffer.slice(0, 32);
var sComponent = signatureBuffer.slice(32, 64);
var messageHash = hashPersonalMessage(Buffer.from(message));
/*
* Elliptic curve recovery.
*
* @NOTE `ecrecover` is just a helper method
* Around `secp256k1`'s `recover()` and `publicKeyConvert()` methods
*
* This is to what the function description comment block note is referring to
*/
var recoveredPublicKeyBuffer = ecrecover(messageHash, recoveryParam, rComponent, sComponent);
/*
* Normalize and return the recovered public key
*/
return hexSequenceNormalizer(recoveredPublicKeyBuffer.toString(HEX_HASH_TYPE));
};
/**
* Verify a signed message.
* By extracting it's public key from the signature and comparing it with a provided one.
*
* @method verifyMessageSignature
*
* @param {string} publicKey Public key to check against, as a 'hex' string
* @param {string} message The message string to hash for the signature verification procedure
* @param {string} signature The signature to recover the private key from, as a `hex` string
*
* All the above params are sent in as props of an {MessageVerificationObjectType} object.
*
* @return {boolean} true or false depending if the signature is valid or not
*
*/
export var verifyMessageSignature = function verifyMessageSignature(_ref3) {
var publicKey = _ref3.publicKey,
message = _ref3.message,
signature = _ref3.signature;
var messages = helperMessages.verifyMessageSignature;
try {
/*
* Normalize the recovered public key by removing the `0x` preifx
*/
var recoveredPublicKey = hexSequenceNormalizer(
/*
* We need this little go-around trick to mock just one export of
* the module, while leaving the rest of the module intact so we can test it
*
* See: https://github.com/facebook/jest/issues/936
*/
/* eslint-disable-next-line no-use-before-define */
coreHelpers.recoverPublicKey({
message: message,
signature: signature
}), false);
/*
* Remove the prefix (0x) and the header (first two bits) from the public key we
* want to test against
*/
var normalizedPublicKey = hexSequenceNormalizer(publicKey, false).slice(2);
/*
* Last 64 bits of the private should match the first 64 bits of the recovered public key
*/
return !!recoveredPublicKey.includes(normalizedPublicKey);
} catch (caughtError) {
warning("".concat(messages.somethingWentWrong, ". Error: ").concat(caughtError.message), {
level: 'high'
});
return false;
}
};
/**
* Validate an transaction object
*
* @NOTE We can only validate here, we can't also normalize. This is because different
* wallet types expect different value formats so we must normalize them on a case by case basis.
*
* @method transactionObjectValidator
*
* @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} chainId the id of the chain for which this transaction is intended. Defaults to 1
* @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 {TransactionObjectType} object.
*
* @return {TransactionObjectType} The validated transaction object containing the exact passed in values
*/
export var transactionObjectValidator = function transactionObjectValidator() {
var _ref4 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
_ref4$gasPrice = _ref4.gasPrice,
gasPrice = _ref4$gasPrice === void 0 ? bigNumber(TRANSACTION.GAS_PRICE) : _ref4$gasPrice,
_ref4$gasLimit = _ref4.gasLimit,
gasLimit = _ref4$gasLimit === void 0 ? bigNumber(TRANSACTION.GAS_LIMIT) : _ref4$gasLimit,
_ref4$chainId = _ref4.chainId,
chainId = _ref4$chainId === void 0 ? TRANSACTION.CHAIN_ID : _ref4$chainId,
_ref4$nonce = _ref4.nonce,
nonce = _ref4$nonce === void 0 ? TRANSACTION.NONCE : _ref4$nonce,
to = _ref4.to,
_ref4$value = _ref4.value,
value = _ref4$value === void 0 ? bigNumber(TRANSACTION.VALUE) : _ref4$value,
_ref4$inputData = _ref4.inputData,
inputData = _ref4$inputData === void 0 ? TRANSACTION.INPUT_DATA : _ref4$inputData;
/*
* Check that the gas price is a big number
*/
bigNumberValidator(gasPrice);
/*
* Check that the gas limit is a big number
*/
bigNumberValidator(gasLimit);
/*
* Check if the chain id value is valid (a positive, safe integer)
*/
safeIntegerValidator(chainId);
/*
* Check if the nonce value is valid (a positive, safe integer)
*/
safeIntegerValidator(nonce);
/*
* Only check if the address (`to` prop) is in the correct
* format, if one was provided in the initial transaction object
*/
if (to) {
addressValidator(to);
}
/*
* Check that the value is a big number
*/
bigNumberValidator(value);
/*
* Check that the input data prop is a valid hex string sequence
*/
hexSequenceValidator(inputData);
/*
* Normalize the values and return them
*/
return {
gasPrice: gasPrice,
gasLimit: gasLimit,
chainId: chainId,
nonce: nonce,
to: to,
value: value,
inputData: inputData
};
};
/**
* Validate a signature verification message object
*
* @method messageVerificationObjectValidator
*
* @param {string} message The message string to check the signature against
* @param {string} signature The signature of the message.
*
* All the above params are sent in as props of an {MessageVerificationObjectType} object.
*
* @return {Object} The validated signature object containing the exact passed in values
*/
export var messageVerificationObjectValidator = function messageVerificationObjectValidator() {
var _ref5 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
message = _ref5.message,
signature = _ref5.signature;
/*
* Check if the messages is in the correct format
*/
messageValidator(message);
/*
* Check if the signature is in the correct format
*/
hexSequenceValidator(signature);
return {
message: message,
/*
* Ensure the signature has the hex `0x` prefix
*/
signature: hexSequenceNormalizer(signature)
};
};
/**
* Check if the user provided input is in the form of an Object and it's required props
*
* @method userInputValidator
*
* @param {Object} firstArgument The argument to validate that it's indeed an object, and that it has the required props
* @param {Array} requiredEither Array of strings representing prop names of which at least one is required.
* @param {Array} requiredAll Array of strings representing prop names of which all are required.
*
* All the above params are sent in as props of an object.
*/
export var userInputValidator = function userInputValidator() {
var _ref6 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
_ref6$firstArgument = _ref6.firstArgument,
firstArgument = _ref6$firstArgument === void 0 ? {} : _ref6$firstArgument,
_ref6$requiredEither = _ref6.requiredEither,
requiredEither = _ref6$requiredEither === void 0 ? [] : _ref6$requiredEither,
_ref6$requiredAll = _ref6.requiredAll,
requiredAll = _ref6$requiredAll === void 0 ? [] : _ref6$requiredAll,
_ref6$requiredOr = _ref6.requiredOr,
requiredOr = _ref6$requiredOr === void 0 ? [] : _ref6$requiredOr;
var messages = helperMessages.userInputValidator;
/*
* First we check if the argument is an Object (also, not an Array)
*/
if (_typeof(firstArgument) !== 'object' || Array.isArray(firstArgument)) {
/*
* Explain the arguments format (if we're in dev mode), then throw the Error
*/
warning(messages.argumentsFormatExplanation);
throw new Error(messages.notObject);
}
/*
* Check if some of the required props are available
* Fail if none are available.
*/
if (requiredEither.length) {
var availableProps = requiredEither.map(function (propName) {
return Object.prototype.hasOwnProperty.call(firstArgument, propName);
});
if (!availableProps.some(function (propExists) {
return propExists === true;
})) {
/*
* Explain the arguments format (if we're in dev mode), then throw the Error
*/
warning(messages.argumentsFormatExplanation);
throw new Error("".concat(messages.notSomeProps, ": { '").concat(requiredEither.join("', '"), "' }"));
}
}
/*
* Check if all required props are present.
* Fail after the first one missing.
*/
requiredAll.map(function (propName) {
if (!Object.prototype.hasOwnProperty.call(firstArgument, propName)) {
/*
* Explain the arguments format (if we're in dev mode), then throw the Error
*/
warning(messages.argumentsFormatExplanation);
throw new Error("".concat(messages.notAllProps, ": { '").concat(requiredAll.join("', '"), "' }"));
}
return propName;
});
/*
* Check if exactly one of the required props is present.
* Fail if multiple are present.
*/
if (requiredOr.length && requiredOr.reduce(function (acc, propName) {
return Object.prototype.hasOwnProperty.call(firstArgument, propName) ? acc + 1 : acc;
}, 0) !== 1) {
warning(messages.argumentsFormatExplanation);
throw new Error();
}
};
export var messageOrDataValidator = function messageOrDataValidator(_ref7) {
var message = _ref7.message,
messageData = _ref7.messageData;
if (message) {
messageValidator(message);
return message;
}
messageDataValidator(messageData);
return typeof messageData === 'string' ? new Uint8Array(Buffer.from(hexSequenceNormalizer(messageData, false), 'hex')) : messageData;
};
/**
* In order to support EIP-155, it's necessary to specify various
* definitions for a given chain (e.g. the chain ID, network ID, hardforks).
*
* Given a chain ID, this function returns a chain definition in the format
* expected by `ethereumjs-tx`.
*
* @param {number} chainId The given chain ID (as defined in EIP-155)
* @return {Object} The common chain definition
*/
export var getChainDefinition = function getChainDefinition(chainId) {
var baseChain = function () {
switch (chainId) {
/*
* Ganache's default chain ID is 1337, and is also the standard for
* private chains. The assumption is taken here that this inherits
* all of the other properties from mainnet, but that might not be
* the case.
*
* @TODO Provide a means to specify all chain properties for transactions
*/
case CHAIN_IDS.HOMESTEAD:
case CHAIN_IDS.LOCAL:
return NETWORK_NAMES.MAINNET;
case CHAIN_IDS.GOERLI:
return NETWORK_NAMES.GOERLI;
/*
* The following (or other) chain IDs _may_ cause validation errors
* in `ethereumjs-common`
*/
case CHAIN_IDS.KOVAN:
return NETWORK_NAMES.KOVAN;
case CHAIN_IDS.ROPSTEN:
return NETWORK_NAMES.ROPSTEN;
case CHAIN_IDS.RINKEBY:
return NETWORK_NAMES.RINKEBY;
default:
return chainId;
}
}();
return {
common: Common.forCustomChain(baseChain, {
chainId: chainId
},
/*
* `ethereumjs-common` requires a hardfork to be defined, so we are
* using the current default for this property. This is also an
* assumption, and this should be made configurable.
*/
HARDFORKS.PETERSBURG)
};
};
/*
* This default export is only here to help us with testing, otherwise
* it wound't be needed
*/
var coreHelpers = {
getChainDefinition: getChainDefinition,
derivationPathSerializer: derivationPathSerializer,
recoverPublicKey: recoverPublicKey,
verifyMessageSignature: verifyMessageSignature,
transactionObjectValidator: transactionObjectValidator,
messageVerificationObjectValidator: messageVerificationObjectValidator,
userInputValidator: userInputValidator,
messageOrDataValidator: messageOrDataValidator
};
export default coreHelpers;