'use strict'; // See: https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI var utils = (function() { var AbiCoder = require('../utils/abi-coder'); var convert = require('../utils/convert'); var properties = require('../utils/properties'); var utf8 = require('../utils/utf8'); return { defineFrozen: properties.defineFrozen, defineProperty: properties.defineProperty, coder: AbiCoder.defaultCoder, parseSignature: AbiCoder.parseSignature, arrayify: convert.arrayify, concat: convert.concat, isHexString: convert.isHexString, toUtf8Bytes: utf8.toUtf8Bytes, keccak256: require('../utils/keccak256'), }; })(); var errors = require('../utils/errors'); function parseParams(params) { var names = []; var types = []; params.forEach(function(param) { if (param.components != null) { if (param.type.substring(0, 5) !== 'tuple') { throw new Error('internal error; report on GitHub'); } var suffix = ''; var arrayBracket = param.type.indexOf('['); if (arrayBracket >= 0) { suffix = param.type.substring(arrayBracket); } var result = parseParams(param.components); names.push({ name: (param.name || null), names: result.names }); types.push('tuple(' + result.types.join(',') + ')' + suffix) } else { names.push(param.name || null); types.push(param.type); } }); return { names: names, types: types } } function populateDescription(object, items) { for (var key in items) { utils.defineProperty(object, key, items[key]); } return object; } /** * - bytecode (optional; only for deploy) * - type ("deploy") */ function DeployDescription() { } /** * - name * - signature * - sighash * - * - * - * - * - type: ("call" | "transaction") */ function FunctionDescription() { } /** * - anonymous * - name * - signature * - parse * - topics * - inputs * - type ("event") */ function EventDescription() { } function Indexed(value) { utils.defineProperty(this, 'indexed', true); utils.defineProperty(this, 'hash', value); } function Result() {} function Interface(abi) { if (!(this instanceof Interface)) { throw new Error('missing new'); } if (typeof(abi) === 'string') { try { abi = JSON.parse(abi); } catch (error) { errors.throwError('could not parse ABI JSON', errors.INVALID_ARGUMENT, { arg: 'abi', errorMessage: error.message, value: abi }); } } var _abi = []; abi.forEach(function(fragment) { if (typeof(fragment) === 'string') { fragment = utils.parseSignature(fragment); } _abi.push(fragment); }); utils.defineFrozen(this, 'abi', _abi); var methods = {}, events = {}, deploy = null; utils.defineProperty(this, 'functions', methods); utils.defineProperty(this, 'events', events); function addMethod(method) { switch (method.type) { case 'constructor': var func = (function() { var inputParams = parseParams(method.inputs); var func = function(bytecode) { if (!utils.isHexString(bytecode)) { errors.throwError('invalid contract bytecode', errors.INVALID_ARGUMENT, { arg: 'bytecode', type: typeof(bytecode), value: bytecode }); } var params = Array.prototype.slice.call(arguments, 1); if (params.length < inputParams.types.length) { errors.throwError('missing constructor argument', errors.MISSING_ARGUMENT, { arg: (inputParams.names[params.length] || 'unknown'), count: params.length, expectedCount: inputParams.types.length }); } else if (params.length > inputParams.types.length) { errors.throwError('too many constructor arguments', errors.UNEXPECTED_ARGUMENT, { count: params.length, expectedCount: inputParams.types.length }); } try { var encodedParams = utils.coder.encode(method.inputs, params) } catch (error) { errors.throwError('invalid constructor argument', errors.INVALID_ARGUMENT, { arg: error.arg, reason: error.reason, value: error.value }); } var result = { bytecode: bytecode + encodedParams.substring(2), type: 'deploy' } return populateDescription(new DeployDescription(), result); } utils.defineFrozen(func, 'inputs', inputParams); utils.defineProperty(func, 'payable', (method.payable == null || !!method.payable)) return func; })(); if (!deploy) { deploy = func; } break; case 'function': var func = (function() { var inputParams = parseParams(method.inputs); var outputParams = parseParams(method.outputs); var signature = '(' + inputParams.types.join(',') + ')'; signature = signature.replace(/tuple/g, ''); signature = method.name + signature; var parse = function(data) { try { return utils.coder.decode(method.outputs, utils.arrayify(data)); } catch(error) { errors.throwError('invalid data for function output', errors.INVALID_ARGUMENT, { arg: 'data', errorArg: error.arg, errorValue: error.value, value: data, reason: error.reason }); } }; var sighash = utils.keccak256(utils.toUtf8Bytes(signature)).substring(0, 10); var func = function() { var result = { name: method.name, signature: signature, sighash: sighash, type: ((method.constant) ? 'call': 'transaction') }; var params = Array.prototype.slice.call(arguments, 0); if (params.length < inputParams.types.length) { errors.throwError('missing input argument', errors.MISSING_ARGUMENT, { arg: (inputParams.names[params.length] || 'unknown'), count: params.length, expectedCount: inputParams.types.length, name: method.name }); } else if (params.length > inputParams.types.length) { errors.throwError('too many input arguments', errors.UNEXPECTED_ARGUMENT, { count: params.length, expectedCount: inputParams.types.length }); } try { var encodedParams = utils.coder.encode(method.inputs, params); } catch (error) { errors.throwError('invalid input argument', errors.INVALID_ARGUMENT, { arg: error.arg, reason: error.reason, value: error.value }); } result.data = sighash + encodedParams.substring(2); result.parse = parse; return populateDescription(new FunctionDescription(), result); } utils.defineFrozen(func, 'inputs', inputParams); utils.defineFrozen(func, 'outputs', outputParams); utils.defineProperty(func, 'payable', (method.payable == null || !!method.payable)) utils.defineProperty(func, 'parseResult', parse); utils.defineProperty(func, 'signature', signature); utils.defineProperty(func, 'sighash', sighash); return func; })(); // Expose the first (and hopefully unique named function if (method.name && methods[method.name] == null) { utils.defineProperty(methods, method.name, func); } // Expose all methods by their signature, for overloaded functions if (methods[func.signature] == null) { utils.defineProperty(methods, func.signature, func); } break; case 'event': var func = (function() { var inputParams = parseParams(method.inputs); var signature = '(' + inputParams.types.join(',') + ')'; signature = signature.replace(/tuple/g, ''); signature = method.name + signature; var result = { anonymous: (!!method.anonymous), name: method.name, signature: signature, type: 'event' }; result.parse = function(topics, data) { if (data == null) { data = topics; topics = null; } // Strip the signature off of non-anonymous topics if (topics != null && !method.anonymous) { topics = topics.slice(1); } var inputIndexed = [], inputNonIndexed = []; var inputDynamic = []; method.inputs.forEach(function(param, index) { if (param.indexed) { if (param.type === 'string' || param.type === 'bytes' || param.type.indexOf('[') >= 0 || param.type.substring(0, 5) === 'tuple') { inputIndexed.push({ type: 'bytes32', name: (param.name || '')}); inputDynamic.push(true); } else { inputIndexed.push(param); inputDynamic.push(false); } } else { inputNonIndexed.push(param); inputDynamic.push(false); } }); if (topics != null) { var resultIndexed = utils.coder.decode( inputIndexed, utils.concat(topics) ); } var resultNonIndexed = utils.coder.decode( inputNonIndexed, utils.arrayify(data) ); var result = new Result(); var nonIndexedIndex = 0, indexedIndex = 0; method.inputs.forEach(function(input, index) { if (input.indexed) { if (topics == null) { result[index] = new Indexed(null); } else if (inputDynamic[index]) { result[index] = new Indexed(resultIndexed[indexedIndex++]); } else { result[index] = resultIndexed[indexedIndex++]; } } else { result[index] = resultNonIndexed[nonIndexedIndex++]; } if (input.name) { result[input.name] = result[index]; } }); result.length = method.inputs.length; return result; }; var func = populateDescription(new EventDescription(), result) utils.defineFrozen(func, 'topics', [ utils.keccak256(utils.toUtf8Bytes(signature)) ]); utils.defineFrozen(func, 'inputs', inputParams); return func; })(); // Expose the first (and hopefully unique) event name if (method.name && events[method.name] == null) { utils.defineProperty(events, method.name, func); } // Expose all events by their signature, for overloaded functions if (methods[func.signature] == null) { utils.defineProperty(methods, func.signature, func); } break; case 'fallback': // Nothing to do for fallback break; default: console.log('WARNING: unsupported ABI type - ' + method.type); break; } }; _abi.forEach(addMethod, this); // If there wasn't a constructor, create the default constructor if (!deploy) { addMethod({type: 'constructor', inputs: []}); } utils.defineProperty(this, 'deployFunction', deploy); } utils.defineProperty(Interface.prototype, 'parseTransaction', function(tx) { var sighash = tx.data.substring(0, 10).toLowerCase(); for (var name in this.functions) { if (name.indexOf('(') === -1) { continue; } var func = this.functions[name]; if (func.sighash === sighash) { var result = utils.coder.decode(func.inputs.types, '0x' + tx.data.substring(10)); return { args: result, signature: func.signature, sighash: func.sighash, parse: func.parseResult, value: tx.value, }; } } return null; }); module.exports = Interface;