UNPKG

@silasdavis/legacy-contracts

Version:

Library with javascript bindings for Solidity contracts.

387 lines (332 loc) 10.4 kB
/** * @file contract.js * @author Marek Kotewicz <marek@ethdev.com> * @author Andreas Olofsson * @date 2014 * @module contract */ const G = require('g-functions') const generic = require('@nodeguy/generic') const _ = generic._ const I = require('iteray') const R = require('ramda') var utils = require('./utils/utils') var SolidityEvent = require('web3/lib/web3/event') var SolidityFunction = require('web3/lib/web3/function') const is = require('@nodeguy/type').is var coder = require('web3/lib/solidity/coder') // TODO remove. var pipe = null function ContractManager (pipe) { this._pipe = pipe } ContractManager.prototype.newContractFactory = function (abi) { return new ContractFactory(abi, this._pipe) } ContractManager.prototype.pipe = function () { return this._pipe } ContractManager.prototype.burrow = function () { return this._pipe.burrow() } // Convert Burrow types to Web3 types. const burrowToWeb3 = generic.function() burrowToWeb3.method(_, R.identity ) burrowToWeb3.method([is(Object)], G.map(burrowToWeb3) ) // https://github.com/hyperledger/burrow/blob/ba74718e702cfb781869d5866bd792a38289e8e7/docs/specs/api.md#numbers-and-strings burrowToWeb3.method([/^[0-9A-F]+$/i], ([string]) => '0x' + string ) // Convert Web3 types to Burrow types. const web3ToBurrow = generic.function() web3ToBurrow.method(_, R.identity ) web3ToBurrow.method([is(Object)], G.map(web3ToBurrow) ) // https://github.com/ethereum/wiki/wiki/JSON-RPC#hex-value-encoding web3ToBurrow.method([/^0x[0-9A-F]*$/i], ([hex]) => hex.slice(2) ) /** * Should be called to encode constructor params * * @method encodeConstructorParams * @param {Array} abi * @param {Array} params */ var encodeConstructorParams = function (abi, params) { return abi.filter(function (json) { return json.type === 'constructor' && json.inputs.length === params.length }).map(function (json) { return json.inputs.map(function (input) { return input.type }) }).map(function (types) { return coder.encodeParams(types, burrowToWeb3(params)) })[0] || '' } const wrapCallback = (callback) => (error, output) => error ? callback(error) : callback(null, burrowToWeb3(output)) // Wrap our 'pipe' to make it look like web3's 'eth'. const eth = (pipe) => ({ call: (callObject, defaultBlock, callback) => pipe.call(web3ToBurrow(callObject), wrapCallback(callback)), sendTransaction: (transactionObject, callback) => pipe.transact(web3ToBurrow(transactionObject), wrapCallback(callback)) }) const createFunction = (formatter, pipe, json, address) => { const solidityFunction = new SolidityFunction(eth(pipe), json, address) const wrapMethod = (name, outputUnpacker = R.identity) => function (...args) { const web3Args = burrowToWeb3(args) // If there is a callback. if ((web3Args.length > 0) && (is(Function, I.get(-1, web3Args)))) { const callback = I.get(-1, web3Args) const callbackWrapper = (error, output) => error ? callback(error) : callback(null, formatter(web3ToBurrow(outputUnpacker(output)))) return solidityFunction[name](...I.concat( I.slice(0, -1, web3Args), callbackWrapper )) } else { // Never call the Solidity function in web3's synchronous mode. return solidityFunction[name](...web3Args.concat(R.identity)) } } const func = { call: wrapMethod('call'), sendTransaction: wrapMethod( 'sendTransaction', solidityFunction.unpackOutput.bind(solidityFunction) ) } Object.setPrototypeOf(func, solidityFunction) return func } /** * Should be called to add functions to contract object * * @method addFunctionsToContract * @param {Contract} contract * @param {Array} abi * TODO * @param {function} pipe - The pipe (added internally). * @param {function} outputFormatter - the output formatter (added internally). */ var addFunctionsToContract = function (contract, pipe, outputFormatter) { contract.abi.filter(function (json) { return json.type === 'function' }).map(function (json) { // Add formatter, or if no formatter exists just pass data through. var of = outputFormatter || function (json, output) { return output } const formatter = (result) => of(json.outputs, result) return createFunction(formatter, pipe, json, contract.address) }).forEach(function (f) { f.attachToContract(contract) }) } const createEvent = (json, address, pipe) => { const solidityEvent = new SolidityEvent(null, json, address) const event = { /** * Should be used to create new Burrow events from events. * * @method execute * @param {function} [startCallback] - error-first callback. The data object is the EventSub. If left out * it will not return a sub and will automatically shut the sub down after the first event is received. * It is equivalent to but less expressive then calling 'Contract.eventName.once(eventCallback)'. * @param {function} eventCallback - error-first callback. The data object is a formatted solidity event. * @return {Object} filter object */ execute (startCallback, eventCallback) { if (!eventCallback) { eventCallback = startCallback startCallback = null } return pipe.eventSub(this._address, startCallback, (error, event) => { event = event.EventDataLog event = { topics: event.Topics, address: event.Address, data: event.Data, height: event.Height } if (error) { eventCallback(error) } else { // TODO handle anonymous. // TODO we don't have filtering in tendermint yet, so I do it here. if (event.topics[0].toLowerCase() === this.signature()) { let decoded let error try { decoded = this.decode(burrowToWeb3(event)) } catch (exception) { error = exception } if (error) { eventCallback(error) } else { const converted = Object.assign( {}, decoded, {args: web3ToBurrow(decoded.args)} ) eventCallback(null, converted) } } } }) }, attachToContract (contract) { super.attachToContract(contract) // Will stop as soon as it receives an event. contract[this.displayName()].once = this.execute.bind(this) } } Object.setPrototypeOf(event, solidityEvent) return event } /** * Should be called to add events to contract object * * @method addEventsToContract * @param {Contract} contract * @param {Array} abi */ var addEventsToContract = function (contract, pipe) { contract.abi.filter(function (json) { return json.type === 'event' }).map(function (json) { return createEvent(json, contract.address, pipe) }).forEach(function (e) { e.attachToContract(contract) }) } /** * @deprecated */ function contract (abi) { console.log("DEPRECATED: Don't use the method 'contractsModule.contracts(abi)', instead use 'contractsModule.newContractManager(pipe)' to get a 'ContractManager' instance, and create factories through 'manager.newContractFactory(abi)") return new ContractFactory(abi) } /** * @deprecated */ function init (pipeIn) { console.log("DEPRECATED: Only needed for 'contractsModule.contracts(abi)' which should no longer be used.") pipe = pipeIn } /** * * @param {Array} abi - The abi object. * @param {Object} edbPipe - The Burrow pipe. * @constructor */ var ContractFactory = function (abi, edbPipe) { this.abi = abi if (!edbPipe) { this._pipe = pipe } else { this._pipe = edbPipe } } /** * Should be called to create new contract on a blockchain * * @method new * @param {*} [contract] constructor param1 (optional) * @param {*} [contract] constructor param2 (optional) * @param {Object} contract transaction object (required) * @param {Function} callback */ ContractFactory.prototype.new = function () { // parse arguments var options = {} // required! var callback var args = Array.prototype.slice.call(arguments) if (utils.isFunction(args[args.length - 1])) { callback = args.pop() } var last = args[args.length - 1] if (utils.isObject(last) && !utils.isArray(last)) { options = args.pop() } if (!options.hasOwnProperty('data')) { options.data = '' } if (!options.hasOwnProperty('to')) { options.to = '' } try { // throw an error if there are no options options.data += encodeConstructorParams(this.abi, args) var that = this this._pipe.transact(options, function (error, address) { if (error) { callback(error) } else { that.at(address, callback) } }) } catch (error) { callback(error) } } /** * Should be called to get access to existing contract on a blockchain * * @method at * @param {string} address - contract address * @param {Function} [callback] - optional callback. * @returns {Contract} returns contract if no callback was passed, * otherwise calls callback function (err, contract) */ ContractFactory.prototype.at = function (address, callback) { var of = this._outputFormatter || function (outputData, output) { return output } var contractAt = new Contract(this.abi, address, this._pipe, of) if (!callback) { return contractAt } else { callback(null, contractAt) } } ContractFactory.prototype.setOutputFormatter = function (outputFormatter) { this._outputFormatter = outputFormatter } /** * The contract type. This class is instantiated internally through the factory. * * @method Contract * @param {Array} abi * @param {string} address * @param {pipe} pipe; * @param {Function} outputFormatter - the output formatter. */ var Contract = function (abi, address, pipe, outputFormatter) { this.address = address // TODO avoid conflict somehow. this.abi = abi addFunctionsToContract(this, pipe, outputFormatter) addEventsToContract(this, pipe) } exports.contract = contract exports.init = init exports.newContractManager = function (pipe) { return new ContractManager(pipe) } exports.getPipe = function () { return pipe }