/* This file is part of web3.js. web3.js is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. web3.js is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ /** * @file utils.js * @author Fabian Vogelsteller * @date 2017 */ var BN = require('bn.js'); var numberToBN = require('number-to-bn'); var utf8 = require('utf8'); var ethereumjsUtil = require('ethereumjs-util'); var ethereumBloomFilters = require('ethereum-bloom-filters'); /** * Returns true if object is BN, otherwise false * * @method isBN * @param {Object} object * @return {Boolean} */ var isBN = function (object) { return BN.isBN(object); }; /** * Returns true if object is BigNumber, otherwise false * * @method isBigNumber * @param {Object} object * @return {Boolean} */ var isBigNumber = function (object) { return object && object.constructor && object.constructor.name === 'BigNumber'; }; /** * Takes an input and transforms it into an BN * * @method toBN * @param {Number|String|BN} number, string, HEX string or BN * @return {BN} BN */ var toBN = function (number) { try { return numberToBN.apply(null, arguments); } catch (e) { throw new Error(e + ' Given value: "' + number + '"'); } }; /** * Takes and input transforms it into BN and if it is negative value, into two's complement * * @method toTwosComplement * @param {Number|String|BN} number * @return {String} */ var toTwosComplement = function (number) { return '0x' + toBN(number).toTwos(256).toString(16, 64); }; /** * Checks if the given string is an address * * @method isAddress * @param {String} address the given HEX address * @return {Boolean} */ var isAddress = function (address) { // check if it has the basic requirements of an address if (!/^(0x)?[0-9a-f]{40}$/i.test(address)) { return false; // If it's ALL lowercase or ALL upppercase } else if (/^(0x|0X)?[0-9a-f]{40}$/.test(address) || /^(0x|0X)?[0-9A-F]{40}$/.test(address)) { return true; // Otherwise check each case } else { return checkAddressChecksum(address); } }; /** * Checks if the given string is a checksummed address * * @method checkAddressChecksum * @param {String} address the given HEX address * @return {Boolean} */ var checkAddressChecksum = function (address) { // Check each case address = address.replace(/^0x/i, ''); var addressHash = sha3(address.toLowerCase()).replace(/^0x/i, ''); for (var i = 0; i < 40; i++) { // the nth letter should be uppercase if the nth digit of casemap is 1 if ((parseInt(addressHash[i], 16) > 7 && address[i].toUpperCase() !== address[i]) || (parseInt(addressHash[i], 16) <= 7 && address[i].toLowerCase() !== address[i])) { return false; } } return true; }; /** * Should be called to pad string to expected length * * @method leftPad * @param {String} string to be padded * @param {Number} chars that result string should have * @param {String} sign, by default 0 * @returns {String} right aligned string */ var leftPad = function (string, chars, sign) { var hasPrefix = /^0x/i.test(string) || typeof string === 'number'; string = string.toString(16).replace(/^0x/i, ''); var padding = (chars - string.length + 1 >= 0) ? chars - string.length + 1 : 0; return (hasPrefix ? '0x' : '') + new Array(padding).join(sign ? sign : "0") + string; }; /** * Should be called to pad string to expected length * * @method rightPad * @param {String} string to be padded * @param {Number} chars that result string should have * @param {String} sign, by default 0 * @returns {String} right aligned string */ var rightPad = function (string, chars, sign) { var hasPrefix = /^0x/i.test(string) || typeof string === 'number'; string = string.toString(16).replace(/^0x/i, ''); var padding = (chars - string.length + 1 >= 0) ? chars - string.length + 1 : 0; return (hasPrefix ? '0x' : '') + string + (new Array(padding).join(sign ? sign : "0")); }; /** * Should be called to get hex representation (prefixed by 0x) of utf8 string * * @method utf8ToHex * @param {String} str * @returns {String} hex representation of input string */ var utf8ToHex = function (str) { str = utf8.encode(str); var hex = ""; // remove \u0000 padding from either side str = str.replace(/^(?:\u0000)*/, ''); str = str.split("").reverse().join(""); str = str.replace(/^(?:\u0000)*/, ''); str = str.split("").reverse().join(""); for (var i = 0; i < str.length; i++) { var code = str.charCodeAt(i); // if (code !== 0) { var n = code.toString(16); hex += n.length < 2 ? '0' + n : n; // } } return "0x" + hex; }; /** * Should be called to get utf8 from it's hex representation * * @method hexToUtf8 * @param {String} hex * @returns {String} ascii string representation of hex value */ var hexToUtf8 = function (hex) { if (!isHexStrict(hex)) throw new Error('The parameter "' + hex + '" must be a valid HEX string.'); var str = ""; var code = 0; hex = hex.replace(/^0x/i, ''); // remove 00 padding from either side hex = hex.replace(/^(?:00)*/, ''); hex = hex.split("").reverse().join(""); hex = hex.replace(/^(?:00)*/, ''); hex = hex.split("").reverse().join(""); var l = hex.length; for (var i = 0; i < l; i += 2) { code = parseInt(hex.substr(i, 2), 16); // if (code !== 0) { str += String.fromCharCode(code); // } } return utf8.decode(str); }; /** * Converts value to it's number representation * * @method hexToNumber * @param {String|Number|BN} value * @return {String} */ var hexToNumber = function (value) { if (!value) { return value; } if (typeof value === 'string' && !isHexStrict(value)) { throw new Error('Given value "' + value + '" is not a valid hex string.'); } return toBN(value).toNumber(); }; /** * Converts value to it's decimal representation in string * * @method hexToNumberString * @param {String|Number|BN} value * @return {String} */ var hexToNumberString = function (value) { if (!value) return value; if (typeof value === 'string' && !isHexStrict(value)) { throw new Error('Given value "' + value + '" is not a valid hex string.'); } return toBN(value).toString(10); }; /** * Converts value to it's hex representation * * @method numberToHex * @param {String|Number|BN} value * @return {String} */ var numberToHex = function (value) { if ((value === null || value === undefined)) { return value; } if (!isFinite(value) && !isHexStrict(value)) { throw new Error('Given input "' + value + '" is not a number.'); } var number = toBN(value); var result = number.toString(16); return number.lt(new BN(0)) ? '-0x' + result.substr(1) : '0x' + result; }; /** * Convert a byte array to a hex string * * Note: Implementation from crypto-js * * @method bytesToHex * @param {Array} bytes * @return {String} the hex string */ var bytesToHex = function (bytes) { for (var hex = [], i = 0; i < bytes.length; i++) { /* jshint ignore:start */ hex.push((bytes[i] >>> 4).toString(16)); hex.push((bytes[i] & 0xF).toString(16)); /* jshint ignore:end */ } return '0x' + hex.join(""); }; /** * Convert a hex string to a byte array * * Note: Implementation from crypto-js * * @method hexToBytes * @param {string} hex * @return {Array} the byte array */ var hexToBytes = function (hex) { hex = hex.toString(16); if (!isHexStrict(hex)) { throw new Error('Given value "' + hex + '" is not a valid hex string.'); } hex = hex.replace(/^0x/i, ''); for (var bytes = [], c = 0; c < hex.length; c += 2) bytes.push(parseInt(hex.substr(c, 2), 16)); return bytes; }; /** * Auto converts any given value into it's hex representation. * * And even stringifys objects before. * * @method toHex * @param {String|Number|BN|Object|Buffer} value * @param {Boolean} returnType * @return {String} */ var toHex = function (value, returnType) { /*jshint maxcomplexity: false */ if (isAddress(value)) { return returnType ? 'address' : '0x' + value.toLowerCase().replace(/^0x/i, ''); } if (typeof value === 'boolean') { return returnType ? 'bool' : value ? '0x01' : '0x00'; } if (Buffer.isBuffer(value)) { return '0x' + value.toString('hex'); } if (typeof value === 'object' && !!value && !isBigNumber(value) && !isBN(value)) { return returnType ? 'string' : utf8ToHex(JSON.stringify(value)); } // if its a negative number, pass it through numberToHex if (typeof value === 'string') { if (value.indexOf('-0x') === 0 || value.indexOf('-0X') === 0) { return returnType ? 'int256' : numberToHex(value); } else if (value.indexOf('0x') === 0 || value.indexOf('0X') === 0) { return returnType ? 'bytes' : value; } else if (!isFinite(value)) { return returnType ? 'string' : utf8ToHex(value); } } return returnType ? (value < 0 ? 'int256' : 'uint256') : numberToHex(value); }; /** * Check if string is HEX, requires a 0x in front * * @method isHexStrict * @param {String} hex to be checked * @returns {Boolean} */ var isHexStrict = function (hex) { return ((typeof hex === 'string' || typeof hex === 'number') && /^(-)?0x[0-9a-f]*$/i.test(hex)); }; /** * Check if string is HEX * * @method isHex * @param {String} hex to be checked * @returns {Boolean} */ var isHex = function (hex) { return ((typeof hex === 'string' || typeof hex === 'number') && /^(-0x|0x)?[0-9a-f]*$/i.test(hex)); }; /** * Remove 0x prefix from string * * @method stripHexPrefix * @param {String} str to be checked * @returns {String} */ var stripHexPrefix = function (str) { if (str !== 0 && isHex(str)) return str.replace(/^(-)?0x/i, '$1'); return str; }; /** * Returns true if given string is a valid Ethereum block header bloom. * * @method isBloom * @param {String} bloom encoded bloom filter * @return {Boolean} */ var isBloom = function (bloom) { return ethereumBloomFilters.isBloom(bloom); }; /** * Returns true if the ethereum users address is part of the given bloom * note: false positives are possible. * * @method isUserEthereumAddressInBloom * @param {String} ethereumAddress encoded bloom filter * @param {String} bloom ethereum addresss * @return {Boolean} */ var isUserEthereumAddressInBloom = function (bloom, ethereumAddress) { return ethereumBloomFilters.isUserEthereumAddressInBloom(bloom, ethereumAddress); }; /** * Returns true if the contract address is part of the given bloom * note: false positives are possible. * * @method isUserEthereumAddressInBloom * @param {String} bloom encoded bloom filter * @param {String} contractAddress contract addresss * @return {Boolean} */ var isContractAddressInBloom = function (bloom, contractAddress) { return ethereumBloomFilters.isContractAddressInBloom(bloom, contractAddress); }; /** * Returns true if given string is a valid log topic. * * @method isTopic * @param {String} topic encoded topic * @return {Boolean} */ var isTopic = function (topic) { return ethereumBloomFilters.isTopic(topic); }; /** * Returns true if the topic is part of the given bloom * note: false positives are possible. * * @method isTopicInBloom * @param {String} bloom encoded bloom filter * @param {String} topic encoded topic * @return {Boolean} */ var isTopicInBloom = function (bloom, topic) { return ethereumBloomFilters.isTopicInBloom(bloom, topic); }; /** * Returns true if the value is part of the given bloom * note: false positives are possible. * * @method isInBloom * @param {String} bloom encoded bloom filter * @param {String | Uint8Array} topic encoded value * @return {Boolean} */ var isInBloom = function (bloom, topic) { return ethereumBloomFilters.isInBloom(bloom, topic); }; /** * Hashes values to a sha3 hash using keccak 256 * * To hash a HEX string the hex must have 0x in front. * * @method sha3 * @return {String} the sha3 string */ var SHA3_NULL_S = '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470'; var sha3 = function (value) { if (isBN(value)) { value = value.toString(); } if (isHexStrict(value) && /^0x/i.test((value).toString())) { value = ethereumjsUtil.toBuffer(value); } else if (typeof value === 'string') { // Assume value is an arbitrary string value = Buffer.from(value, 'utf-8'); } var returnValue = ethereumjsUtil.bufferToHex(ethereumjsUtil.keccak256(value)); if (returnValue === SHA3_NULL_S) { return null; } else { return returnValue; } }; // expose the under the hood keccak256 sha3._Hash = ethereumjsUtil.keccak256; /** * @method sha3Raw * * @param value * * @returns {string} */ var sha3Raw = function (value) { value = sha3(value); if (value === null) { return SHA3_NULL_S; } return value; }; /** * Auto converts any given value into it's hex representation, * then converts hex to number. * * @method toNumber * @param {String|Number|BN} value * @return {Number} */ var toNumber = function (value) { return typeof value === 'number' ? value : hexToNumber(toHex(value)); }; module.exports = { BN: BN, isBN: isBN, isBigNumber: isBigNumber, toBN: toBN, isAddress: isAddress, isBloom: isBloom, isUserEthereumAddressInBloom: isUserEthereumAddressInBloom, isContractAddressInBloom: isContractAddressInBloom, isTopic: isTopic, isTopicInBloom: isTopicInBloom, isInBloom: isInBloom, checkAddressChecksum: checkAddressChecksum, utf8ToHex: utf8ToHex, hexToUtf8: hexToUtf8, hexToNumber: hexToNumber, hexToNumberString: hexToNumberString, numberToHex: numberToHex, toHex: toHex, hexToBytes: hexToBytes, bytesToHex: bytesToHex, isHex: isHex, isHexStrict: isHexStrict, stripHexPrefix: stripHexPrefix, leftPad: leftPad, rightPad: rightPad, toTwosComplement: toTwosComplement, sha3: sha3, sha3Raw: sha3Raw, toNumber: toNumber };