six-caver-js
Version:
caver-js is a JavaScript API library that allows developers to interact with a Klaytn node
1,370 lines (1,180 loc) • 52.7 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-contract/src/index.js (2019/06/12).
Modified and improved for the caver-js development.
*/
/**
* @file contract.js
*
* To initialize a contract use:
*
* let Contract = require('web3-eth-contract');
* Contract.setProvider('ws://localhost:8546');
* let contract = new Contract(abi, address, ...);
*
* @author Fabian Vogelsteller <fabian@ethereum.org>
* @date 2017
*/
const _ = require('lodash')
const core = require('../../caver-core')
const Method = require('../../caver-core-method')
const utils = require('../../caver-utils')
const Subscription = require('../../caver-core-subscriptions').subscription
const SmartContractDeploy = require('../../caver-transaction/src/transactionTypes/smartContractDeploy/smartContractDeploy')
const SmartContractExecution = require('../../caver-transaction/src/transactionTypes/smartContractExecution/smartContractExecution')
const FeeDelegatedSmartContractDeploy = require('../../caver-transaction/src/transactionTypes/smartContractDeploy/feeDelegatedSmartContractDeploy')
const FeeDelegatedSmartContractExecution = require('../../caver-transaction/src/transactionTypes/smartContractExecution/feeDelegatedSmartContractExecution')
const FeeDelegatedSmartContractDeployWithRatio = require('../../caver-transaction/src/transactionTypes/smartContractDeploy/feeDelegatedSmartContractDeployWithRatio')
const FeeDelegatedSmartContractExecutionWithRatio = require('../../caver-transaction/src/transactionTypes/smartContractExecution/feeDelegatedSmartContractExecutionWithRatio')
const KeyringContainer = require('../../caver-wallet')
const { formatters } = require('../../caver-core-helpers')
const { errors } = require('../../caver-core-helpers')
const abi = require('../../caver-abi')
/**
* Should be called to create new contract instance
*
* @method Contract
* @constructor
* @param {Array} jsonInterface
* @param {String} address
* @param {Object} options
*/
/**
* let myContract = new cav.klay.Contract([...], '0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe', {
* from: '0x1234567890123456789012345678901234567891', // default from address
* gasPrice: '20000000000', // default gas price in wei, 20 gwei in this case
* data: '',(bytecode, when contract deploy)
* gas: 200000, (gas limit)
* });
*/
const Contract = function Contract(jsonInterface, address, options) {
const _this = this
const args = Array.prototype.slice.call(arguments)
if (!(this instanceof Contract)) {
throw new Error('Please use the "new" keyword to instantiate a caver.contract() or caver.klay.Contract() object!')
}
// sets _requestmanager
core.packageInit(this, [this.constructor.currentProvider])
this.clearSubscriptions = this._requestManager.clearSubscriptions
if (!jsonInterface || !Array.isArray(jsonInterface)) {
throw new Error('You must provide the json interface of the contract when instantiating a contract object.')
}
// create the options object
this.options = {}
const lastArg = args[args.length - 1]
if (_.isObject(lastArg) && !_.isArray(lastArg)) {
options = lastArg
this.options = _.extend(this.options, this._getOrSetDefaultOptions(options))
if (_.isObject(address)) {
address = null
}
}
Object.defineProperty(this, 'defaultSendOptions', {
get() {
return _this.options
},
})
// set address
Object.defineProperty(this.options, 'address', {
set(value) {
if (value) {
_this._address = utils.toChecksumAddress(formatters.inputAddressFormatter(value))
}
},
get() {
return _this._address
},
enumerable: true,
})
// add method and event signatures, when the jsonInterface gets set
Object.defineProperty(this.options, 'jsonInterface', {
set(value) {
_this.methods = {}
_this.events = {}
_this._jsonInterface = value.map(function(method) {
let func
let funcName
if (method.name) {
funcName = utils._jsonInterfaceMethodToString(method)
}
// function
if (method.type === 'function') {
method.signature = abi.encodeFunctionSignature(funcName)
func = _this._createTxObject.bind({
method,
parent: _this,
})
// add method only if not one already exists
if (!_this.methods[method.name]) {
_this.methods[method.name] = func
} else {
const cascadeFunc = _this._createTxObject.bind({
method,
parent: _this,
nextMethod: _this.methods[method.name],
})
_this.methods[method.name] = cascadeFunc
}
// definitely add the method based on its signature
_this.methods[method.signature] = func
// add method by name
_this.methods[funcName] = func
// event
} else if (method.type === 'event') {
method.signature = abi.encodeEventSignature(funcName)
const event = _this._on.bind(_this, method.signature)
// add method only if not already exists
if (!_this.events[method.name] || _this.events[method.name].name === 'bound ') {
_this.events[method.name] = event
}
// definitely add the method based on its signature
_this.events[method.signature] = event
// add event by name
_this.events[funcName] = event
}
// Make transaction object for constructor and add to the `this.methods`
const constructor = _.find(_this._jsonInterface, function(mth) {
return mth.type === 'constructor'
}) || { type: 'constructor' }
constructor.signature = 'constructor'
const constructorFunc = _this._createTxObject.bind({ method: constructor, parent: _this })
_this.methods[constructor.signature] = constructorFunc
return method
})
// add allEvents
_this.events.allEvents = _this._on.bind(_this, 'allevents')
return _this._jsonInterface
},
get() {
return _this._jsonInterface
},
enumerable: true,
})
// get default account from the Class
let { defaultAccount } = this.constructor
let defaultBlock = this.constructor.defaultBlock || 'latest'
Object.defineProperty(this, 'defaultAccount', {
get() {
return defaultAccount
},
set(val) {
if (val) {
defaultAccount = utils.toChecksumAddress(formatters.inputAddressFormatter(val))
}
return val
},
enumerable: true,
})
Object.defineProperty(this, 'defaultBlock', {
get() {
return defaultBlock
},
set(val) {
if (!utils.isValidBlockNumberCandidate(val)) {
throw new Error('Invalid default block number.')
}
defaultBlock = val
return val
},
enumerable: true,
})
// Check for setting options property.
Object.defineProperty(this.options, 'from', {
set(value) {
if (value) {
_this._from = utils.toChecksumAddress(formatters.inputAddressFormatter(value))
}
},
get() {
return _this._from
},
enumerable: true,
})
Object.defineProperty(this.options, 'feePayer', {
set(value) {
if (value) {
_this._feePayer = utils.toChecksumAddress(formatters.inputAddressFormatter(value))
}
},
get() {
return _this._feePayer
},
enumerable: true,
})
Object.defineProperty(this.options, 'feeDelegation', {
set(value) {
if (value !== undefined) {
_this._feeDelegation = value
}
},
get() {
return _this._feeDelegation
},
enumerable: true,
})
Object.defineProperty(this.options, 'feeRatio', {
set(fr) {
if (fr !== undefined) {
if (!_.isNumber(fr) && !utils.isHex(fr))
throw new Error(`Invalid type fo feeRatio: feeRatio should be number type or hex number string.`)
if (utils.hexToNumber(fr) <= 0 || utils.hexToNumber(fr) >= 100)
throw new Error(`Invalid feeRatio: feeRatio is out of range. [1, 99]`)
_this._feeRatio = utils.numberToHex(fr)
}
},
get() {
return _this._feeRatio
},
enumerable: true,
})
Object.defineProperty(this.options, 'gasPrice', {
set(value) {
if (value) {
if (!utils.isValidNSHSN(value)) {
throw errors.invalidGasPrice()
}
_this._gasPrice = value
}
},
get() {
return _this._gasPrice
},
enumerable: true,
})
Object.defineProperty(this.options, 'gas', {
set(value) {
if (value) {
if (!utils.isValidNSHSN(value)) throw errors.invalidGasLimit()
_this._gas = value
}
},
get() {
return _this._gas
},
enumerable: true,
})
Object.defineProperty(this.options, 'data', {
set(value) {
if (value) {
if (!utils.isHexStrict(value)) throw errors.invalidData()
_this._data = value
}
},
get() {
return _this._data
},
enumerable: true,
})
// properties
this.methods = {}
this.events = {}
this._address = null
this._jsonInterface = []
// set getter/setter properties
this.options.address = address
this.options.jsonInterface = jsonInterface
}
/**
* Creates an instance of Contract.
*
* @method create
* @constructor
* @param {Array} jsonInterface The Contract Application Binary Interface (ABI).
* @param {string} [address] The contract address to call.
* @param {object} [options] The options of the contract.
*/
Contract.create = function(jsonInterface, address, options) {
return new Contract(jsonInterface, address, options)
}
Contract.setProvider = function(provider, accounts) {
core.packageInit(this, [provider])
this._klayAccounts = accounts
}
/**
* Set _keyrings in contract instance.
*
* @param {KeyringContainer} keyrings
*/
Contract.prototype.setKeyrings = function(keyrings) {
if (!(keyrings instanceof KeyringContainer)) throw new Error(`keyrings should be an instance of 'KeyringContainer'`)
this._keyrings = keyrings
}
/**
* Set _wallet in contract instance.
* When _wallet exists, contract will use _wallet instead of _klayAccounts
*
* @param {IWallet} wallet
*/
Contract.prototype.setWallet = function(wallet) {
this._wallet = wallet
}
Contract.prototype.addAccounts = function(accounts) {
this._klayAccounts = accounts
}
/**
* Get the callback and modiufy the array if necessary
*
* @method _getCallback
* @param {Array} args
* @return {Function} the callback
*/
Contract.prototype._getCallback = function getCallback(args) {
if (args && _.isFunction(args[args.length - 1])) {
return args.pop() // modify the args array!
}
}
/**
* Checks that no listener with name "newListener" or "removeListener" is added.
*
* @method _checkListener
* @param {String} type
* @param {String} event
* @return {Object} the contract instance
*/
/**
* this._checkListener('newListener', subOptions.event.name);
* this._checkListener('removeListener', subOptions.event.name);
*/
Contract.prototype._checkListener = function(type, event) {
if (event === type) {
throw new Error(`The event "${type}" is a reserved event name, you can't use it.`)
}
}
/**
* Use default values, if options are not available
*
* @method _getOrSetDefaultOptions
* @param {Object} options the options gived by the user
* @return {Object} the options with gaps filled by defaults
*/
Contract.prototype._getOrSetDefaultOptions = function getOrSetDefaultOptions(options) {
const gasPrice = options.gasPrice ? String(options.gasPrice) : null
const from = options.from ? utils.toChecksumAddress(formatters.inputAddressFormatter(options.from)) : null
options.data = options.data || this.options.data
options.from = from || this.options.from
options.gasPrice = gasPrice || this.options.gasPrice
const feePayer = options.feePayer ? utils.toChecksumAddress(formatters.inputAddressFormatter(options.feePayer)) : null
const feeRatio = options.feeRatio ? options.feeRatio : null
const feeDelegation = options.feeDelegation !== undefined ? options.feeDelegation : null
options.feePayer = feePayer || this.options.feePayer
options.feeRatio = feeRatio || this.options.feeRatio
options.feeDelegation = feeDelegation || this.options.feeDelegation
// If options.gas isn't set manually, use options.gasLimit, this.options.gas instead.
if (typeof options.gas === 'undefined') {
options.gas = options.gasLimit || this.options.gas
}
// TODO replace with only gasLimit?
delete options.gasLimit
return options
}
/**
* Should be used to encode indexed params and options to one final object
*
* @method _encodeEventABI
* @param {Object} event
* @param {Object} options
* @return {Object} everything combined together and encoded
*/
/**
* _encodeEventABI
* 1. options
* options = {
* filter: {...},
* topics: [...],
* }
* cf. topics
* - This allows you to manually set the topics for the event filter.
* - If given the filter property and event signature, (topic[0]) will not
* - be set automatically.
*
* 2. event
* {
* anonymous: Bool,
* signature:
* name: String,
* inputs: [...],
* }
* cf) signature
* - The signature’s hash of the event is one of the topics,
* - unless you used the anonymous specifier to declare the event.
* - This would mean filtering for anonymous, specific events by name is not possible.
* - keccak256("burned(address,uint)") = 0x0970ce1235167a71...
*/
Contract.prototype._encodeEventABI = function(event, options) {
options = options || {}
const filter = options.filter || {}
const result = {}
;['fromBlock', 'toBlock']
.filter(function(f) {
return options[f] !== undefined
})
.forEach(function(f) {
result[f] = formatters.inputBlockNumberFormatter(options[f])
})
// use given topics
if (_.isArray(options.topics)) {
result.topics = options.topics
// create topics based on filter
} else {
result.topics = []
// add event signature
if (event && !event.anonymous && event.name !== 'ALLEVENTS') {
result.topics.push(event.signature)
}
// add event topics (indexed arguments)
if (event.name !== 'ALLEVENTS') {
const indexedTopics = event.inputs
.filter(i => i.indexed === true)
.map(i => {
const value = filter[i.name]
if (!value) return null
// TODO: https://github.com/ethereum/web3.js/issues/344
if (_.isArray(value)) {
return value.map(v => abi.encodeParameter(i.type, v))
}
return abi.encodeParameter(i.type, value)
})
result.topics = result.topics.concat(indexedTopics)
}
if (!result.topics.length) delete result.topics
}
if (this.options.address) {
result.address = this.options.address.toLowerCase()
}
return result
}
/**
* Should be used to decode indexed params and options
*
* @method _decodeEventABI
* @param {Object} data
* @return {Object} result object with decoded indexed && not indexed params
*/
Contract.prototype._decodeEventABI = function(data) {
let event = this
data.data = data.data || ''
data.topics = data.topics || []
const result = formatters.outputLogFormatter(data)
// if allEvents get the right event
if (event.name === 'ALLEVENTS') {
event = event.jsonInterface.find(function(intf) {
return intf.signature === data.topics[0]
}) || { anonymous: true }
}
// create empty inputs if none are present (e.g. anonymous events on allEvents)
event.inputs = event.inputs || []
const argTopics = event.anonymous ? data.topics : data.topics.slice(1)
result.returnValues = abi.decodeLog(event.inputs, data.data, argTopics)
delete result.returnValues.__length__
// add name
result.event = event.name
// add signature
result.signature = event.anonymous || !data.topics[0] ? null : data.topics[0]
// move the data and topics to "raw"
result.raw = {
data: result.data,
topics: result.topics,
}
delete result.data
delete result.topics
return result
}
/**
* Encodes an ABI for a method, including signature or the method.
* Or when constructor encodes only the constructor parameters.
*
* @method _encodeMethodABI
* @param {Mixed} args the arguments to encode
* @param {String} the encoded ABI
*/
Contract.prototype._encodeMethodABI = function _encodeMethodABI() {
const methodSignature = this._method.signature
const args = this.arguments || []
let signature = false
const paramsABI =
this._parent.options.jsonInterface
.filter(function(json) {
return (
(methodSignature === 'constructor' && json.type === methodSignature) ||
((json.signature === methodSignature ||
json.signature === methodSignature.replace('0x', '') ||
json.name === methodSignature) &&
json.type === 'function')
)
})
.map(function(json) {
const inputLength = _.isArray(json.inputs) ? json.inputs.length : 0
if (inputLength !== args.length) {
throw new Error(
`The number of arguments is not matching the methods required number. You need to pass ${inputLength} arguments.`
)
}
if (json.type === 'function') {
signature = json.signature
}
return _.isArray(json.inputs) ? json.inputs : []
})
.map(function(inputs) {
return abi.encodeParameters(inputs, args).replace('0x', '')
})[0] || ''
// return constructor
if (methodSignature === 'constructor') {
if (!this._deployData) {
throw new Error('The contract has no contract data option set. This is necessary to append the constructor parameters.')
}
return this._deployData + paramsABI
// return method
}
const returnValue = signature ? signature + paramsABI : paramsABI
if (!returnValue) {
throw new Error(`Couldn't find a matching contract method named "${this._method.name}".`)
} else {
return returnValue
}
}
/**
* Decode method return values
*
* @method _decodeMethodReturn
* @param {Array} outputs
* @param {String} returnValues
* @return {Object} decoded output return values
*/
Contract.prototype._decodeMethodReturn = function(outputs, returnValues) {
if (!returnValues) {
return null
}
returnValues = returnValues.length >= 2 ? returnValues.slice(2) : returnValues
const result = abi.decodeParameters(outputs, returnValues)
if (result.__length__ === 1) {
return result[0]
}
delete result.__length__
return result
}
/**
* Deploys the contract to the Klaytn.
* After a successful deployment, the promise will be resolved with a new contract instance.
*
* @method deploy
* @param {Object} options An object in which data, which is the byte code of the smart contract to be deployed, and arguments, which are parameters to be passed to the constructor of the smart contract, are defined.
* @param {Function} [callback] The callback function.
* @return {object} An object in which arguments and functions for contract deployment are defined
*/
/**
* Deploys the contract to the Klaytn.
* After a successful deployment, the promise will be resolved with a new contract instance.
*
* @method deploy
* @param {object} sendOptions An object holding parameters that are required for sending a transaction.
* @param {string} byteCode The byte code of the contract.
* @param {...*} parameters The parameters to be passed to the constructor of the smart contract.
* @return {object} Promise will be resolved with a new contract instance. EventEmitter possible events are "error", "transactionHash" and "receipt"
*/
Contract.prototype.deploy = function(options, callback) {
const args = Array.prototype.slice.call(arguments)
// This if condition will handle original usage
// contract.deploy({ data, arguments })
// contract.deploy({ data, arguments }, callback)
if (args.length === 1 || (args.length === 2 && _.isFunction(args[args.length - 1]))) {
options = options || {}
options.arguments = options.arguments || []
options = this._getOrSetDefaultOptions(options)
// return error, if no "data" is specified
if (!options.data) {
const error = new Error('No "data" specified in neither the given options, nor the default options.')
if (callback) callback(error)
throw error
}
return this.methods.constructor(options.data, ...options.arguments)
}
// contract.deploy({from, gas, ...}, byteCode, parameters)
const sendOptions = args[0]
const byteCode = args[1]
const params = args.slice(2)
return this.methods.constructor(byteCode, ...params).send(sendOptions)
}
/**
* Sends a SmartContractExecution transaction to execute the function of the contract deployed in the Klaytn.
* After a successful deployment, the promise will be resolved with a transaction receipt.
*
* @method send
* @param {object} sendOptions An object holding parameters that are required for sending a transaction.
* @param {string} functionName The function name to execute.
* @param {...*} parameters The parameters to be passed to the smart contract function.
* @return {object} Promise will be resolved with a transaction receipt. EventEmitter possible events are "error", "transactionHash" and "receipt"
*/
Contract.prototype.send = function() {
const args = Array.prototype.slice.call(arguments)
// contract.send({from, gas, ...}, 'functionName', parameters)
const sendOptions = args[0]
const functionName = args[1]
const params = args.slice(2)
return this.methods[functionName](...params).send(sendOptions)
}
/**
* Calls a "constant" method and execute its smart contract method in the Klaytn Virtual Machine without sending any transaction.
*
* @method call
* @param {object} [callObject] The options used for calling.
* @param {string} functionName The function name to execute.
* @param {...*} parameters The parameters to be passed to the smart contract function.
* @return {object} Promise will be resolved with a transaction receipt. EventEmitter possible events are "error", "transactionHash" and "receipt"
*/
Contract.prototype.call = function() {
let args = Array.prototype.slice.call(arguments)
// contract.call('functionName', parameters)
// contract.call({from, gas, ...}, 'functionName', parameters)
let callObject = {}
if (_.isObject(args[0])) {
callObject = args[0]
args = args.slice(1)
}
const functionName = args[0]
const params = args.slice(1)
return this.methods[functionName](...params).call(callObject)
}
/**
* Signs a transaction as a sender to deploy or execute the contract.
* After signing, the promise will be resolved with the signed transaction.
*
* If you want to use fee delegation, `feeDelegation` should be defined as `true` in the `sendOptions` parameter.
* Also if you want to use partial fee delegation, you can define `feeRatio` in the `sendOptions` parameter.
*
* @method sign
* @param {object} sendOptions An object holding parameters that are required for sending a transaction.
* @param {string} functionName The function name to execute. If you want to sign for deployig, please send 'constructor' here.
* @param {...*} parameters The parameters to be passed to the smart contract constructor or function.
* @return {object} Promise will be resolved with a transaction receipt. EventEmitter possible events are "error", "transactionHash" and "receipt"
*/
Contract.prototype.sign = function() {
const args = Array.prototype.slice.call(arguments)
// contract.sign({from, gas, ...}, 'constructor', arguments)
// contract.sign({from, gas, ...}, 'functionName', arguments)
// contract.sign({from, gas, feeDelegation: true ...}, 'constructor', arguments)
// contract.sign({from, gas, feeDelegation: true, feeRatio: 30, ...}, 'functionName', arguments)
const sendOptions = args[0]
const functionName = args[1]
const params = args.slice(2)
return this.methods[functionName](...params).sign(sendOptions)
}
/**
* Signs a transaction as a fee payer to deploy or execute the contract.
* After signing, the promise will be resolved with the signed transaction.
*
* To sign as a fee payer, `feeDelegation` and `feePayer` should be defined in the `sendOptions` parameter.
* `feeDelegation` field should be true.
* Also if you want to use partial fee delegation, you can define `feeRatio` in the `sendOptions` parameter.
*
* @method sign
* @param {object} sendOptions An object holding parameters that are required for sending a transaction.
* @param {string} functionName The function name to execute. If you want to sign for deployig, please send 'constructor' here.
* @param {...*} parameters The parameters to be passed to the smart contract constructor or function.
* @return {object} Promise will be resolved with a transaction receipt. EventEmitter possible events are "error", "transactionHash" and "receipt"
*/
Contract.prototype.signAsFeePayer = function() {
const args = Array.prototype.slice.call(arguments)
// contract.signAsFeePayer({from, gas, feeDelegation: true ...}, 'constructor', arguments)
// contract.signAsFeePayer({from, gas, feeDelegation: true, feeRatio: 30, ...}, 'functionName', arguments)
const sendOptions = args[0]
const functionName = args[1]
const params = args.slice(2)
return this.methods[functionName](...params).signAsFeePayer(sendOptions)
}
/**
* Gets the event signature and outputformatters
*
* @method _generateEventOptions
* @param {Object} event
* @param {Object} options
* @param {Function} callback
* @return {Object} the event options object
*/
Contract.prototype._generateEventOptions = function() {
const args = Array.prototype.slice.call(arguments)
// get the callback
const callback = this._getCallback(args)
// get the options
const options = _.isObject(args[args.length - 1]) ? args.pop() : {}
let event = _.isString(args[0]) ? args[0] : 'allevents'
event =
event.toLowerCase() === 'allevents'
? {
name: 'ALLEVENTS',
jsonInterface: this.options.jsonInterface,
}
: this.options.jsonInterface.find(function(json) {
return json.type === 'event' && (json.name === event || json.signature === `0x${event.replace('0x', '')}`)
})
if (!event) {
throw new Error(`Event "${event.name}" doesn't exist in this contract.`)
}
if (!utils.isAddress(this.options.address)) {
throw new Error("This contract object doesn't have address set yet, please set an address first.")
}
return {
params: this._encodeEventABI(event, options),
event,
callback,
}
}
/**
* Adds event listeners and creates a subscription, and remove it once its fired.
*
* @method clone
* @return {Object} the event subscription
*/
Contract.prototype.clone = function(contractAddress = this.options.address) {
const cloned = new this.constructor(this.options.jsonInterface, contractAddress, this.options)
cloned.setWallet(this._wallet)
return cloned
}
/**
* Adds event listeners and creates a subscription, and remove it once its fired.
* (Subscribes to an event and unsubscribes immediately after the first event or error. Will only fire for a single event.)
*
*
* @method once
* @param {String} event
* @param {Object} options
* @param {Function} callback
* @return {Object} the event subscription
*
* myContract.once('MyEvent', {
filter: {myIndexedParam: [20,23], myOtherIndexedParam: '0x123456789...'}, // Using an array means OR: e.g. 20 or 23
fromBlock: 0
}, function(error, event){ console.log(event); });
// event output example
> {
returnValues: {
myIndexedParam: 20,
myOtherIndexedParam: '0x123456789...',
myNonIndexParam: 'My String'
},
raw: {
data: '0x7f9fade1c0d57a7af66ab4ead79fade1c0d57a7af66ab4ead7c2c2eb7b11a91385',
topics: ['0xfd43ade1c09fade1c0d57a7af66ab4ead7c2c2eb7b11a91ffdd57a7af66ab4ead7', '0x7f9fade1c0d57a7af66ab4ead79fade1c0d57a7af66ab4ead7c2c2eb7b11a91385']
},
event: 'MyEvent',
signature: '0xfd43ade1c09fade1c0d57a7af66ab4ead7c2c2eb7b11a91ffdd57a7af66ab4ead7',
logIndex: 0,
transactionIndex: 0,
transactionHash: '0x7f9fade1c0d57a7af66ab4ead79fade1c0d57a7af66ab4ead7c2c2eb7b11a91385',
blockHash: '0xfd43ade1c09fade1c0d57a7af66ab4ead7c2c2eb7b11a91ffdd57a7af66ab4ead7',
blockNumber: 1234,
address: '0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe'
}
*/
Contract.prototype.once = function(event, options, callback) {
const args = Array.prototype.slice.call(arguments)
// get the callback
callback = this._getCallback(args)
if (!callback) {
throw new Error('Once requires a callback as the second parameter.')
}
// don't allow fromBlock
if (options) {
delete options.fromBlock
}
// don't return as once shouldn't provide "on"
this._on(event, options, function(err, res, sub) {
sub.unsubscribe()
if (_.isFunction(callback)) {
callback(err, res, sub)
}
})
return undefined
}
/**
* Adds event listeners and creates a subscription.
*
* @method _on
* @param {String} event
* @param {Object} options
* @param {Function} callback
* @return {Object} the event subscription
*/
Contract.prototype._on = function() {
const subOptions = this._generateEventOptions.apply(this, arguments)
// prevent the event "newListener" and "removeListener" from being overwritten
this._checkListener('newListener', subOptions.event.name)
this._checkListener('removeListener', subOptions.event.name)
// TODO check if listener already exists? and reuse subscription if options are the same.
const subscription = new Subscription({
subscription: {
params: 1,
inputFormatter: [formatters.inputLogFormatter],
outputFormatter: this._decodeEventABI.bind(subOptions.event),
// DUBLICATE, also in caver-klay
subscriptionHandler(output) {
this.emit('data', output)
if (_.isFunction(this.callback)) {
this.callback(null, output, this)
}
},
},
type: 'klay',
requestManager: this._requestManager,
})
subscription.subscribe('logs', subOptions.params, subOptions.callback || function() {})
return subscription
}
/**
* Get past events from contracts
*
* @method getPastEvents
* @param {String} event
* @param {Object} options
* @param {Function} callback
* @return {Object} the promievent
*/
/**
* myContract.getPastEvents('MyEvent', {
filter: {myIndexedParam: [20,23], myOtherIndexedParam: '0x123456789...'}, // Using an array means OR: e.g. 20 or 23
fromBlock: 0,
toBlock: 'latest'
}, function(error, events){ console.log(events); })
.then(function(events){
console.log(events) // same results as the optional callback above
});
> [{
returnValues: {
myIndexedParam: 20,
myOtherIndexedParam: '0x123456789...',
myNonIndexParam: 'My String'
},
raw: {
data: '0x7f9fade1c0d57a7af66ab4ead79fade1c0d57a7af66ab4ead7c2c2eb7b11a91385',
topics: ['0xfd43ade1c09fade1c0d57a7af66ab4ead7c2c2eb7b11a91ffdd57a7af66ab4ead7', '0x7f9fade1c0d57a7af66ab4ead79fade1c0d57a7af66ab4ead7c2c2eb7b11a91385']
},
event: 'MyEvent',
signature: '0xfd43ade1c09fade1c0d57a7af66ab4ead7c2c2eb7b11a91ffdd57a7af66ab4ead7',
logIndex: 0,
transactionIndex: 0,
transactionHash: '0x7f9fade1c0d57a7af66ab4ead79fade1c0d57a7af66ab4ead7c2c2eb7b11a91385',
blockHash: '0xfd43ade1c09fade1c0d57a7af66ab4ead7c2c2eb7b11a91ffdd57a7af66ab4ead7',
blockNumber: 1234,
address: '0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe'
},{
...
}]
*/
Contract.prototype.getPastEvents = function() {
const subOptions = this._generateEventOptions.apply(this, arguments)
let getPastLogs = new Method({
name: 'getPastLogs',
call: 'klay_getLogs',
params: 1,
inputFormatter: [formatters.inputLogFormatter],
outputFormatter: this._decodeEventABI.bind(subOptions.event),
})
getPastLogs.setRequestManager(this._requestManager)
const call = getPastLogs.buildCall()
getPastLogs = null
return call(subOptions.params, subOptions.callback)
}
/**
* returns the an object with call, send, estimate functions
*
* @method _createTxObject
* @returns {Object} an object with functions to call the methods
*/
Contract.prototype._createTxObject = function _createTxObject() {
let args = Array.prototype.slice.call(arguments)
const txObject = {}
if (this.method.type === 'function') {
txObject.call = this.parent._executeMethod.bind(txObject, 'call')
txObject.call.request = this.parent._executeMethod.bind(txObject, 'call', true) // to make batch requests
}
txObject.sign = this.parent._executeMethod.bind(txObject, 'sign')
txObject.signAsFeePayer = this.parent._executeMethod.bind(txObject, 'signAsFeePayer')
txObject.send = this.parent._executeMethod.bind(txObject, 'send')
txObject.send.request = this.parent._executeMethod.bind(txObject, 'send', true) // to make batch requests
txObject.encodeABI = this.parent._encodeMethodABI.bind(txObject)
txObject.estimateGas = this.parent._executeMethod.bind(txObject, 'estimate')
// When deploying a smart contract, if a parameter is passed by directly accessing the tx object,
// the byte code is transferred as the first parameter.
// (i.e. `contract.methods['constructor'](byteCode, arguments...).send({from, ...})`)
// To handle such a case, when `method.type` is a "constructor" and `this.deployData` is empty,
// the byte code received as a parameter is allocated to `this.deployData`,
// and the args after that are used as parameter arguments.
if (this.method.type === 'constructor' && !this.deployData) {
this.deployData = args[0]
args = args.slice(1)
}
txObject.arguments = args || []
txObject._method = this.method
txObject._parent = this.parent
if (args && this.method.inputs) {
if (args.length !== this.method.inputs.length) {
if (this.nextMethod) {
return this.nextMethod.apply(null, args)
}
throw errors.InvalidNumberOfParams(args.length, this.method.inputs.length, this.method.name)
} else if (this.nextMethod) {
// If the number of parameters of the function is the same, but the types of parameters are different,
// determine whether the function is an appropriate function through encoding operation with the input parameter.
// If an encoding error occurs, check by using to the next method.
try {
txObject.encodeABI(args)
} catch (e) {
return this.nextMethod.apply(null, args)
}
}
}
txObject._klayAccounts = this.parent.constructor._klayAccounts || this._klayAccounts
txObject._wallet = this.parent._wallet || this._wallet
if (this.deployData) {
txObject._deployData = this.deployData
}
return txObject
}
/**
* Generates the options for the execute call
*
* @method _processExecuteArguments
* @param {Array} args
* @param {Promise} defer
*/
Contract.prototype._processExecuteArguments = function _processExecuteArguments(args, defer) {
const processedArgs = {}
processedArgs.type = args.shift()
// get the callback
processedArgs.callback = this._parent._getCallback(args)
// get block number to use for call
if (
processedArgs.type === 'call' &&
args[args.length - 1] !== true &&
(_.isString(args[args.length - 1]) || isFinite(args[args.length - 1]))
) {
processedArgs.defaultBlock = args.pop()
}
// get the options
processedArgs.options = _.isObject(args[args.length - 1]) ? args.pop() : {}
// get the generateRequest argument for batch requests
processedArgs.generateRequest = args[args.length - 1] === true ? args.pop() : false
processedArgs.options = this._parent._getOrSetDefaultOptions(processedArgs.options)
processedArgs.options.data = this.encodeABI()
// add contract address
if (!this._deployData && !utils.isAddress(this._parent.options.address)) {
throw new Error("This contract object doesn't have address set yet, please set an address first.")
}
if (!this._deployData) {
processedArgs.options.to = this._parent.options.address
}
// return error, if no "data" is specified
if (!processedArgs.options.data) {
return utils._fireError(
new Error("Couldn't find a matching contract method, or the number of parameters is wrong."),
defer.eventEmitter,
defer.reject,
processedArgs.callback
)
}
return processedArgs
}
/**
* Executes a call, transact or estimateGas on a contract function
*
* @method _executeMethod
* @param {String} type the type this execute function should execute
* @param {Boolean} makeRequest if true, it simply returns the request parameters, rather than executing it
*/
Contract.prototype._executeMethod = async function _executeMethod() {
const _this = this
const args = this._parent._processExecuteArguments.call(this, Array.prototype.slice.call(arguments), defer)
var defer = utils.promiEvent(args.type !== 'send') /* eslint-disable-line no-var */
const klayAccounts = _this.constructor._klayAccounts || _this._klayAccounts
const wallet = _this._parent._wallet || _this._wallet
// Not allow to specify options.gas to 0.
if (args.options && args.options.gas === 0) {
throw errors.notAllowedZeroGas()
}
// simple return request for batch requests
if (args.generateRequest) {
const payload = {
params: [formatters.inputCallFormatter.call(this._parent, args.options)],
callback: args.callback,
}
if (args.type === 'call') {
payload.params.push(formatters.inputDefaultBlockNumberFormatter.call(this._parent, args.defaultBlock))
payload.method = 'klay_call'
payload.format = this._parent._decodeMethodReturn.bind(null, this._method.outputs)
} else {
payload.method = 'klay_sendTransaction'
}
return payload
}
switch (args.type) {
case 'estimate':
const estimateGas = new Method({
name: 'estimateGas',
call: 'klay_estimateGas',
params: 1,
inputFormatter: [formatters.inputCallFormatter],
outputFormatter: utils.hexToNumber,
requestManager: _this._parent._requestManager,
accounts: klayAccounts, // is klay.accounts (necessary for wallet signing)
defaultAccount: _this._parent.defaultAccount,
defaultBlock: _this._parent.defaultBlock,
}).createFunction()
return estimateGas(args.options, args.callback)
case 'call':
// TODO check errors: missing "from" should give error on deploy and send, call ?
const call = new Method({
name: 'call',
call: 'klay_call',
params: 2,
inputFormatter: [formatters.inputCallFormatter, formatters.inputDefaultBlockNumberFormatter],
// add output formatter for decoding
outputFormatter(result) {
return _this._parent._decodeMethodReturn(_this._method.outputs, result)
},
requestManager: _this._parent._requestManager,
accounts: klayAccounts, // is klay.accounts (necessary for wallet signing)
defaultAccount: _this._parent.defaultAccount,
defaultBlock: _this._parent.defaultBlock,
}).createFunction()
return call(args.options, args.defaultBlock, args.callback)
case 'sign':
case 'signAsFeePayer':
const tx = await createTransactionFromArgs(args, this._method, this._deployData, defer)
if (!wallet) {
return utils._fireError(
new Error(
`Contract sign/signAsFeePayer works with 'caver.wallet'. Set to use'caver.wallet' by calling'contract.setWallet'.`
),
defer.eventEmitter,
defer.reject,
args.callback
)
}
const signer = args.type === 'signAsFeePayer' ? args.options.feePayer : args.options.from
const signFunction = args.type === 'signAsFeePayer' ? wallet.signAsFeePayer.bind(wallet) : wallet.sign.bind(wallet)
const isExisted = await wallet.isExisted(signer)
if (!isExisted) {
throw new Error(`Failed to find ${signer}. Please check that the corresponding account or keyring exists.`)
}
return signFunction(signer, tx).then(signedTx => {
return signedTx
})
case 'send':
const transaction = await createTransactionFromArgs(args, this._method, this._deployData, defer)
// make sure receipt logs are decoded
const extraFormatters = {
receiptFormatter(receipt) {
if (_.isArray(receipt.logs)) {
// decode logs
const events = _.map(receipt.logs, function(log) {
return _this._parent._decodeEventABI.call(
{
name: 'ALLEVENTS',
jsonInterface: _this._parent.options.jsonInterface,
},
log
)
})
// make log names keys
receipt.events = {}
let count = 0
events.forEach(function(ev) {
if (ev.event) {
// if > 1 of the same event, don't overwrite any existing events
if (receipt.events[ev.event]) {
if (Array.isArray(receipt.events[ev.event])) {
receipt.events[ev.event].push(ev)
} else {
receipt.events[ev.event] = [receipt.events[ev.event], ev]
}
} else {
receipt.events[ev.event] = ev
}
} else {
receipt.events[count] = ev
count++
}
})
delete receipt.logs
}
return receipt
},
contractDeployFormatter(receipt) {
const newContract = _this._parent.clone(receipt.contractAddress)
return newContract
},
}
// This is the logic for testing to check the transaction type used when deploying the smart contract.
// You can define and use a custom formatter in this way: `contract.deploy({ ... }).send({ ..., contractDeployFormatter })`
extraFormatters.contractDeployFormatter = args.options.contractDeployFormatter
? args.options.contractDeployFormatter
: extraFormatters.contractDeployFormatter
const sendTransaction = new Method({
name: 'sendTransaction',
call: 'klay_sendTransaction',
params: 1,
inputFormatter: [formatters.inputTransactionFormatter],
requestManager: _this._parent._requestManager,
accounts: klayAccounts, // is klay.accounts (necessary for wallet signing)
defaultAccount: _this._parent.defaultAccount,
defaultBlock: _this._parent.defaultBlock,
extraFormatters,
}).createFunction()
if (wallet) {
const isExistedInWallet = await wallet.isExisted(args.options.from)
if (!isExistedInWallet) {
if (wallet instanceof KeyringContainer) {
return sendTransaction(args.options, args.callback)
}
throw new Error(`Failed to find ${args.options.from}. Please check that the corresponding account or keyring exists.`)
}
const sendRawTransaction = new Method({
name: 'sendRawTransaction',
call: 'klay_sendRawTransaction',
params: 1,
requestManager: _this._parent._requestManager,
defaultAccount: _this._parent.defaultAccount,
defaultBlock: _this._parent.defaultBlock,
extraFormatters,
}).createFunction()
return wallet.sign(transaction.from, transaction).then(signedTx => {
if (signedTx.feePayer) {
return wallet.signAsFeePayer(transaction.feePayer, transaction).then(feePayerSignedTx => {
return sendRawTransaction(feePayerSignedTx)
})
}
return sendRawTransaction(signedTx)
})
}
if (args.options.type === undefined) {
if (this._deployData !== undefined) {
args.options.type = 'SMART_CONTRACT_DEPLOY'
} else {
args.options.type = 'SMART_CONTRACT_EXECUTION'
}
}
if (args.options.type !== 'SMART_CONTRACT_EXECUTION' && args.options.type !== 'SMART_CONTRACT_DEPLOY') {
throw new Error('Unsupported transaction type. Please use SMART_CONTRACT_EXECUTION or SMART_CONTRACT_DEPLOY.')
}
const fromInWallet = sendTransaction.method.accounts.wallet[args.options.from.toLowerCase()]
if (!fromInWallet || !fromInWallet.privateKey) {
args.options.type = 'LEGACY'
}
return sendTransaction(args.options, args.callback)
}
}
function createTransactionFromArgs(args, method, deployData, defer) {
// Not to affect original data, copy args.options
const options = Object.assign({}, args.options)
options.value = options.value || 0
if (!utils.isAdd