'use strict'; // See: https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI var utils = (function() { var convert = require('../utils/convert.js'); var utf8 = require('../utils/utf8.js'); return { defineProperty: require('../utils/properties.js').defineProperty, arrayify: convert.arrayify, padZeros: convert.padZeros, bigNumberify: require('../utils/bignumber.js').bigNumberify, getAddress: require('../utils/address').getAddress, concat: convert.concat, toUtf8Bytes: utf8.toUtf8Bytes, toUtf8String: utf8.toUtf8String, hexlify: convert.hexlify, }; })(); var errors = require('./errors'); var paramTypeBytes = new RegExp(/^bytes([0-9]*)$/); var paramTypeNumber = new RegExp(/^(u?int)([0-9]*)$/); var paramTypeArray = new RegExp(/^(.*)\[([0-9]*)\]$/); var defaultCoerceFunc = function(type, value) { var match = type.match(paramTypeNumber) if (match && parseInt(match[2]) <= 48) { return value.toNumber(); } return value; } var coderNull = function(coerceFunc) { return { name: 'null', type: '', encode: function(value) { return utils.arrayify([]); }, decode: function(data, offset) { if (offset > data.length) { throw new Error('invalid null'); } return { consumed: 0, value: coerceFunc('null', undefined) } }, dynamic: false }; } var coderNumber = function(coerceFunc, size, signed, localName) { var name = ((signed ? 'int': 'uint') + (size * 8)); return { localName: localName, name: name, type: name, encode: function(value) { try { value = utils.bigNumberify(value) } catch (error) { errors.throwError('invalid number value', errors.INVALID_ARGUMENT, { arg: localName, type: typeof(value), value: value }); } value = value.toTwos(size * 8).maskn(size * 8); //value = value.toTwos(size * 8).maskn(size * 8); if (signed) { value = value.fromTwos(size * 8).toTwos(256); } return utils.padZeros(utils.arrayify(value), 32); }, decode: function(data, offset) { if (data.length < offset + 32) { errors.throwError('insufficient data for ' + name + ' type', errors.INVALID_ARGUMENT, { arg: localName, coderType: name, value: utils.hexlify(data.slice(offset, offset + 32)) }); } var junkLength = 32 - size; var value = utils.bigNumberify(data.slice(offset + junkLength, offset + 32)); if (signed) { value = value.fromTwos(size * 8); } else { value = value.maskn(size * 8); } //if (size <= 6) { value = value.toNumber(); } return { consumed: 32, value: coerceFunc(name, value), } } }; } var uint256Coder = coderNumber(function(type, value) { return value; }, 32, false); var coderBoolean = function(coerceFunc, localName) { return { localName: localName, name: 'boolean', type: 'boolean', encode: function(value) { return uint256Coder.encode(!!value ? 1: 0); }, decode: function(data, offset) { try { var result = uint256Coder.decode(data, offset); } catch (error) { if (error.reason === 'insufficient data for uint256 type') { errors.throwError('insufficient data for boolean type', errors.INVALID_ARGUMENT, { arg: localName, coderType: 'boolean', value: error.value }); } throw error; } return { consumed: result.consumed, value: coerceFunc('boolean', !result.value.isZero()) } } } } var coderFixedBytes = function(coerceFunc, length, localName) { var name = ('bytes' + length); return { localName: localName, name: name, type: name, encode: function(value) { try { value = utils.arrayify(value); } catch (error) { errors.throwError('invalid ' + name + ' value', errors.INVALID_ARGUMENT, { arg: localName, type: typeof(value), value: error.value }); } if (length === 32) { return value; } var result = new Uint8Array(32); result.set(value); return result; }, decode: function(data, offset) { if (data.length < offset + 32) { errors.throwError('insufficient data for ' + name + ' type', errors.INVALID_ARGUMENT, { arg: localName, coderType: name, value: utils.hexlify(data.slice(offset, offset + 32)) }); } return { consumed: 32, value: coerceFunc(name, utils.hexlify(data.slice(offset, offset + length))) } } }; } var coderAddress = function(coerceFunc, localName) { return { localName: localName, name: 'address', type: 'address', encode: function(value) { try { value = utils.arrayify(utils.getAddress(value)); } catch (error) { errors.throwError('invalid address', errors.INVALID_ARGUMENT, { arg: localName, type: typeof(value), value: value }); } var result = new Uint8Array(32); result.set(value, 12); return result; }, decode: function(data, offset) { if (data.length < offset + 32) { errors.throwError('insufficuent data for address type', errors.INVALID_ARGUMENT, { arg: localName, coderType: 'address', value: utils.hexlify(data.slice(offset, offset + 32)) }); } return { consumed: 32, value: coerceFunc('address', utils.getAddress(utils.hexlify(data.slice(offset + 12, offset + 32)))) } } } } function _encodeDynamicBytes(value) { var dataLength = parseInt(32 * Math.ceil(value.length / 32)); var padding = new Uint8Array(dataLength - value.length); return utils.concat([ uint256Coder.encode(value.length), value, padding ]); } function _decodeDynamicBytes(data, offset, localName) { if (data.length < offset + 32) { errors.throwError('insufficient data for dynamicBytes length', errors.INVALID_ARGUMENT, { arg: localName, coderType: 'dynamicBytes', value: utils.hexlify(data.slice(offset, offset + 32)) }); } var length = uint256Coder.decode(data, offset).value; try { length = length.toNumber(); } catch (error) { errors.throwError('dynamic bytes count too large', errors.INVALID_ARGUMENT, { arg: localName, coderType: 'dynamicBytes', value: length.toString() }); } if (data.length < offset + 32 + length) { errors.throwError('insufficient data for dynamicBytes type', errors.INVALID_ARGUMENT, { arg: localName, coderType: 'dynamicBytes', value: utils.hexlify(data.slice(offset, offset + 32 + length)) }); } return { consumed: parseInt(32 + 32 * Math.ceil(length / 32)), value: data.slice(offset + 32, offset + 32 + length), } } var coderDynamicBytes = function(coerceFunc, localName) { return { localName: localName, name: 'bytes', type: 'bytes', encode: function(value) { try { value = utils.arrayify(value); } catch (error) { errors.throwError('invalid bytes value', errors.INVALID_ARGUMENT, { arg: localName, type: typeof(value), value: error.value }); } return _encodeDynamicBytes(value); }, decode: function(data, offset) { var result = _decodeDynamicBytes(data, offset, localName); result.value = coerceFunc('bytes', utils.hexlify(result.value)); return result; }, dynamic: true }; } var coderString = function(coerceFunc, localName) { return { localName: localName, name: 'string', type: 'string', encode: function(value) { if (typeof(value) !== 'string') { errors.throwError('invalid string value', errors.INVALID_ARGUMENT, { arg: localName, type: typeof(value), value: value }); } return _encodeDynamicBytes(utils.toUtf8Bytes(value)); }, decode: function(data, offset) { var result = _decodeDynamicBytes(data, offset, localName); result.value = coerceFunc('string', utils.toUtf8String(result.value)); return result; }, dynamic: true }; } function alignSize(size) { return parseInt(32 * Math.ceil(size / 32)); } function pack(coders, values) { if (Array.isArray(values)) { // do nothing } else if (values && typeof(values) === 'object') { var arrayValues = []; coders.forEach(function(coder) { arrayValues.push(values[coder.localName]); }); values = arrayValues; } else { errors.throwError('invalid tuple value', errors.INVALID_ARGUMENT, { coderType: 'tuple', type: typeof(values), value: values }); } if (coders.length !== values.length) { errors.throwError('types/value length mismatch', errors.INVALID_ARGUMENT, { coderType: 'tuple', value: values }); } var parts = []; coders.forEach(function(coder, index) { parts.push({ dynamic: coder.dynamic, value: coder.encode(values[index]) }); }); var staticSize = 0, dynamicSize = 0; parts.forEach(function(part, index) { if (part.dynamic) { staticSize += 32; dynamicSize += alignSize(part.value.length); } else { staticSize += alignSize(part.value.length); } }); var offset = 0, dynamicOffset = staticSize; var data = new Uint8Array(staticSize + dynamicSize); parts.forEach(function(part, index) { if (part.dynamic) { //uint256Coder.encode(dynamicOffset).copy(data, offset); data.set(uint256Coder.encode(dynamicOffset), offset); offset += 32; //part.value.copy(data, dynamicOffset); @TODO data.set(part.value, dynamicOffset); dynamicOffset += alignSize(part.value.length); } else { //part.value.copy(data, offset); @TODO data.set(part.value, offset); offset += alignSize(part.value.length); } }); return data; } function unpack(coders, data, offset) { var baseOffset = offset; var consumed = 0; var value = []; coders.forEach(function(coder) { if (coder.dynamic) { var dynamicOffset = uint256Coder.decode(data, offset); var result = coder.decode(data, baseOffset + dynamicOffset.value.toNumber()); // The dynamic part is leap-frogged somewhere else; doesn't count towards size result.consumed = dynamicOffset.consumed; } else { var result = coder.decode(data, offset); } if (result.value != undefined) { value.push(result.value); } offset += result.consumed; consumed += result.consumed; }); coders.forEach(function(coder, index) { var name = coder.localName; if (!name) { return; } if (typeof(name) === 'object') { name = name.name; } if (!name) { return; } if (name === 'length') { name = '_length'; } if (value[name] != null) { return; } value[name] = value[index]; }); return { value: value, consumed: consumed } return result; } function coderArray(coerceFunc, coder, length, localName) { var type = (coder.type + '[' + (length >= 0 ? length: '') + ']'); return { coder: coder, localName: localName, length: length, name: 'array', type: type, encode: function(value) { if (!Array.isArray(value)) { errors.throwError('expected array value', errors.INVALID_ARGUMENT, { arg: localName, coderType: 'array', type: typeof(value), value: value }); } var count = length; var result = new Uint8Array(0); if (count === -1) { count = value.length; result = uint256Coder.encode(count); } if (count !== value.length) { error.throwError('array value length mismatch', errors.INVALID_ARGUMENT, { arg: localName, coderType: 'array', count: value.length, expectedCount: count, value: value }); } var coders = []; value.forEach(function(value) { coders.push(coder); }); return utils.concat([result, pack(coders, value)]); }, decode: function(data, offset) { // @TODO: //if (data.length < offset + length * 32) { throw new Error('invalid array'); } var consumed = 0; var count = length; if (count === -1) { try { var decodedLength = uint256Coder.decode(data, offset); } catch (error) { errors.throwError('insufficient data for dynamic array length', errors.INVALID_ARGUMENT, { arg: localName, coderType: 'array', value: error.value }); } try { count = decodedLength.value.toNumber(); } catch (error) { errors.throwError('array count too large', errors.INVALID_ARGUMENT, { arg: localName, coderType: 'array', value: decodedLength.value.toString() }); } consumed += decodedLength.consumed; offset += decodedLength.consumed; } var coders = []; for (var i = 0; i < count; i++) { coders.push(coder); } var result = unpack(coders, data, offset); result.consumed += consumed; result.value = coerceFunc(type, result.value); return result; }, dynamic: (length === -1 || coder.dynamic) } } function coderTuple(coerceFunc, coders, localName) { var dynamic = false; var types = []; coders.forEach(function(coder) { if (coder.dynamic) { dynamic = true; } types.push(coder.type); }); var type = ('tuple(' + types.join(',') + ')'); return { coders: coders, localName: localName, name: 'tuple', type: type, encode: function(value) { return pack(coders, value); }, decode: function(data, offset) { var result = unpack(coders, data, offset); result.value = coerceFunc(type, result.value); return result; }, dynamic: dynamic }; } /* function getTypes(coders) { var type = coderTuple(coders).type; return type.substring(6, type.length - 1); } */ function splitNesting(value) { var result = []; var accum = ''; var depth = 0; for (var offset = 0; offset < value.length; offset++) { var c = value[offset]; if (c === ',' && depth === 0) { result.push(accum); accum = ''; } else { accum += c; if (c === '(') { depth++; } else if (c === ')') { depth--; if (depth === -1) { throw new Error('unbalanced parenthsis'); } } } } result.push(accum); return result; } var paramTypeSimple = { address: coderAddress, bool: coderBoolean, string: coderString, bytes: coderDynamicBytes, }; function getParamCoder(coerceFunc, type, localName) { var coder = paramTypeSimple[type]; if (coder) { return coder(coerceFunc, localName); } var match = type.match(paramTypeNumber); if (match) { var size = parseInt(match[2] || 256); if (size === 0 || size > 256 || (size % 8) !== 0) { errors.throwError('invalid ' + match[1] + ' bit length', errors.INVALID_ARGUMENT, { arg: 'type', value: type }); } return coderNumber(coerceFunc, size / 8, (match[1] === 'int'), localName); } var match = type.match(paramTypeBytes); if (match) { var size = parseInt(match[1]); if (size === 0 || size > 32) { errors.throwError('invalid bytes length', errors.INVALID_ARGUMENT, { arg: 'type', value: type }); } return coderFixedBytes(coerceFunc, size, localName); } var match = type.match(paramTypeArray); if (match) { var size = parseInt(match[2] || -1); return coderArray(coerceFunc, getParamCoder(coerceFunc, match[1], localName), size, localName); } if (type.substring(0, 6) === 'tuple(' && type.substring(type.length - 1) === ')') { var coders = []; var names = []; if (localName && typeof(localName) === 'object') { if (Array.isArray(localName.names)) { names = localName.names; } if (typeof(localName.name) === 'string') { localName = localName.name; } } splitNesting(type.substring(6, type.length - 1)).forEach(function(type, index) { coders.push(getParamCoder(coerceFunc, type, names[index])); }); return coderTuple(coerceFunc, coders, localName); } if (type === '') { return coderNull(coerceFunc); } errors.throwError('invalid type', errors.INVALID_ARGUMENT, { arg: 'type', value: type }); } function Coder(coerceFunc) { if (!(this instanceof Coder)) { throw new Error('missing new'); } if (!coerceFunc) { coerceFunc = defaultCoerceFunc; } utils.defineProperty(this, 'coerceFunc', coerceFunc); } utils.defineProperty(Coder.prototype, 'encode', function(names, types, values) { // Names is optional, so shift over all the parameters if not provided if (arguments.length < 3) { values = types; types = names; names = null; } if (types.length !== values.length) { errors.throwError('types/values length mismatch', errors.INVALID_ARGUMENT, { count: { types: types.length, values: values.length }, value: { types: types, values: values } }); } if (names && names.length != types.length) { errors.throwError('names/types length mismatch', errors.INVALID_ARGUMENT, { count: { names: names.length, types: types.length }, value: { names: names, types: types } }); } var coders = []; types.forEach(function(type, index) { coders.push(getParamCoder(this.coerceFunc, type, (names ? names[index]: undefined))); }, this); return utils.hexlify(coderTuple(this.coerceFunc, coders).encode(values)); }); utils.defineProperty(Coder.prototype, 'decode', function(names, types, data) { // Names is optional, so shift over all the parameters if not provided if (arguments.length < 3) { data = types; types = names; names = null; } data = utils.arrayify(data); var coders = []; types.forEach(function(type, index) { coders.push(getParamCoder(this.coerceFunc, type, (names ? names[index]: undefined))); }, this); return coderTuple(this.coerceFunc, coders).decode(data, 0).value; }); utils.defineProperty(Coder, 'defaultCoder', new Coder()); module.exports = Coder