UNPKG

six-caver-js

Version:

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

473 lines (400 loc) 14.8 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-eth-abi/src/index.js (2019/06/12). Modified and improved for the caver-js development. */ /** * @file index.js * @author Marek Kotewicz <marek@parity.io> * @author Fabian Vogelsteller <fabian@frozeman.de> * @date 2017 */ const _ = require('lodash') const EthersAbiCoder = require('@ethersproject/abi').AbiCoder const ParamType = require('@ethersproject/abi').ParamType const utils = require('../../caver-utils') const ethersAbiCoder = new EthersAbiCoder(function(type, value) { if (type.match(/^u?int/) && !_.isArray(value) && (!_.isObject(value) || value.constructor.name !== 'BN')) { return value.toString() } return value }) // result method function Result() {} /** * ABICoder prototype should be used to encode/decode solidity params of any type */ const ABICoder = function() {} /** * Encodes the function name to its ABI representation, which are the first 4 bytes of the sha3 of the function name including types. * * @method encodeFunctionSignature * @param {String|Object} functionName * @return {String} encoded function name */ ABICoder.prototype.encodeFunctionSignature = function(functionName) { if (_.isObject(functionName)) { functionName = utils._jsonInterfaceMethodToString(functionName) } return utils.sha3(functionName).slice(0, 10) } /** * Encodes the function name to its ABI representation, which are the first 4 bytes of the sha3 of the function name including types. * * @method encodeEventSignature * @param {String|Object} functionName * @return {String} encoded function name */ ABICoder.prototype.encodeEventSignature = function(functionName) { if (_.isObject(functionName)) { functionName = utils._jsonInterfaceMethodToString(functionName) } return utils.sha3(functionName) } /** * Should be used to encode plain param * * @method encodeParameter * @param {String} type * @param {Object} param * @return {String} encoded plain param */ ABICoder.prototype.encodeParameter = function(type, param) { return this.encodeParameters([type], [param]) } /** * Should be used to encode list of params * Tuple type can be used like below * `caver.abi.encodeParameters(['tuple(bytes32,bool)', 'tuple(bool,address)'], [['0xabd...', true], [true, '0x776...']])` * * @method encodeParameters * @param {Array} types * @param {Array} params * @return {String} encoded list of params */ ABICoder.prototype.encodeParameters = function(types, params) { const self = this types = self.mapTypes(types) params = params.map(function(param, index) { let type = types[index] // { components: [[Object], [Object]], name: 'b', type: 'tuple' } if (typeof type === 'object' && type.type) { // We may get a named type of shape {name, type} type = type.type } param = self.formatParam(type, param) // If the type is string but number comes in, ethersAbiCoder ignores the type and encodes successfully. // To avoid invalid encoding value, adding error handling. if (type === 'string' && typeof param !== 'string') throw new Error(`Invalid parameter: Parameter value and type do not match.`) // Format params for tuples if (typeof type === 'string' && type.includes('tuple')) { const coder = ethersAbiCoder._getCoder(ParamType.from(type)) // eslint-disable-next-line no-shadow const modifyParams = (coder, param) => { if (coder.name === 'array') { return param.map(p => modifyParams(ethersAbiCoder._getCoder(ParamType.from(coder.type.replace('[]', ''))), p)) } coder.coders.forEach((c, i) => { if (c.name === 'tuple') { modifyParams(c, param[i]) } else { param[i] = self.formatParam(c.name, param[i]) } }) } modifyParams(coder, param) } return param }) return ethersAbiCoder.encode(types, params) } /** * Should be used to encode smart contract deployment with constructor arguments * * @method encodeContractDeploy * @param {Array} types * @param {Array} params * @return {String} bytecode + args */ ABICoder.prototype.encodeContractDeploy = function(jsonInterface, bytecode, ...args) { if (!jsonInterface) { throw new Error('jsonInterface should be provided for encoding contract deployment.') } if (!bytecode) { throw new Error('bytecode should be provided for encoding contract deployment.') } const constructorInterface = jsonInterface.filter(({ type }) => type === 'constructor')[0] const constructorInputs = constructorInterface && constructorInterface.inputs // If constructor doesn't exist in smart contract, only bytecode is needed for deploying. if (!constructorInterface || !constructorInputs || _.isEmpty(constructorInputs)) { return bytecode } if (constructorInputs.length !== args.length) { throw new Error(`invalid number of parameters for deploying. Got ${args.length} expected ${constructorInputs.length}!`) } const constructorTypes = constructorInputs.map(({ type }) => type) return bytecode + this.encodeParameters(constructorTypes, args).replace('0x', '') } /** * Map types if simplified format is used * * @method mapTypes * @param {Array} types * @return {Array} */ ABICoder.prototype.mapTypes = function(types) { const self = this const mappedTypes = [] types.forEach(function(type) { // Remap `function` type params to bytes24 since Ethers does not // recognize former type. Solidity docs say `Function` is a bytes24 // encoding the contract address followed by the function selector hash. if (typeof type === 'object' && type.type === 'function') { type = Object.assign({}, type, { type: 'bytes24' }) } if (self.isSimplifiedStructFormat(type)) { const structName = Object.keys(type)[0] mappedTypes.push( Object.assign(self.mapStructNameAndType(structName), { components: self.mapStructToCoderFormat(type[structName]), }) ) return } mappedTypes.push(type) }) return mappedTypes } /** * Check if type is simplified struct format * * @method isSimplifiedStructFormat * @param {string | Object} type * @returns {boolean} */ ABICoder.prototype.isSimplifiedStructFormat = function(type) { return typeof type === 'object' && typeof type.components === 'undefined' && typeof type.name === 'undefined' } /** * Maps the correct tuple type and name when the simplified format in encode/decodeParameter is used * * @method mapStructNameAndType * @param {string} structName * @return {{type: string, name: *}} */ ABICoder.prototype.mapStructNameAndType = function(structName) { let type = 'tuple' if (structName.indexOf('[]') > -1) { type = 'tuple[]' structName = structName.slice(0, -2) } return { type: type, name: structName } } /** * Maps the simplified format in to the expected format of the ABICoder * * @method mapStructToCoderFormat * @param {Object} struct * @return {Array} */ ABICoder.prototype.mapStructToCoderFormat = function(struct) { const self = this const components = [] Object.keys(struct).forEach(function(key) { if (typeof struct[key] === 'object') { components.push( Object.assign(self.mapStructNameAndType(key), { components: self.mapStructToCoderFormat(struct[key]), }) ) return } components.push({ name: key, type: struct[key], }) }) return components } /** * Handle some formatting of params for backwards compatability with Ethers V4 * * @method formatParam * @param {String} - type * @param {any} - param * @return {any} - The formatted param */ ABICoder.prototype.formatParam = function(type, param) { const paramTypeBytes = new RegExp(/^bytes([0-9]*)$/) const paramTypeBytesArray = new RegExp(/^bytes([0-9]*)\[\]$/) const paramTypeNumber = new RegExp(/^(u?int)([0-9]*)$/) const paramTypeNumberArray = new RegExp(/^(u?int)([0-9]*)\[\]$/) // Format BN to string if (utils.isBN(param) || utils.isBigNumber(param)) { return param.toString(10) } if (type.match(paramTypeBytesArray) || type.match(paramTypeNumberArray)) { return param.map(p => this.formatParam(type.replace('[]', ''), p)) } // Format correct width for u?int[0-9]* let match = type.match(paramTypeNumber) if (match) { const size = parseInt(match[2] || '256') if (size / 8 < param.length) { // pad to correct bit width param = utils.leftPad(param, size) } } // Format correct length for bytes[0-9]+ match = type.match(paramTypeBytes) if (match) { if (Buffer.isBuffer(param)) { param = utils.toHex(param) } // format to correct length const size = parseInt(match[1]) if (size) { let maxSize = size * 2 if (param.substring(0, 2) === '0x') { maxSize += 2 } if (param.length < maxSize) { // pad to correct length param = utils.rightPad(param, size * 2) } } // format odd-length bytes to even-length if (param.length % 2 === 1) { param = `0x0${param.substring(2)}` } } return param } /** * Encodes a function call from its json interface and parameters. * * @method encodeFunctionCall * @param {Array} jsonInterface * @param {Array} params * @return {String} The encoded ABI for this function call */ ABICoder.prototype.encodeFunctionCall = function(jsonInterface, params) { return this.encodeFunctionSignature(jsonInterface) + this.encodeParameters(jsonInterface.inputs, params).replace('0x', '') } /** * Should be used to decode bytes to plain param * * @method decodeParameter * @param {String} type * @param {String} bytes * @return {Object} plain param */ ABICoder.prototype.decodeParameter = function(type, bytes) { return this.decodeParameters([type], bytes)[0] } /** * Should be used to decode list of params * * @method decodeParameters * @param {Array} outputs * @param {String} bytes * @return {Array} array of plain params */ ABICoder.prototype.decodeParameters = function(outputs, bytes) { return this.decodeParametersWith(outputs, bytes, false) } /** * Should be used to decode list of params * * @method decodeParametersWith * @param {Array} outputs * @param {String} bytes * @param {Boolean} loose must be passed for decoding bytes and string parameters for logs emitted with solc 0.4.x * Please refer to https://github.com/ChainSafe/web3.js/commit/e80337e16e5c04683fc40148378775234c28e0fb. * @return {Array} array of plain params */ ABICoder.prototype.decodeParametersWith = function(outputs, bytes, loose) { if (outputs.length > 0 && (!bytes || bytes === '0x' || bytes === '0X')) { throw new Error( "Returned values aren't valid, did it run Out of Gas? " + 'You might also see this error if you are not using the ' + 'correct ABI for the contract you are retrieving data from, ' + 'requesting data from a block number that does not exist, ' + 'or querying a node which is not fully synced.' ) } const res = ethersAbiCoder.decode(this.mapTypes(outputs), `0x${bytes.replace(/0x/i, '')}`, loose) const returnValue = new Result() returnValue.__length__ = 0 outputs.forEach(function(output, i) { let decodedValue = res[returnValue.__length__] decodedValue = decodedValue === '0x' ? null : decodedValue returnValue[i] = decodedValue if (_.isObject(output) && output.name) { returnValue[output.name] = decodedValue } returnValue.__length__++ }) return returnValue } /** * Decodes events non- and indexed parameters. * * @method decodeLog * @param {Object} inputs * @param {String} data * @param {Array} topics * @return {Array} array of plain params */ ABICoder.prototype.decodeLog = function(inputs, data, topics) { const _this = this topics = _.isArray(topics) ? topics : [topics] data = data || '' const notIndexedInputs = [] const indexedParams = [] let topicCount = 0 // TODO check for anonymous logs? inputs.forEach(function(input, i) { if (input.indexed) { indexedParams[i] = ['bool', 'int', 'uint', 'address', 'fixed', 'ufixed'].find(function(staticType) { return input.type.indexOf(staticType) !== -1 }) ? _this.decodeParameter(input.type, topics[topicCount]) : topics[topicCount] topicCount++ } else { notIndexedInputs[i] = input } }) const nonIndexedData = data const notIndexedParams = nonIndexedData ? this.decodeParametersWith(notIndexedInputs, nonIndexedData, true) : [] const returnValue = new Result() returnValue.__length__ = 0 inputs.forEach(function(res, i) { returnValue[i] = res.type === 'string' ? '' : null if (typeof notIndexedParams[i] !== 'undefined') { returnValue[i] = notIndexedParams[i] } if (typeof indexedParams[i] !== 'undefined') { returnValue[i] = indexedParams[i] } if (res.name) { returnValue[res.name] = returnValue[i] } returnValue.__length__++ }) return returnValue } const coder = new ABICoder() module.exports = coder