UNPKG

ethjs-abi

Version:

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

380 lines (325 loc) 12 kB
const BN = require('bn.js'); const numberToBN = require('number-to-bn'); const 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)) { const 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)), }; }, }; } const uint256Coder = coderNumber(32, false); const 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')}`, }; }, }; } const 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), }; } const 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, }; const 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((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 const loopResult = coder.decode(data, offset); consumed += loopResult.consumed; offset += loopResult.consumed; value.push(loopResult.value); } return { consumed, value, }; }, dynamic: (lengthInput === -1), }; } // Break the type up into [staticType][staticArray]*[dynamicArray]? | [dynamicType] and // build the coder up from its parts const 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 const 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, bnToBuffer, isHexString, hexOrBuffer, hexlify, stripZeros, keccak256, getKeys, numberToBN, coderNumber, uint256Coder, coderBoolean, coderFixedBytes, coderAddress, coderDynamicBytes, coderString, coderArray, paramTypePart, getParamCoder, };