caver-js
Version:
caver-js is a JavaScript API library that allows developers to interact with a Kaia node
702 lines (625 loc) • 28.4 kB
JavaScript
/*
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() {}
class ABI {
/**
* Encodes the function signature to its ABI signature, which are the first 4 bytes of the sha3 hash of the function name including parameter types.
*
* @example
* caver.abi.encodeFunctionSignature({ name: 'myMethod', type: 'function', inputs: [{ type: 'uint256', name: 'myNumber' },{ type: 'string', name: 'mystring' }]})
*
* caver.abi.encodeFunctionSignature('myMethod(uint256,string)')
*
* @method encodeFunctionSignature
* @memberof ABI
* @instance
* @param {string|object} functionSignature The function signature or the JSON interface object of the function to encode. If this is a string, it has to be in the form `function(type, type,...)`, e.g: `myFunction(uint256,uint32[],bytes10,bytes)`.
* @return {string} function signature
*/
encodeFunctionSignature(functionSignature) {
if (_.isObject(functionSignature)) {
functionSignature = utils._jsonInterfaceMethodToString(functionSignature)
}
return utils.sha3(functionSignature).slice(0, 10)
}
/**
* Encodes the event signature to its ABI signature, which is the sha3 hash of the event name including input parameter types.
*
* @example
* caver.abi.encodeEventSignature({ name: 'myEvent', type: 'event', inputs: [{ type: 'uint256', name: 'myNumber' },{ type: 'string', name: 'mystring' }]})
*
* caver.abi.encodeEventSignature('myEvent(uint256,bytes32)')
*
* @method encodeEventSignature
* @memberof ABI
* @instance
* @param {string|object} eventSignature The event signature or the JSON interface object of the event to encode. If this is a string, it has to be in the form `event(type,type,...)`, e.g: `myEvent(uint256,uint32[],bytes10,bytes)`.
* @return {string} event signature
*/
encodeEventSignature(eventSignature) {
if (_.isObject(eventSignature)) {
eventSignature = utils._jsonInterfaceMethodToString(eventSignature)
}
return utils.sha3(eventSignature)
}
/**
* Encodes a parameter based on its type to its ABI representation.
*
* @example
* caver.abi.encodeParameter('uint256', '2345675643')
* caver.abi.encodeParameter('bytes32[]', [caver.utils.rightPad('0xdf3234', 64), caver.utils.rightPad('0xfdfd', 64)])
* caver.abi.encodeParameter('tuple(bytes32,bool)', ['0xabdef18710a18a18abdef18710a18a18abdef18710a18a18abdef18710a18a18', true])
*
* @method encodeParameter
* @memberof ABI
* @instance
* @param {string|object} type The type of the parameter, see the {@link http://solidity.readthedocs.io/en/develop/types.html|solidity documentation} for a list of types.
* @param {*} param The actual parameter to encode.
* @return {string} encoded plain param
*/
encodeParameter(type, param) {
return this.encodeParameters([type], [param])
}
/**
* Encodes function parameters based on its JSON interface object.
*
* @example
* caver.abi.encodeParameters(['uint256','string'], ['2345675643', 'Hello!%'])
*
* caver.abi.encodeParameters(
* ['tuple(bytes32,bool)', 'tuple(bool,address)'],
* [['0xabdef18710a18a18abdef18710a18a18abdef18710a18a18abdef18710a18a18', true], [true, '0x77656c636f6d6520746f20657468657265756d2e']]
* )
*
* caver.abi.encodeParameters(
* [
* {
* components: [{ name: 'a', type: 'bytes32' }, { name: 'b', type: 'bool' }],
* name: 'tupleExample',
* type: 'tuple',
* },
* {
* components: [{ name: 'c', type: 'bool' }, { name: 'd', type: 'address' }],
* name: 'tupleExample2',
* type: 'tuple',
* },
* ],
* [
* ['0xabdef18710a18a18abdef18710a18a18abdef18710a18a18abdef18710a18a18', true],
* [true, '0x77656c636f6d6520746f20657468657265756d2e']
* ]
*)
*
* @method encodeParameters
* @memberof ABI
* @instance
* @param {Array.<string|object>} types An array with types or a JSON interface of a function. See the {@link http://solidity.readthedocs.io/en/develop/types.html|solidity documentation} for a list of types.
* @param {Array.<*>} params The parameters to encode.
* @return {string} encoded list of params
*/
encodeParameters(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 => {
// `coder.type.replace('[]','')` can handle'tuple(string,string)[]', but cannot handle `tuple(string,string)[3]'.
// Therefore, in order to handle tuple arrays of fixed length, the logic is changed to handle strings using regular expression expressions.
const replacedType = coder.type.replace(/\[[1-9]*\]/g, '')
const parameterType = ParamType.from(replacedType)
const gotCoder = ethersAbiCoder._getCoder(parameterType)
modifyParams(gotCoder, 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.
*
* @example
* // There is no argument for constructor
* caver.abi.encodeContractDeploy([
* { "constant": true, "inputs": [], "name": "count", "outputs": [ { "name": "", "type": "uint256" } ], "payable": false, "stateMutability": "view", "type": "function" },
* { "constant": true, "inputs": [], "name": "getBlockNumber", "outputs": [ { "name": "", "type": "uint256" } ], "payable": false, "stateMutability": "view", "type": "function" },
* { "constant": false, "inputs": [ { "name": "_count", "type": "uint256" } ], "name": "setCount", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }
* ],'0x{byte code}')
*
* // There is one argument for constructor
* caver.abi.encodeContractDeploy([
* { "constant": true, "inputs": [], "name": "count", "outputs": [ { "name": "", "type": "uint256" } ], "payable": false, "stateMutability": "view", "type": "function" },
* { "constant": true, "inputs": [], "name": "getBlockNumber", "outputs": [ { "name": "", "type": "uint256" } ], "payable": false, "stateMutability": "view", "type": "function" },
* { "constant": false, "inputs": [ { "name": "_count", "type": "uint256" } ], "name": "setCount", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" },
* { "inputs": [ { "name": "_a", "type": "uint256" } ], "payable": false, "stateMutability": "nonpayable", "type": "constructor" }
* ],'0x{byte code}', 1)
*
* @method encodeContractDeploy
* @memberof ABI
* @instance
* @param {Array.<object>} jsonInterface The JSON interface of the contract.
* @param {string} bytecode A bytecode of smart contract to be deployed.
* @param {...*} [args] Arguments to pass to the constructor.
* @return {string} bytecode + args
*/
encodeContractDeploy(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
* @memberof ABI
* @ignore
* @instance
* @param {Array} types
* @return {Array}
*/
mapTypes(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 = { ...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
* @memberof ABI
* @ignore
* @instance
* @param {string|Object} type
* @returns {boolean}
*/
isSimplifiedStructFormat(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
* @memberof ABI
* @ignore
* @instance
* @param {string} structName
* @return {{type: string, name: *}}
*/
mapStructNameAndType(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 ABI
*
* @method mapStructToCoderFormat
* @memberof ABI
* @ignore
* @instance
* @param {object} struct
* @return {Array}
*/
mapStructToCoderFormat(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
* @memberof ABI
* @ignore
* @param {string} - type
* @param {any} - param
* @return {string|Array.<string>} - The formatted param
*/
formatParam(type, param) {
const paramTypeBytes = /^bytes([0-9]*)$/
const paramTypeBytesArray = /^bytes([0-9]*)\[\]$/
const paramTypeNumber = /^(u?int)([0-9]*)$/
const paramTypeNumberArray = /^(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.
*
* @example
* caver.abi.encodeFunctionCall({
* name: 'myMethod',
* type: 'function',
* inputs: [{
* type: 'uint256',
* name: 'myNumber'
* },{
* type: 'string',
* name: 'mystring'
* }]
* }, ['2345675643', 'Hello!%'])
*
* @method encodeFunctionCall
* @memberof ABI
* @instance
* @param {object} jsonInterface The JSON interface object of a function.
* @param {Array.<*>} [params] The parameters to encode.
* @return {string} The encoded ABI for this function call
*/
encodeFunctionCall(jsonInterface, params) {
params = params || []
return this.encodeFunctionSignature(jsonInterface) + this.encodeParameters(jsonInterface.inputs, params).replace('0x', '')
}
/**
* Decodes a function call from its abi object of a function and returns parameters.
* If the function signature of the `abi` passed as a parameter does not match the function signature of the `functionCall`, an error is returned.
*
* @example
* const abi = {
* name: 'myMethod',
* type: 'function',
* inputs: [
* {
* type: 'uint256',
* name: 'myNumber',
* },
* {
* type: 'string',
* name: 'mystring',
* },
* ],
* }
* const functionCall = '0x24ef0...'
* caver.abi.decodeFunctionCall(abi, functionCall)
*
* @method decodeFunctionCall
* @memberof ABI
* @instance
* @param {object} abi The abi object of a function.
* @param {string} functionCall The encoded function call string.
* @return {object} An object which includes plain params. You can use `result[0]` as it is provided to be accessed like an array in the order of the parameters.
*/
decodeFunctionCall(abi, functionCall) {
functionCall = utils.addHexPrefix(functionCall)
if (!_.isObject(abi) || _.isArray(abi))
throw new Error(
`Invalid abi parameter type: To decode function call, you need to pass an abi object of the function as a first parameter.`
)
if (!abi.name || !abi.inputs)
throw new Error(`Insufficient info in abi object: The function name and inputs must be defined inside the abi function object.`)
const funcSig = this.encodeFunctionSignature(abi)
const extractFuncSig = functionCall.slice(0, funcSig.length)
if (funcSig !== extractFuncSig)
throw new Error(
`Invalid function signature: The function signature of the abi as a parameter and the function signatures extracted from the function call string do not match.`
)
return this.decodeParameters(abi.inputs, `0x${functionCall.slice(funcSig.length)}`)
}
/**
* Decodes an ABI encoded parameter to its JavaScript type.
*
* @example
* caver.abi.decodeParameter('uint256', '0x0000000000000000000000000000000000000000000000000000000000000010')
*
* caver.abi.decodeParameter('string', '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000848656c6c6f212521000000000000000000000000000000000000000000000000')
*
* caver.abi.decodeParameter('tuple(bytes32,bool)', '0xabdef18710a18a18abdef18710a18a18abdef18710a18a18abdef18710a18a180000000000000000000000000000000000000000000000000000000000000001')
*
* caver.abi.decodeParameter(
* {
* components: [{ name: 'a', type: 'bytes32' }, { name: 'b', type: 'bool' }],
* name: 'tupleExample',
* type: 'tuple',
* }, '0xabdef18710a18a18abdef18710a18a18abdef18710a18a18abdef18710a18a180000000000000000000000000000000000000000000000000000000000000001'
* )
*
* @method decodeParameter
* @memberof ABI
* @instance
* @param {string|object} type The type of the parameter, see the {@link http://solidity.readthedocs.io/en/develop/types.html|solidity documentation} for a list of types.
* @param {string} encodedString The ABI byte code to decode.
* @return {string} plain param
*/
decodeParameter(type, encodedString) {
return this.decodeParameters([type], encodedString)[0]
}
/**
* Decodes ABI encoded parameters to its JavaScript types.
*
* @example
* caver.abi.decodeParameters(['string', 'uint256'], '0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000ea000000000000000000000000000000000000000000000000000000000000000848656c6c6f212521000000000000000000000000000000000000000000000000')
*
* caver.abi.decodeParameters(
* ['tuple(bytes32,bool)', 'tuple(bool,address)'],
* '0xabdef18710a18a18abdef18710a18a18abdef18710a18a18abdef18710a18a180000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000077656c636f6d6520746f20657468657265756d2e'
* )
*
* caver.abi.decodeParameters([{
* type: 'string',
* name: 'mystring'
* },{
* type: 'uint256',
* name: 'myNumber'
* }], '0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000ea000000000000000000000000000000000000000000000000000000000000000848656c6c6f212521000000000000000000000000000000000000000000000000')
*
* caver.abi.decodeParameters(
* [
* {
* components: [{ name: 'a', type: 'bytes32' }, { name: 'b', type: 'bool' }],
* name: 'tupleExample',
* type: 'tuple',
* },
* {
* components: [{ name: 'c', type: 'bool' }, { name: 'd', type: 'address' }],
* name: 'tupleExample2',
* type: 'tuple',
* },
* ],
* '0xabdef18710a18a18abdef18710a18a18abdef18710a18a18abdef18710a18a180000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000077656c636f6d6520746f20657468657265756d2e'
* )
*
* @method decodeParameters
* @memberof ABI
* @instance
* @param {Array.<string|object>} typesArray An array with types or an array of JSON interface outputs. See the {@link http://solidity.readthedocs.io/en/develop/types.html|solidity documentation} for a list of types.
* @param {string} encodedString The ABI byte code to decode.
* @return {object} An object which includes plain params. You can use `result[0]` as it is provided to be accessed like an array in the order of the parameters.
*/
decodeParameters(outputs, encodedString) {
return this.decodeParametersWith(outputs, encodedString, false)
}
/**
* Should be used to decode list of params
*
* @method decodeParametersWith
* @memberof ABI
* @instance
* @ignore
* @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 {object} An object which includes plain params. You can use `result[0]` as it is provided to be accessed like an array in the order of the parameters.
*/
decodeParametersWith(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 ABI encoded log data and indexed topic data.
*
* @example
* caver.abi.decodeLog(
* [
* {
* type: 'string',
* name: 'mystring'
* },{
* type: 'uint256',
* name: 'myNumber',
* indexed: true
* },{
* type: 'uint8',
* name: 'mySmallNumber',
* indexed: true
* }
* ],
* '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000748656c6c6f252100000000000000000000000000000000000000000000000000',
* ['0x000000000000000000000000000000000000000000000000000000000000f310', '0x0000000000000000000000000000000000000000000000000000000000000010']
* )
*
*
* @method decodeLog
* @memberof ABI
* @instance
* @param {Array.<object>} inputs An array of JSON interface inputs. See the solidity documentation for a list of types.
* @param {string} data The ABI byte code in the data field of a log.
* @param {*} topics An array of the index parameter topics of the log. This array doesn't have topic[0] if it is a non-anonymous event, or otherwise, it has topic[0].
* @return {object} An object which includes plain params. You can use `result[0]` as it is provided to be accessed like an array in the order of the parameters.
*/
decodeLog(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
}
}
/** @instance */
const abi = new ABI()
module.exports = abi