UNPKG

caver-js

Version:

caver-js is a JavaScript API library that allows developers to interact with a Kaia node

1,500 lines (1,340 loc) 51.7 kB
/* Modifications copyright 2018 The caver-js Authors 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 <http://www.gnu.org/licenses/>. This file is derived from web3.js/packages/web3-utils/src/utils.js (2019/06/12). Modified and improved for the caver-js development. */ /** * @file utils.js * @author Fabian Vogelsteller <fabian@ethereum.org> * @date 2017 */ const _ = require('lodash') const BN = require('bn.js') const BigNumber = require('bignumber.js') const numberToBN = require('number-to-bn') const utf8 = require('utf8') const Hash = require('eth-lib/lib/hash') const RLP = require('eth-lib/lib/rlp') const Account = require('eth-lib/lib/account') const elliptic = require('elliptic') const secp256k1 = new elliptic.ec('secp256k1') const txTypeToString = { '0x20': 'ACCOUNT_UPDATE', '0x21': 'FEE_DELEGATED_ACCOUNT_UPDATE', '0x22': 'FEE_DELEGATED_ACCOUNT_UPDATE_WITH_RATIO', '0x08': 'VALUE_TRANSFER', '0x10': 'VALUE_TRANSFER_MEMO', '0x09': 'FEE_DELEGATED_VALUE_TRANSFER', '0x0a': 'FEE_DELEGATED_VALUE_TRANSFER_WITH_RATIO', '0x11': 'FEE_DELEGATED_VALUE_TRANSFER_MEMO', '0x12': 'FEE_DELEGATED_VALUE_TRANSFER_MEMO_WITH_RATIO', '0x28': 'SMART_CONTRACT_DEPLOY', '0x29': 'FEE_DELEGATED_SMART_CONTRACT_DEPLOY', '0x2a': 'FEE_DELEGATED_SMART_CONTRACT_DEPLOY_WITH_RATIO', '0x30': 'SMART_CONTRACT_EXECUTION', '0x31': 'FEE_DELEGATED_SMART_CONTRACT_EXECUTION', '0x32': 'FEE_DELEGATED_SMART_CONTRACT_EXECUTION_WITH_RATIO', '0x38': 'CANCEL', '0x39': 'FEE_DELEGATED_CANCEL', '0x3a': 'FEE_DELEGATED_CANCEL_WITH_RATIO', '0x48': 'CHAIN_DATA_ANCHORING', } const HASH_LENGTH = 66 /** * Returns `true` if parameter is a BN instance, otherwise `false`. * * @example * const bn = new caver.utils.BN(10) * const result = caver.utils.isBN(bn) * * @memberof module:utils * @inner * * @param {*} bn * @return {boolean} `true` if a given value is a `BN.js` instance. */ const isBN = function(bn) { return BN.isBN(bn) } /** * Returns `true` if object is a BigNumber instance, otherwise `false`. * * @example * const bigNumber = new caver.utils.BigNumber(10) * const result = caver.utils.isBigNumber(bigNumber) * * @memberof module:utils * @inner * * @param {*} bigNumber * @return {boolean} `true` if a given value is a `Bignumber.js` instance. */ const isBigNumber = function(bigNumber) { return BigNumber.isBigNumber(bigNumber) } /** * Safely converts any given value (including `Bignumber.js` instances) into a `BN.js` instance, for handling big numbers in JavaScript. * * @example * const result = caver.utils.toBN(num) * * @memberof module:utils * @inner * * @param {number|string|BN|BigNumber} number The number to convert to a BN.js instance. * @return {BN} The {@link https://github.com/indutny/bn.js/|BN.js} instance. */ function toBN(number) { try { return numberToBN.apply(null, arguments) } catch (e) { throw new Error(`${e} Given value: "${number}"`) } } /** * Converts a negative number into a two's complement. * * @example * const result = caver.utils.toTwosComplement(num) * * @memberof module:utils * @inner * * @param {number|string|BN|BigNumber} number The number to convert. * @return {string} The converted hex string. */ const toTwosComplement = function(number) { return `0x${toBN(number) .toTwos(256) .toString(16, 64)}` } /** * Checks if a given string is a valid Klaytn address. * It will also check the checksum if the address has upper and lowercase letters. * * @example * const result = caver.utils.isAddress('0x{address in hex}') * * @memberof module:utils * @inner * * @param {string} address An address string. * @return {boolean} `true` if a given string is a valid Klaytn address. */ const 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 } if (/^(0x|0X)?[0-9a-f]{40}$/.test(address) || /^(0x|0X)?[0-9A-F]{40}$/.test(address)) { return true // Otherwise check each case } return checkAddressChecksum(address) } /** * Checks the checksum of a given address. * Will also return `false` on non-checksum addresses. * * @example * const result = caver.utils.checkAddressChecksum('0x{address in hex}') * * @memberof module:utils * @inner * * @param {string} address An address string. * @return {boolean} */ const checkAddressChecksum = function(address) { // Check each case address = address.replace(/^0x/i, '') const addressHash = sha3(address.toLowerCase()).replace(/^0x/i, '') for (let 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 } /** * Adds padding on the left of a string. Useful for adding paddings to HEX strings. * * @example * const result = caver.utils.padLeft('0x3456ff', 20) // '0x000000000000003456ff' * const result = caver.utils.padLeft('Hello', 20, 'x') // 'xxxxxxxxxxxxxxxHello' * * @memberof module:utils * @inner * @alias padLeft * * @param {string} string The string to add padding on the left. * @param {number} chars The number of characters the total string should have. * @param {string} [sign] The character sign to use, defaults to `0`. * @returns {string} The padded string. */ const leftPad = function(string, chars, sign) { const hasPrefix = /^0x/i.test(string) || typeof string === 'number' string = string.toString(16).replace(/^0x/i, '') const padding = chars - string.length + 1 >= 0 ? chars - string.length + 1 : 0 return (hasPrefix ? '0x' : '') + new Array(padding).join(sign || '0') + string } /** * Adds padding on the right of a string, Useful for adding paddings to HEX strings. * * @example * const result = caver.utils.rightPad('0x3456ff', 20) // '0x3456ff00000000000000' * const result = caver.utils.rightPad('Hello', 20, 'x') // 'Helloxxxxxxxxxxxxxxx' * * @memberof module:utils * @inner * @alias padRight * * @param {string} string The string to add padding on the right. * @param {number} chars The number of characters the total string should have. * @param {string} [sign] The character sign to use, defaults to `0`. * @returns {string} The padded string. */ const rightPad = function(string, chars, sign) { const hasPrefix = /^0x/i.test(string) || typeof string === 'number' string = string.toString(16).replace(/^0x/i, '') const padding = chars - string.length + 1 >= 0 ? chars - string.length + 1 : 0 return (hasPrefix ? '0x' : '') + string + new Array(padding).join(sign || '0') } /** * Returns the HEX representation of a given UTF-8 string. * * @example * const result = caver.utils.utf8ToHex('I have 100€') // '0x49206861766520313030e282ac' * * @memberof module:utils * @inner * * @param {string} str A UTF-8 string to convert to a HEX string. * @returns {string} The HEX string. */ const utf8ToHex = function(str) { str = utf8.encode(str) let 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 (let i = 0; i < str.length; i++) { const code = str.charCodeAt(i) // if (code !== 0) { const n = code.toString(16) hex += n.length < 2 ? `0${n}` : n // } } return `0x${hex}` } /** * Returns the UTF-8 string representation of a given HEX value. * * @example * const result = caver.utils.hexToUtf8('0x49206861766520313030e282ac') // 'I have 100€' * * @memberof module:utils * @inner * * @param {string} hex A HEX string to convert to a UTF-8 string. * @returns {string} The UTF-8 string. */ const hexToUtf8 = function(hex) { if (!isHexStrict(hex)) { throw new Error(`The parameter "${hex}" must be a valid HEX string.`) } let str = '' let 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('') const l = hex.length for (let i = 0; i < l; i += 2) { code = parseInt(hex.substr(i, 2), 16) // if (code !== 0) { str += String.fromCharCode(code) // } } return utf8.decode(str) } /** * Returns the number representation of a given HEX value. * Please note that this function is not useful for big numbers, rather use `caver.utils.toBN`. * * @example * const result = caver.utils.hexToNumber('0xea') // 234 * * @memberof module:utils * @inner * * @param {string} A HEX string to be converted. * @return {number} The number representation of a given HEX value. */ const 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() } /** * Returns the number representation of a given HEX value as a string. * * @example * const result = caver.utils.hexToNumberString('0xea') // '234' * * @memberof module:utils * @inner * * @param {string} A HEX string to be converted. * @return {string} The number as a string. */ const hexToNumberString = function(value) { if (!value) return value if (_.isString(value) && !isHexStrict(value)) { throw new Error(`Given value "${value}" is not a valid hex string.`) } return toBN(value).toString(10) } /** * Returns the HEX representation of a given number value. * * @example * const result = caver.utils.numberToHex(234) // '0xea' * const result = caver.utils.numberToHex('234') * const result = caver.utils.numberToHex(new caver.utils.BN(234)) * const result = caver.utils.numberToHex(new caver.utils.BigNumber(234)) * * @memberof module:utils * @inner * * @param {string|number|BN|BigNumber} value A number as string or number. * @return {string} The HEX value of the given number. */ const numberToHex = function(value) { if (_.isNumber(value)) { const bn = toBN(value) try { bn.toNumber() } catch (e) { throw new Error(`${e.message}: Number type cannot handle big number. Please use hex string or BigNumber/BN.`) } } if (_.isNull(value) || _.isUndefined(value)) { return value } if (!isFinite(value) && !isHexStrict(value)) { throw new Error(`Given input "${value}" is not a number.`) } const number = toBN(value) const result = number.toString(16) return number.lt(new BN(0)) ? `-0x${result.substr(1)}` : `0x${result}` } /** * Returns a HEX string from a byte array. * * @example * const result = caver.utils.bytesToHex([ 72, 101, 108, 108, 111, 33, 36 ]) // '0x48656c6c6f2124' * * @memberof module:utils * @inner * * @param {Array} bytes A byte array to convert. * @return {string} The HEX string. */ const bytesToHex = function(bytes) { const hex = [] for (let i = 0; i < bytes.length; i++) { // eslint-disable-next-line no-bitwise hex.push((bytes[i] >>> 4).toString(16)) // eslint-disable-next-line no-bitwise hex.push((bytes[i] & 0xf).toString(16)) } return `0x${hex.join('')}` } /** * Returns a byte array from the given HEX string. * * @example * const result = caver.utils.hexToBytes('0x000000ea') // [ 0, 0, 0, 234 ] * * @memberof module:utils * @inner * * @param {string} hex A HEX string to be converted. * @return {Array.<number>} The byte array. */ const 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, '') const bytes = [] for (let c = 0; c < hex.length; c += 2) { bytes.push(parseInt(hex.substr(c, 2), 16)) } return bytes } /** * Converts any given value to HEX. * The numeric strings will be interpreted as numbers. * Text strings will be interpreted as UTF-8 strings. * * @example * const result = caver.utils.toHex('234') // '0xea' * const result = caver.utils.toHex(234) // '0xea' * const result = caver.utils.toHex(new caver.utils.BN('234')) // '0xea' * const result = caver.utils.toHex(new caver.utils.Bignumber('234')) // '0xea' * const result = caver.utils.toHex('I have 100€') // '0x49206861766520313030e282ac' * * @memberof module:utils * @inner * * @param {string|number|BN|BigNumber|Buffer} value The input to convert to HEX. * @return {string} The resulting HEX string. */ /* eslint-disable complexity */ const toHex = function(value, returnType) { if (Buffer.isBuffer(value)) { return returnType ? 'buffer' : bufferToHex(value) } if (isAddress(value)) { return returnType ? 'address' : `0x${value.toLowerCase().replace(/^0x/i, '')}` } if (_.isBoolean(value)) { return returnType ? 'bool' : value ? '0x01' : '0x00' } if (_.isObject(value) && !isBigNumber(value) && !isBN(value)) { return returnType ? 'string' : utf8ToHex(JSON.stringify(value)) } // if its a negative number, pass it through numberToHex if (_.isString(value)) { if (value.indexOf('-0x') === 0 || value.indexOf('-0X') === 0) { return returnType ? 'int256' : numberToHex(value) } if (value.indexOf('0x') === 0 || value.indexOf('0X') === 0) { return returnType ? 'bytes' : value } if (!isFinite(value)) { return returnType ? 'string' : utf8ToHex(value) } } return returnType ? (value < 0 ? 'int256' : 'uint256') : numberToHex(value) } /* eslint-enable complexity */ /** * Converts buffer to 0x-prefixed hex string. * * @example * const result = caver.utils.bufferToHex(Buffer.from('5b9ac8', 'hex')) // '0x5b9ac8' * * @memberof module:utils * @inner * * @param {Buffer} buf A buffer to convert to hex string. * @return {string} The 0x-prefixed hex string. */ const bufferToHex = function(buf) { buf = toBuffer(buf) return `0x${buf.toString('hex')}` } /** * This function converts the input to a Buffer. * To convert an object into a Buffer using `caver.utils.toBuffer`, the object must implement `toArray` function. * For string type input, this function only works with a 0x-prefixed hex string. * * @example * const result = caver.utils.toBuffer(Buffer.alloc(0)) * const result = caver.utils.toBuffer('0x1234') * const result = caver.utils.toBuffer(1) * const result = caver.utils.toBuffer([1,2,3]) * const result = caver.utils.toBuffer(new caver.utils.BN(255)) * const result = caver.utils.toBuffer(new caver.utils.BigNumber(255)) * const result = caver.utils.toBuffer({toArray: function() {return [1,2,3,4]}}) // An object that implements `toArray` function * const result = caver.utils.toBuffer(null) * const result = caver.utils.toBuffer(undefined) * * @memberof module:utils * @inner * * @param {Buffer|Array.<number>|string|number|BN|BigNumber|object} input The value to be converted to a Buffer. * @return {Buffer} The value converted to Buffer type is returned. */ const toBuffer = function(input) { if (Buffer.isBuffer(input)) return input if (input === null || input === undefined) return Buffer.alloc(0) if (Array.isArray(input)) return Buffer.from(input) if (isBigNumber(input)) input = toBN(input) if (isBN(input)) return input.toArrayLike(Buffer) if (_.isObject(input)) { if (input.toArray && _.isFunction(input.toArray)) return Buffer.from(input.toArray()) throw new Error('To convert an object to a buffer, the toArray function must be implemented inside the object') } switch (typeof input) { case 'string': if (isHexStrict(input)) return Buffer.from(makeEven(input).replace('0x', ''), 'hex') throw new Error("Failed to convert string to Buffer. 'toBuffer' function only supports 0x-prefixed hex string") case 'number': return numberToBuffer(input) } throw new Error(`Not supported type with ${input}`) } /** * This function converts a number to a Buffer. * The {@link module:utils~toBuffer|caver.utils.toBuffer} has the same behavior as this function when the input is a number. * * @example * const result = caver.utils.numberToBuffer(1) * const result = caver.utils.numberToBuffer('2') * const result = caver.utils.numberToBuffer('0x3') * const result = caver.utils.numberToBuffer(new caver.utils.BN(4)) * const result = caver.utils.numberToBuffer(new caver.utils.BigNumber(4)) * * @memberof module:utils * @inner * * @param {number|string|BN|BigNumber} num A number to be converted to a Buffer. * @return {Buffer} */ const numberToBuffer = function(num) { return Buffer.from(makeEven(numberToHex(num)).replace('0x', ''), 'hex') } /** * Checks if a given string is a HEX string. * Difference to {@link module:utils~isHex|caver.utils.isHex} is that it expects HEX to be prefixed with `0x`. * * @example * const result = caver.utils.isHexStrict('0xc1912') // true * const result = caver.utils.isHexStrict('c1912') // false * const result = caver.utils.isHexStrict('Hello') // false * * @memberof module:utils * @inner * * @param {string} hex The given HEX string. * @returns {boolean} `true` if a given string is a HEX string. */ const isHexStrict = function(hex) { return (_.isString(hex) || _.isNumber(hex)) && /^(-)?0x[0-9a-f]*$/i.test(hex) } /** * Checks if a given string is a HEX string. * * @example * const result = caver.utils.isHex('0xc1912') // true * const result = caver.utils.isHex('c1912') // true * const result = caver.utils.isHex('Hello') // false * * @memberof module:utils * @inner * * @param {string} hex The given HEX string. * @returns {boolean} `true` if a given parameter is a HEX string. */ const isHex = function(hex) { return (_.isString(hex) || _.isNumber(hex)) && /^(-0x|0x)?[0-9a-f]*$/i.test(hex) } /** * Checks if the given string is a hexadecimal transaction hash with or without prefix 0x * @deprecated since version v1.5.0 * @ignore * @method isTxHash * @param {String} txHash given hexadecimal transaction hash * @return {Boolean} */ const isTxHash = txHash => isValidHash(txHash) /** * Returns `true` if the input is in 32-bytes hash format, otherwise it returns `false`. * * @example * const result = caver.utils.isValidHash('0xe9a11d9ef95fb437f75d07ce768d43e74f158dd54b106e7d3746ce29d545b550') // true * const result = caver.utils.isValidHash('e9a11d9ef95fb437f75d07ce768d43e74f158dd54b106e7d3746ce29d545b550') // true * const result = caver.utils.isValidHash('0x1') // false * * @memberof module:utils * @inner * * @param {string} hash The value to be examined that if it is in 32-bytes hash format or not. * @return {boolean} `true` means the input is in the format of 32-bytes hash. */ const isValidHash = hash => new RegExp(`^(0x|0X)?[0-9a-fA-F]{${HASH_LENGTH - 2}}$`).test(hash) /** * Checks if the given string is a hexadecimal transaction hash that starts with 0x * @deprecated since version v1.5.0 * @ignore * @method isTxHashStrict * @param {String} txHash given hexadecimal transaction hash * @return {Boolean} */ const isTxHashStrict = txHash => isValidHashStrict(txHash) /** * Returns `true` if the input is in 0x-prefixed 32-bytes hash format, otherwise it returns `false`. * This function only looks at the input and determines if it is in the format of 0x-prefixed 32-bytes hash. * Difference to {@link module:utils~isValidHash|caver.utils.isValidHash} is that it expects HEX to be prefixed with 0x. * * @example * const result = caver.utils.isValidHashStrict('0xe9a11d9ef95fb437f75d07ce768d43e74f158dd54b106e7d3746ce29d545b550') // true * const result = caver.utils.isValidHashStrict('e9a11d9ef95fb437f75d07ce768d43e74f158dd54b106e7d3746ce29d545b550') // false * const result = caver.utils.isValidHashStrict('0x1') // false * * @memberof module:utils * @inner * * @param {string} hash The value to be examined that if it is in the format of 0x-prefixed 32-bytes hash or not. * @return {boolean} `true` means the input is in the format of 0x-prefixed 32-bytes hash. */ const isValidHashStrict = hash => new RegExp(`^(0x|0X)[0-9a-fA-F]{${HASH_LENGTH - 2}}$`).test(hash) /** * Returns `true` if the bloom is a valid bloom. * * @example * const result = caver.utils.isBloom('0x00000...') * * @memberof module:utils * @inner * * @param {string} bloom An encoded bloom filter. * @return {boolean} `true` means the input bloom parameter is valid. */ const isBloom = function(bloom) { if (!/^(0x)?[0-9a-f]{512}$/i.test(bloom)) { return false } if (/^(0x)?[0-9a-f]{512}$/.test(bloom) || /^(0x)?[0-9A-F]{512}$/.test(bloom)) { return true } return false } /** * Returns `true` if the topic is valid. * * @example * const result = caver.utils.isBloom('0x00000...') * * @memberof module:utils * @inner * * @param {string} hex An encoded topic. * @return {boolean} */ const isTopic = function(topic) { if (!/^(0x)?[0-9a-f]{64}$/i.test(topic)) { return false } if (/^(0x)?[0-9a-f]{64}$/.test(topic) || /^(0x)?[0-9A-F]{64}$/.test(topic)) { return true } return false } const parsePredefinedBlockNumber = blockNumber => { switch (blockNumber) { case 'genesis': case 'earliest': return '0x0' default: return blockNumber } } /** * Returns `true` if the parameter is predefined block tag. * * @example * const result = caver.utils.isPredefinedBlockNumber('latest') // true * * @memberof module:utils * @inner * * @param {string} predefinedBlock The predefined block. * @return {boolean} `true` means predefinedBlock is valid predefined block tag. */ const isPredefinedBlockNumber = function(predefinedBlock) { return predefinedBlock === 'latest' || predefinedBlock === 'pending' || predefinedBlock === 'earliest' || predefinedBlock === 'genesis' } /** * Validtes block number (or block tag string). * * The block number should be one of a type below: * 1) predefined block number ex:) 'latest', 'earliest', 'pending', 'genesis' * 2) hex * 3) finite number * * @example * const result = caver.utils.isValidBlockNumberCandidate('latest') // true * const result = caver.utils.isValidBlockNumberCandidate('0x1') // true * const result = caver.utils.isValidBlockNumberCandidate('1') // true * const result = caver.utils.isValidBlockNumberCandidate(1) // true * * @memberof module:utils * @inner * * @param {string|number} blockNumber The block number to validate. This can be block number in number type or block tag(`latest`, `pending`, `earliest`, `genesis`) string. * @return {boolean} `true` means blockNumber is valid. */ const isValidBlockNumberCandidate = blockNumber => { return isPredefinedBlockNumber(blockNumber) || isHexStrict(blockNumber) || Number.isFinite(Number(blockNumber)) } /** * Hashes values to a sha3 hash using keccak 256 * * To hash a HEX string the hex must have 0x in front. * * @return {String} the sha3 string */ const SHA3_NULL_S = '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470' /** * Calculates the sha3 of the input. * * @example * const hash = caver.utils.sha3('234') * * @memberof module:utils * @inner * * @param {string} str - A string to hash. * @return {string} The result hash. */ const sha3 = function(value) { // return null when value is not string type. if (typeof value === 'number') return null if (isHexStrict(value) && /^0x/i.test(value.toString())) { value = hexToBytes(value) } if (isBN(value)) { value = value.toString(10) } const returnValue = Hash.keccak256(value) if (returnValue === SHA3_NULL_S) { return null } return returnValue } // expose the under the hood keccak256 sha3._Hash = Hash /** * An object defines the AccountKeyLegacy. * * @example * { privateKey: '0x{private key}', address: '0x{address in hex}', type: '0x00' } * * @typedef {object} module:utils.ParsedPrivateKey * @property {string} privateKey - The private key string. * @property {string} address - The address string. * @property {string} type - The type string. Currently only `0x00` is supported. */ /** * Parses private key string to { privateKey, address, type }. * * @example * const { privateKey, address, type } = caver.utils.parsePrivateKey('0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8') * const { privateKey, address, type } = caver.utils.parsePrivateKey('0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d80x000xa94f5374fce5edbc8e2a8697c15331677e6ebf0b') * * @memberof module:utils * @inner * * @param {string} privateKey - A private key or KlaytnWalletKey string to parse. * @return {module:utils.ParsedPrivateKey} A parsed private key object. */ function parsePrivateKey(privateKey) { if (typeof privateKey !== 'string') throw new Error('The private key must be of type string') const has0xPrefix = privateKey.slice(0, 2) === '0x' privateKey = has0xPrefix ? privateKey.slice(2) : privateKey if (privateKey.length !== 110 && privateKey.length !== 64) { throw new Error(`Invalid private key(${privateKey})`) } const parsedPrivateKey = privateKey.slice(0, 64) if (!isHex(parsedPrivateKey)) { throw new Error('Invalid private key format : privateKey must be in hex format.') } if (privateKey.length !== 110) { return { privateKey: `0x${privateKey}`, address: '', type: '', } } const type = privateKey.slice(66, 68) if (type !== '00') throw new Error('Invalid type: Currently only type `0x00` is supported.') if (!isKlaytnWalletKey(privateKey)) throw new Error(`Invalid KlaytnWalletKey format.`) const parsedAddress = privateKey.slice(68) return { privateKey: `0x${parsedPrivateKey}`, address: parsedAddress, type: `0x${type}`, } } /** * Parses KlatynWalletKey to [ '0x{privateKey}', '0x{type}', '0x{address}' ]. * * @example * const parsed = caver.utils.parseKlaytnWalletKey('0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d80x000xa94f5374fce5edbc8e2a8697c15331677e6ebf0b') * * @memberof module:utils * @inner * * @param {string} key - A KlaytnWalletKey string to parse. * @return {Array.<string>} An array that includes parsed KlaytnWalletKey. */ function parseKlaytnWalletKey(key) { if (!isKlaytnWalletKey(key)) throw new Error(`Invalid KlaytnWalletKey format: ${key}`) const klaytnWalletKey = key.startsWith('0x') ? key.slice(2) : key const splitted = klaytnWalletKey.split('0x') return [`0x${splitted[0]}`, `0x${splitted[1]}`, `0x${splitted[2]}`] } /** * Validate a KlaytnWalletKey string. * * @example * const result = caver.utils.isKlaytnWalletKey('0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d80x000xa94f5374fce5edbc8e2a8697c15331677e6ebf0b') * * @memberof module:utils * @inner * * @param {string} privateKey - A KlaytnWalletKey string to validate. * @return {boolean} `true` means valid KlaytnWalletKey. */ const isKlaytnWalletKey = privateKey => { if (!_.isString(privateKey)) return false const has0xPrefix = privateKey.slice(0, 2) === '0x' privateKey = has0xPrefix ? privateKey.slice(2) : privateKey if (privateKey.length !== 110) { return false } const splited = privateKey.split('0x') if (splited.length !== 3) return false for (let i = 0; i < splited.length; i++) { if (!isHex(splited[i])) return false switch (i) { case 0: if (splited[i].length !== 64 || !isValidPrivateKey(splited[i])) return false break case 1: if (splited[i].length !== 2 || splited[i] !== '00') return false break case 2: if (splited[i].length !== 40 || !isAddress(splited[i])) return false break } } return true } /** * Validate a private key string. * * @example * const result = caver.utils.isValidPrivateKey('0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8') * * @memberof module:utils * @inner * * @param {string} privateKey - A private key string to validate. * @return {boolean} `true` means valid private key. */ function isValidPrivateKey(privateKey) { if (typeof privateKey !== 'string') return false const has0xPrefix = privateKey.slice(0, 2) === '0x' privateKey = has0xPrefix ? privateKey.slice(2) : privateKey // Private key validation 1: private key should be string and minimum length of it is 64. if (privateKey.length !== 64 || !isHex(privateKey)) return false // order n value in secp256k1. privateKey should be less than order n value. const VALID_PRIVATE_KEY_LIMIT = 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141' const VALID_PRIVATE_LOWER_BOUND = '0000000000000000000000000000000000000000000000000000000000000000' return VALID_PRIVATE_LOWER_BOUND < privateKey.toUpperCase() && privateKey.toUpperCase() < VALID_PRIVATE_KEY_LIMIT } // Check is 1)Number string or 2)Hex string or 3)Number. function isValidNSHSN(value) { switch (typeof value) { case 'number': if (value < 0) { return false } break case 'string': if (Number(value) != value && !isHexStrict(value)) { return false } break default: return false } return true } const rlpEncode = data => RLP.encode(data) const rlpDecode = encodedData => RLP.decode(encodedData) /** * Converts from public key to x, y points. * * @example * const result = caver.utils.xyPointFromPublicKey('0x04019b186993b620455077b6bc37bf61666725d8d87ab33eb113ac0414cd48d78ff46e5ea48c6f22e8f19a77e5dbba9d209df60cbcb841b7e3e81fe444ba829831') * * @memberof module:utils * @inner * * @param {string} publicKey - A public key string. * @return {Array.<string>} An array with x, y points. */ const xyPointFromPublicKey = pub => { let publicKey = pub if (isCompressedPublicKey(publicKey)) publicKey = decompressPublicKey(pub) publicKey = publicKey.replace('0x', '') if (publicKey.length === 130 && publicKey.slice(0, 2) === '04') publicKey = publicKey.slice(2) if (publicKey.length !== 128) throw Error('Invalid public key') // + 2 means '0x' const pubX = `0x${publicKey.slice(0, 64).replace(/^0+/, '')}` const pubY = `0x${publicKey.slice(64).replace(/^0+/, '')}` return [pubX, pubY] } /** * Trims leading zero from 0x-prefixed hex string. * * @example * const result = caver.utils.trimLeadingZero('0x0000011') // '0x11' * * @memberof module:utils * @inner * * @param {string} hex - A hex string to trim. * @return {string} A hex string without leading zero. */ const trimLeadingZero = function(hex) { while (hex && hex.startsWith('0x0')) { hex = `0x${hex.slice(3)}` } return hex } /** * Returns a string to an even length. * * @example * const result = caver.utils.makeEven('0x011') // '0x0011' * * @memberof module:utils * @inner * * @param {string} hex - A hex string to make even. * @return {string} A string with even length. */ const makeEven = function(hex) { if (hex.length % 2 === 1) { hex = hex.replace('0x', '0x0') } return hex } /** * Converts the signature to an array format. * * @example * const result = caver.utils.resolveSignature({ v: '0x0fe9', r: '0x02aca...', s: '0x20502...' }) * const result = caver.utils.resolveSignature({ V: '0x0fe9', R: '0x02aca...', S: '0x20502...' }) * const result = caver.utils.resolveSignature('0x7e85aaff6a6ef0730308af49f6b512741e61f958a21df387a0d0e8973fb40ca0307a8b87f6ac249f7218b4ee1a1d2f7d764ec2d20d9824e7b7b842dd214f139c7f6') * * @ignore * @param {string|object|Array.<string>|SignatureData} signature A signature string, object or array. * @return {Array.<string>} A signature array. */ const resolveSignature = signature => { if (_.isArray(signature)) { const [v, r, s] = signature return [v, r, s] } if (_.isObject(signature)) { const v = signature.V || signature.v const r = signature.R || signature.r const s = signature.S || signature.s if (!v || !r || !s) throw new Error('v, r, s fields should exist in signature') return [v, r, s] } if (_.isString(signature)) { const v = `0x${signature.slice(64 * 2 + 2)}` const decoded = Account.decodeSignature(signature) return [v, decoded[1], decoded[2]] } } /** * Converts the signature to an `{ V, R, S }` format. * Klaytn Node uses `{ V, R, S }` format, so you can use this function to convert caver signature format to `{ V, R, S }`. * * @example * const result = caver.utils.transformSignaturesToObject([ * '0x7f6', * '0x7e85aaff6a6ef0730308af49f6b512741e61f958a21df387a0d0e8973fb40ca0', * '0x307a8b87f6ac249f7218b4ee1a1d2f7d764ec2d20d9824e7b7b842dd214f139c' * ]) * * @ignore * @param {string|object|Array.<string>|SignatureData} signature A signature string, object or array. * @return {Klay.SignatureData} A signature object. */ const transformSignaturesToObject = signatures => { let isSingular = false if (!signatures) throw new Error(`Failed to transform signatures to object: invalid signatures ${signatures}`) // Input cases // case 1. '0xf1998...' // case 2. {V: '0x4e44', R: '0x1692a...', S: '0x277b9...'} or {v: '0x4e44', r: '0x1692a...', s: '0x277b9...'} // case 3. ['0xf1998...', '0x53fe7...'] // case 4. ['0x4e44', '0x1692a...', '0x277b9...'] // case 5. [{V: '0x4e44', R: '0x1692a...', S: '0x277b9...'}, {v: '0x4e44', r: '0x1692a...', s: '0x277b9...'}] // case 6. [['0x4e44', '0x1692a...', '0x277b9...'], ['0x4e44', '0x1692a...', '0x277b9...']] // Transform a signature to an array of signatures to execute the same logic in the for loop below. if (!_.isArray(signatures)) { signatures = [signatures] isSingular = true } else if (_.isString(signatures[0])) { // This logic is performed for case 3 and case 4. // In case 3, the signature string is in the array. // In case 4, v, r, and s are separately included in the array. // The signature string is a combination of v, r, and s, so the length of the signature string will be longer than 64. // Hence, only case 4 will perform the below logic to form an array of signatures. const stripped = signatures[0].replace('0x', '') if (stripped.length <= 64) { signatures = [signatures] isSingular = true } } const ret = [] for (const sig of signatures) { const sigObj = {} if (_.isArray(sig)) { if (sig.length !== 3) throw new Error(`Failed to transform signatures to object: invalid length of signature (${sig.length})`) if (isEmptySig(sig)) continue const [V, R, S] = sig sigObj.V = V sigObj.R = R sigObj.S = S } else if (_.isString(sig)) { const decoded = Account.decodeSignature(sig).map(s => makeEven(trimLeadingZero(s))) sigObj.V = decoded[0] sigObj.R = decoded[1] sigObj.S = decoded[2] } else if (_.isObject(sig)) { Object.keys(sig).map(key => { if (key === 'v' || key === 'V' || key === '_v') { sigObj.V = sig[key] } else if (key === 'r' || key === 'R' || key === '_r') { sigObj.R = sig[key] } else if (key === 's' || key === 'S' || key === '_s') { sigObj.S = sig[key] } else { throw new Error(`Failed to transform signatures to object: invalid key(${key}) is defined in signature object.`) } }) } else { throw new Error(`Unsupported signature type: ${typeof sig}`) } if (!sigObj.V || !sigObj.R || !sigObj.S) { throw new Error(`Failed to transform signatures to object: invalid signature ${sig}`) } Object.keys(sigObj).map(k => { sigObj[k] = trimLeadingZero(sigObj[k]) }) ret.push(sigObj) } return isSingular ? ret[0] : ret } /** * Returns tx type string. * This function uses an old type string. * * @example * const result = caver.utils.getTxTypeStringFromRawTransaction('0x08f83a808505d21dba00824e20945b2840bcbc2be07fb12d9129ed3a02d8e446594401945b2840bcbc2be07fb12d9129ed3a02d8e4465944c4c3018080') * * @deprecated * @ignore * @param {string} rawTransaction An RLP-encoded transaction string. * @return {string} A transaction type string. */ const getTxTypeStringFromRawTransaction = rawTransaction => { if (typeof rawTransaction !== 'string') throw new Error('Invalid raw Tx', rawTransaction) const type = rawTransaction.slice(0, 4) const typeString = txTypeToString[type] return typeString } /** * Returns tx type string. * This function uses an old type string. * * @example * const result = caver.utils.isValidPublicKey('0x3a06fcf2eb4f096e01bc70ab2c81ba79e82af9c62a3ef5fe1fef329c3ad89e8622aed245899ffa530ddd8ebf1a0a66f157b75a38a715f82ad6061af36cbd9cd8') * * @memberof module:utils * @inner * * @param {string} rawTransaction An RLP-encoded transaction string. * @return {string} A transaction type string. */ const isValidPublicKey = publicKey => { let pubString = publicKey.replace('0x', '') if (pubString.length === 130 && pubString.slice(0, 2) === '04') pubString = pubString.slice(2) if (pubString.length !== 66 && pubString.length !== 128) return false if (pubString.length === 66 && !isCompressedPublicKey(pubString)) return false if (pubString.length === 66) pubString = decompressPublicKey(pubString) const xyPoints = xyPointFromPublicKey(pubString) if (xyPoints === undefined || !xyPoints.length || xyPoints.length !== 2) return false const point = secp256k1.curve.point(xyPoints[0].slice(2), xyPoints[1].slice(2), true) return secp256k1.keyFromPublic(point).validate().result } /** * Return `true` is public key is compressed, otherwise `false`. * * @example * const result = caver.utils.isCompressedPublicKey('0x023a06fcf2eb4f096e01bc70ab2c81ba79e82af9c62a3ef5fe1fef329c3ad89e86') * * @memberof module:utils * @inner * * @param {string} publicKey A public key string. * @return {boolean} `true` means compressed. */ const isCompressedPublicKey = publicKey => { const compressedIndicators = ['02', '03'] const withoutPrefix = publicKey.replace('0x', '') return withoutPrefix.length === 66 && compressedIndicators.includes(withoutPrefix.slice(0, 2)) } /** * Compresses the uncompressed public key. * * @example * const result = caver.utils.compressPublicKey('0x62cef87819b82f62e9c0a38c1fa7dfa089084959df86aca19ff2f6c903db2248b45dc23220ee6bcd8753bb9df8ce7d58e56eabebb14479f3a0ca5ccd4bdea632') * * @memberof module:utils * @inner * * @param {string} publicKey A public key string. * @return {string} A compressed public key. */ const compressPublicKey = uncompressedPublicKey => { const isAlreadyCompressed = isCompressedPublicKey(uncompressedPublicKey) if (isAlreadyCompressed) return uncompressedPublicKey const xyPoints = xyPointFromPublicKey(uncompressedPublicKey) if (xyPoints === undefined || !xyPoints.length || xyPoints.length !== 2) { throw new Error('invalid public key') } const [x, y] = xyPoints const keyPair = secp256k1.keyPair({ pub: { x: x.replace('0x', ''), y: y.replace('0x', ''), }, pubEnc: 'hex', }) const compressedPublicKey = `0x${keyPair.getPublic(true, 'hex')}` return compressedPublicKey } /** * Decompresses the compressed public key. * * @example * const result = caver.utils.decompressPublicKey('0x0262cef87819b82f62e9c0a38c1fa7dfa089084959df86aca19ff2f6c903db2248') * * @memberof module:utils * @inner * * @param {string} publicKey A public key string. * @return {string} A uncompressed public key. */ const decompressPublicKey = compressedPublicKey => { if (!isCompressedPublicKey(compressedPublicKey)) { if (!isValidPublicKey(compressedPublicKey)) throw new Error(`Invalid public key`) return compressedPublicKey } const compressedWithoutPrefix = compressedPublicKey.replace('0x', '') const curve = secp256k1.curve const decoded = curve.decodePoint(compressedWithoutPrefix, 'hex') const hexEncoded = decoded.encode('hex').slice(2) return `0x${hexEncoded}` } const isContractDeployment = txObject => { if (txObject.type) { if (txObject.type.includes('SMART_CONTRACT_DEPLOY') || txObject.type.includes('SmartContractDeploy')) return true if (txObject.type !== 'LEGACY' && txObject.type !== 'TxTypeLegacyTransaction') return false } if (txObject.data && txObject.data !== '0x' && (!txObject.to || txObject.to === '0x')) return true return false } const isValidRole = role => { switch (role) { case 'roleTransactionKey': case 'roleAccountUpdateKey': case 'roleFeePayerKey': case 'transactionKey': case 'updateKey': case 'feePayerKey': return true } return false } // ['0x01', '0x', '0x] // [['0x01', '0x', '0x]] // '0x....' // { v: '0x01', r: '0x', s:'0x' } // SignatureData { _v: '0x01', _r: '0x', _s:'0x' } // [SignatureData { _v: '0x01', _r: '0x', _s:'0x' }] /** * Returns `true` if sig is in the format of empty signature (`SignatureData { _v: '0x01', _r: '0x', _s: '0x' }` or `[SignatureData { _v: '0x01', _r: '0x', _s: '0x' }]`), otherwise it returns `false`. * * In caver-js, if signatures or feePayerSignatures is empty, the value representing an empty signature, `[SignatureData { _v: '0x01', _r: '0x', _s: '0x' }]`, is returned for the property. * This function is used to check whether the given signature is `[SignatureData { _v: '0x01', _r: '0x', _s: '0x' }]` (or `SignatureData { _v: '0x01', _r: '0x', _s: '0x' }` in the 'LEGACY' transaction). * * @example * const result = caver.utils.isEmptySig(['0x01', '0x', '0x']) * const result = caver.utils.isEmptySig({ v: '0x01', r: '0x', s: '0x' }) * const result = caver.utils.isEmptySig({ V: '0x01', R: '0x', S: '0x' }) * const result = caver.utils.isEmptySig(new caver.wallet.keyring.signatureData(['0x01', '0x', '0x'])) * * const result = caver.utils.isEmptySig([['0x01', '0x', '0x']]) * const result = caver.utils.isEmptySig([{ v: '0x01', r: '0x', s: '0x' }]) * const result = caver.utils.isEmptySig([{ V: '0x01', R: '0x', S: '0x' }]) * const result = caver.utils.isEmptySig([new caver.wallet.keyring.signatureData(['0x01', '0x', '0x'])]) * * @memberof module:utils * @inner * * @param {object|Array.<object>|Array.<string>|Array.<Array.<string>>|SignatureData|Array.<SignatureData>} sig An instance of {@link SignatureData} or array of {@link SignatureData} to check empty or not. * @return {boolean} `true` means the sig is empty. */ const isEmptySig = sig => { let sigs = sig // Convert to array format if (!_.isArray(sig)) sigs = resolveSignature(sigs) // Format to two-dimentional array if (_.isString(sigs[0])) sigs = [sigs] for (let s of sigs) { if (!_.isArray(s)) s = resolveSignature(s) if (s.length !== 3) throw new Error(`Invalid signatures length: ${s.length}`) if (s[0] !== '0x01' || s[1] !== '0x' || s[2] !== '0x') return false } return true } /** * Hashes message with Klaytn specific prefix: `keccak256("\x19Klaytn Signed Message:\n" + len(message) + message))` * * @example * const result = caver.utils.hashMessage('Hello') // '0x640bfab59b6e27468abd367888f4ab1a1c77aa2b45e76a1d3adcbd039c305917' * * @memberof module:utils * @inner * * @param {string} data A message to hash. If it is a HEX string, it will be UTF-8 decoded first. * @return {string} The hashed message with Klaytn specific prefix. */ const hashMessage = data => { const messageHex = isHexStrict(data) ? data : utf8ToHex(data) const messageBytes = hexToBytes(messageHex) const messageBuffer = Buffer.from(messageBytes) const preamble = `\x19Klaytn Signed Message:\n${messageBytes.length}` const preambleBuffer = Buffer.from(preamble) // klayMessage is concatenated buffer (preambleBuffer + messageBuffer) const klayMessage = Buffer.concat([preambleBuffer, messageBuffer]) // Finally, run keccak256 on klayMessage. return Hash.keccak256(klayMessage) } /** * Recovers the public key that was used to sign the given data. * * @example * const message = 'Some data' * const signature = { v: '0x1c', r: '0xd0b8d...', s: '0x5472e...' } // You can get a signature via `keyring.signMessage(...).signatures[0]`. * const recoveredPublicKey = caver.utils.recoverPublicKey(message, signature) * * @memberof module:utils * @inner * * @param {string} message The raw message string. If this message is hased with Klaytn specific prefix, the third parameter should be passed as `true`. * @param {SignatureData|Array.<string>|object} signature An instance of `SignatureData`, `[v, r, s]` or `{v, r, s}`. * @param {boolean} [isHashed] (optional, default: `false`) If the `isHashed` is true, the given message will NOT automatically be prefixed with "\x19Klaytn Signed Message:\n" + message.length + message, and be assumed as already prefixed. * @return {string} */ const recoverPublicKey = (message, signature, isHashed = false) => { if (!isHashed) message = hashMessage(message) if (_.isArray(signature)) signature = { v: signature[0], r: signature[1], s: signature[2] } const vrs = { v: parseInt(signature.v.slice(2), 16), r: signature.r.slice(2), s: signature.s.slice(2) } const ecPublicKey = secp256k1.recoverPubKey(Buffer.from(message.slice(2), 'hex'), vrs, vrs.v < 2 ? vrs.v : 1 - (vrs.v % 2)) return `0x${ecPublicKey.encode('hex', false).slice(2)}` } /** * Recovers the Klaytn address that was used to sign the given data. * * @example * const message = 'Some data' * const signature = { v: '0x1c', r: '0xd0b8d...', s: '0x5472e...' } // You can get a signature via `keyring.signMessage(...).signatures[0]`. * const recoveredPublicKey = caver.utils.recover(message, signature) * * @memberof module:utils * @inner * * @param {string} message The raw message string. If this message is hased with Klaytn specific prefix, the third parameter should be passed as `true`. * @param {SignatureData|Array.<string>|object} signature An instance of `SignatureData`, `[v, r, s]` or `{v, r, s}`. * @param {boolean} [isHashed] (optional, default: `false`) If the `isHashed` is true, the given message will NOT automatically be prefixed with "\x19Klaytn Signed Message:\n" + message.length + message, and be assumed as already prefixed. * @return {string} */ const recover = (message, signature, isHashed = false) => { if (!isHashed) { message = hashMessage(message) } return Account.recover(message, Account.encodeSignature(resolveSignature(signature))).toLowerCase() } /** * Returns an address which is derived by a public key. * This function simply converts the public key string into address form by hashing it. * It has nothing to do with the actual account in the Klaytn. * * @example * const address = caver.utils.publicKeyToAddress('0x{public key}') * * @memberof module:utils * @inner * * @method publicKeyToAddress * @param {string} pubKey The public key string to get the address. * @return {string} */ const publicKeyToAddress = pubKey => { let publicKey = pubKey.slice(0, 2) === '0x' ? pubKey : `0x${pubKey}` if (isCompressedPublicKey(publicKey)) publicKey = decompressPublicKey(publicKey) // With '0x' prefix, 65 bytes in uncompressed format. if (Buffer.byteLength(publicKey, 'hex') !== 65) throw new Error(`Invalid public key: ${pubKey}`) const publicHash = Hash.keccak256(publicKey) const address = `0x${publicHash.slice(-40)}` const addressHash = Hash.keccak256s(address.slice(2)) let checksumAddress = '0x' for (let i = 0; i < 40; i++) checksumAddress += parseInt(addressHash[i + 2], 16) > 7 ? address[i + 2].toUpperCase() : address[i + 2] return checksumAddress } module.exports = { BN: BN, isBN: isBN, BigNumber: BigNumber, isBigNumber: isBigNumber, toBN: to