ethjs-abi
Version:
Just the Ethereum encoding and decoding methods from the ethers-io-wallet.
417 lines (362 loc) • 12.5 kB
JavaScript
'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
};