UNPKG

@colony/purser-core

Version:

A collection of helpers, utils, validators and normalizers to assist the individual purser modules

494 lines (479 loc) 15.2 kB
/* @flow */ 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'; import type { DerivationPathObjectType, TransactionObjectType, MessageVerificationObjectType, } from './flowtypes'; /** * 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 const derivationPathSerializer = ({ purpose = PATH.PURPOSE, coinType = PATH.COIN_MAINNET, account = PATH.ACCOUNT, change, addressIndex, }: DerivationPathObjectType = {}): string => { const { DELIMITER } = PATH; const hasChange = change || change === 0; const 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 */ `${PATH.HEADER_KEY}/${purpose}` + `${DELIMITER}${coinType}` + `${DELIMITER}${account}` + `${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 */ `${hasChange ? change : ''}` + /* $FlowFixMe */ (hasChange && hasAddressIndex ? `/${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 const recoverPublicKey = ({ message, signature, }: MessageVerificationObjectType = {}): string => { const { verifyMessageSignature: messages } = helperMessages; const 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 */ const recoveryParam = recoveryParamNormalizer(signatureBuffer[64]); const rComponent = signatureBuffer.slice(0, 32); const sComponent = signatureBuffer.slice(32, 64); const 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 */ const 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 const verifyMessageSignature = ({ publicKey, message, signature, }: Object): boolean => { const { verifyMessageSignature: messages } = helperMessages; try { /* * Normalize the recovered public key by removing the `0x` preifx */ const recoveredPublicKey: string = 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, signature }), false, ); /* * Remove the prefix (0x) and the header (first two bits) from the public key we * want to test against */ const normalizedPublicKey: string = 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(`${messages.somethingWentWrong}. Error: ${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 const transactionObjectValidator = ({ gasPrice = bigNumber(TRANSACTION.GAS_PRICE), gasLimit = bigNumber(TRANSACTION.GAS_LIMIT), chainId = TRANSACTION.CHAIN_ID, nonce = TRANSACTION.NONCE, to, value = bigNumber(TRANSACTION.VALUE), inputData = TRANSACTION.INPUT_DATA, }: TransactionObjectType = {}): TransactionObjectType => { /* * 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, gasLimit, chainId, nonce, to, value, 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 const messageVerificationObjectValidator = ({ /* * For the Ledger wallet implementation we can't pass in an empty string, so * we try with the next best thing. */ message, signature, }: MessageVerificationObjectType = {}): MessageVerificationObjectType => { /* * Check if the messages is in the correct format */ messageValidator(message); /* * Check if the signature is in the correct format */ hexSequenceValidator(signature); return { 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 const userInputValidator = ({ firstArgument = {}, requiredEither = [], requiredAll = [], requiredOr = [], }: { firstArgument: Object, requiredAll?: Array<String>, requiredEither?: Array<String>, requiredOr?: Array<String>, } = {}): void => { const { userInputValidator: messages } = helperMessages; /* * 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) { const availableProps: Array<boolean> = requiredEither.map(propName => Object.prototype.hasOwnProperty.call(firstArgument, propName), ); if (!availableProps.some(propExists => propExists === true)) { /* * Explain the arguments format (if we're in dev mode), then throw the Error */ warning(messages.argumentsFormatExplanation); throw new Error( `${messages.notSomeProps}: { '${requiredEither.join(`', '`)}' }`, ); } } /* * Check if all required props are present. * Fail after the first one missing. */ requiredAll.map(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( `${messages.notAllProps}: { '${requiredAll.join(`', '`)}' }`, ); } return propName; }); /* * Check if exactly one of the required props is present. * Fail if multiple are present. */ if ( requiredOr.length && requiredOr.reduce( (acc, propName) => Object.prototype.hasOwnProperty.call(firstArgument, propName) ? acc + 1 : acc, 0, ) !== 1 ) { warning(messages.argumentsFormatExplanation); throw new Error(); } }; export const messageOrDataValidator = ( { message, messageData }: { message: any, messageData: any } ): string | Uint8Array => { 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 const getChainDefinition = (chainId: number): {| common: Object |} => { const baseChain = (() => { 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 }, /* * `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 */ const coreHelpers: Object = { getChainDefinition, derivationPathSerializer, recoverPublicKey, verifyMessageSignature, transactionObjectValidator, messageVerificationObjectValidator, userInputValidator, messageOrDataValidator, }; export default coreHelpers;