UNPKG

ethjs-abi

Version:

Just the Ethereum encoding and decoding methods from the ethers-io-wallet.

417 lines (362 loc) 12.5 kB
'use strict'; var BN = require('bn.js'); var numberToBN = require('number-to-bn'); var keccak256 = require('js-sha3').keccak_256; // from ethereumjs-util function stripZeros(aInput) { var a = aInput; // eslint-disable-line var first = a[0]; // eslint-disable-line while (a.length > 0 && first.toString() === '0') { a = a.slice(1); first = a[0]; } return a; } function bnToBuffer(bnInput) { var bn = bnInput; // eslint-disable-line var hex = bn.toString(16); // eslint-disable-line if (hex.length % 2) { hex = '0' + hex; } return stripZeros(new Buffer(hex, 'hex')); } function isHexString(value, length) { if (typeof value !== 'string' || !value.match(/^0x[0-9A-Fa-f]*$/)) { return false; } if (length && value.length !== 2 + 2 * length) { return false; } return true; } function hexOrBuffer(valueInput, name) { var value = valueInput; // eslint-disable-line if (!Buffer.isBuffer(value)) { if (!isHexString(value)) { var error = new Error(name ? '[ethjs-abi] invalid ' + name : '[ethjs-abi] invalid hex or buffer, must be a prefixed alphanumeric even length hex string'); error.reason = '[ethjs-abi] invalid hex string, hex must be prefixed and alphanumeric (e.g. 0x023..)'; error.value = value; throw error; } value = value.substring(2); if (value.length % 2) { value = '0' + value; } value = new Buffer(value, 'hex'); } return value; } function hexlify(value) { if (typeof value === 'number') { return '0x' + bnToBuffer(new BN(value)).toString('hex'); } else if (value.mod || value.modulo) { return '0x' + bnToBuffer(value).toString('hex'); } else { // eslint-disable-line return '0x' + hexOrBuffer(value).toString('hex'); } } // getKeys([{a: 1, b: 2}, {a: 3, b: 4}], 'a') => [1, 3] function getKeys(params, key, allowEmpty) { var result = []; // eslint-disable-line if (!Array.isArray(params)) { throw new Error('[ethjs-abi] while getting keys, invalid params value ' + JSON.stringify(params)); } for (var i = 0; i < params.length; i++) { // eslint-disable-line var value = params[i][key]; // eslint-disable-line if (allowEmpty && !value) { value = ''; } else if (typeof value !== 'string') { throw new Error('[ethjs-abi] while getKeys found invalid ABI data structure, type value not string'); } result.push(value); } return result; } function coderNumber(size, signed) { return { encode: function encodeNumber(valueInput) { var value = valueInput; // eslint-disable-line if (typeof value === 'object' && value.toString && (value.toTwos || value.dividedToIntegerBy)) { value = value.toString(10).split('.')[0]; } if (typeof value === 'string' || typeof value === 'number') { value = String(value).split('.')[0]; } value = numberToBN(value); value = value.toTwos(size * 8).maskn(size * 8); if (signed) { value = value.fromTwos(size * 8).toTwos(256); } return value.toArrayLike(Buffer, 'be', 32); }, decode: function decodeNumber(data, offset) { var junkLength = 32 - size; // eslint-disable-line var value = new BN(data.slice(offset + junkLength, offset + 32)); // eslint-disable-line if (signed) { value = value.fromTwos(size * 8); } else { value = value.maskn(size * 8); } return { consumed: 32, value: new BN(value.toString(10)) }; } }; } var uint256Coder = coderNumber(32, false); var coderBoolean = { encode: function encodeBoolean(value) { return uint256Coder.encode(value ? 1 : 0); }, decode: function decodeBoolean(data, offset) { var result = uint256Coder.decode(data, offset); // eslint-disable-line return { consumed: result.consumed, value: !result.value.isZero() }; } }; function coderFixedBytes(length) { return { encode: function encodeFixedBytes(valueInput) { var value = valueInput; // eslint-disable-line value = hexOrBuffer(value); if (value.length === 32) { return value; } var result = new Buffer(32); // eslint-disable-line result.fill(0); value.copy(result); return result; }, decode: function decodeFixedBytes(data, offset) { if (data.length !== 0 && data.length < offset + 32) { throw new Error('[ethjs-abi] while decoding fixed bytes, invalid bytes data length: ' + length); } return { consumed: 32, value: '0x' + data.slice(offset, offset + length).toString('hex') }; } }; } var coderAddress = { encode: function encodeAddress(valueInput) { var value = valueInput; // eslint-disable-line var result = new Buffer(32); // eslint-disable-line if (!isHexString(value, 20)) { throw new Error('[ethjs-abi] while encoding address, invalid address value, not alphanumeric 20 byte hex string'); } value = hexOrBuffer(value); result.fill(0); value.copy(result, 12); return result; }, decode: function decodeAddress(data, offset) { if (data.length === 0) { return { consumed: 32, value: '0x' }; } if (data.length !== 0 && data.length < offset + 32) { throw new Error('[ethjs-abi] while decoding address data, invalid address data, invalid byte length ' + data.length); } return { consumed: 32, value: '0x' + data.slice(offset + 12, offset + 32).toString('hex') }; } }; function encodeDynamicBytesHelper(value) { var dataLength = parseInt(32 * Math.ceil(value.length / 32)); // eslint-disable-line var padding = new Buffer(dataLength - value.length); // eslint-disable-line padding.fill(0); return Buffer.concat([uint256Coder.encode(value.length), value, padding]); } function decodeDynamicBytesHelper(data, offset) { if (data.length !== 0 && data.length < offset + 32) { throw new Error('[ethjs-abi] while decoding dynamic bytes data, invalid bytes length: ' + data.length + ' should be less than ' + (offset + 32)); } var length = uint256Coder.decode(data, offset).value; // eslint-disable-line length = length.toNumber(); if (data.length !== 0 && data.length < offset + 32 + length) { throw new Error('[ethjs-abi] while decoding dynamic bytes data, invalid bytes length: ' + data.length + ' should be less than ' + (offset + 32 + length)); } return { consumed: parseInt(32 + 32 * Math.ceil(length / 32), 10), value: data.slice(offset + 32, offset + 32 + length) }; } var coderDynamicBytes = { encode: function encodeDynamicBytes(value) { return encodeDynamicBytesHelper(hexOrBuffer(value)); }, decode: function decodeDynamicBytes(data, offset) { var result = decodeDynamicBytesHelper(data, offset); // eslint-disable-line result.value = '0x' + result.value.toString('hex'); return result; }, dynamic: true }; var coderString = { encode: function encodeString(value) { return encodeDynamicBytesHelper(new Buffer(value, 'utf8')); }, decode: function decodeString(data, offset) { var result = decodeDynamicBytesHelper(data, offset); // eslint-disable-line result.value = result.value.toString('utf8'); return result; }, dynamic: true }; function coderArray(coder, lengthInput) { return { encode: function encodeArray(value) { var result = new Buffer(0); // eslint-disable-line var length = lengthInput; // eslint-disable-line if (!Array.isArray(value)) { throw new Error('[ethjs-abi] while encoding array, invalid array data, not type Object (Array)'); } if (length === -1) { length = value.length; result = uint256Coder.encode(length); } if (length !== value.length) { throw new Error('[ethjs-abi] while encoding array, size mismatch array length ' + length + ' does not equal ' + value.length); } value.forEach(function (resultValue) { result = Buffer.concat([result, coder.encode(resultValue)]); }); return result; }, decode: function decodeArray(data, offsetInput) { var length = lengthInput; // eslint-disable-line var offset = offsetInput; // eslint-disable-line // @TODO: // if (data.length < offset + length * 32) { throw new Error('invalid array'); } var consumed = 0; // eslint-disable-line var decodeResult; // eslint-disable-line if (length === -1) { decodeResult = uint256Coder.decode(data, offset); length = decodeResult.value.toNumber(); consumed += decodeResult.consumed; offset += decodeResult.consumed; } var value = []; // eslint-disable-line for (var i = 0; i < length; i++) { // eslint-disable-line var loopResult = coder.decode(data, offset); consumed += loopResult.consumed; offset += loopResult.consumed; value.push(loopResult.value); } return { consumed: consumed, value: value }; }, dynamic: lengthInput === -1 }; } // Break the type up into [staticType][staticArray]*[dynamicArray]? | [dynamicType] and // build the coder up from its parts var paramTypePart = new RegExp(/^((u?int|bytes)([0-9]*)|(address|bool|string)|(\[([0-9]*)\]))/); function getParamCoder(typeInput) { var type = typeInput; // eslint-disable-line var coder = null; // eslint-disable-line var invalidTypeErrorMessage = '[ethjs-abi] while getting param coder (getParamCoder) type value ' + JSON.stringify(type) + ' is either invalid or unsupported by ethjs-abi.'; while (type) { var part = type.match(paramTypePart); // eslint-disable-line if (!part) { throw new Error(invalidTypeErrorMessage); } type = type.substring(part[0].length); var prefix = part[2] || part[4] || part[5]; // eslint-disable-line switch (prefix) { case 'int':case 'uint': if (coder) { throw new Error(invalidTypeErrorMessage); } var intSize = parseInt(part[3] || 256); // eslint-disable-line if (intSize === 0 || intSize > 256 || intSize % 8 !== 0) { throw new Error('[ethjs-abi] while getting param coder for type ' + type + ', invalid ' + prefix + '<N> width: ' + type); } coder = coderNumber(intSize / 8, prefix === 'int'); break; case 'bool': if (coder) { throw new Error(invalidTypeErrorMessage); } coder = coderBoolean; break; case 'string': if (coder) { throw new Error(invalidTypeErrorMessage); } coder = coderString; break; case 'bytes': if (coder) { throw new Error(invalidTypeErrorMessage); } if (part[3]) { var size = parseInt(part[3]); // eslint-disable-line if (size === 0 || size > 32) { throw new Error('[ethjs-abi] while getting param coder for prefix bytes, invalid type ' + type + ', size ' + size + ' should be 0 or greater than 32'); } coder = coderFixedBytes(size); } else { coder = coderDynamicBytes; } break; case 'address': if (coder) { throw new Error(invalidTypeErrorMessage); } coder = coderAddress; break; case '[]': if (!coder || coder.dynamic) { throw new Error(invalidTypeErrorMessage); } coder = coderArray(coder, -1); break; // "[0-9+]" default: if (!coder || coder.dynamic) { throw new Error(invalidTypeErrorMessage); } var defaultSize = parseInt(part[6]); // eslint-disable-line coder = coderArray(coder, defaultSize); } } if (!coder) { throw new Error(invalidTypeErrorMessage); } return coder; } module.exports = { BN: BN, bnToBuffer: bnToBuffer, isHexString: isHexString, hexOrBuffer: hexOrBuffer, hexlify: hexlify, stripZeros: stripZeros, keccak256: keccak256, getKeys: getKeys, numberToBN: numberToBN, coderNumber: coderNumber, uint256Coder: uint256Coder, coderBoolean: coderBoolean, coderFixedBytes: coderFixedBytes, coderAddress: coderAddress, coderDynamicBytes: coderDynamicBytes, coderString: coderString, coderArray: coderArray, paramTypePart: paramTypePart, getParamCoder: getParamCoder };