Abstracted out encoding/decoding of numeric values into its own utility. Could be useful elsewhere.
This commit is contained in:
parent
58a2dfbc4d
commit
d2d89adbdd
@ -4,17 +4,17 @@ import { DataTypeFactory, MemberDataType } from '../abstract_data_types';
|
|||||||
import * as Constants from '../utils/constants';
|
import * as Constants from '../utils/constants';
|
||||||
|
|
||||||
export class Array extends MemberDataType {
|
export class Array extends MemberDataType {
|
||||||
private static readonly _matcher = RegExp('^(.+)\\[([0-9]*)\\]$');
|
private static readonly _MATCHER = RegExp('^(.+)\\[([0-9]*)\\]$');
|
||||||
private readonly _arraySignature: string;
|
private readonly _arraySignature: string;
|
||||||
private readonly _elementType: string;
|
private readonly _elementType: string;
|
||||||
|
|
||||||
public static matchType(type: string): boolean {
|
public static matchType(type: string): boolean {
|
||||||
return Array._matcher.test(type);
|
return Array._MATCHER.test(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) {
|
public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) {
|
||||||
// Sanity check
|
// Sanity check
|
||||||
const matches = Array._matcher.exec(dataItem.type);
|
const matches = Array._MATCHER.exec(dataItem.type);
|
||||||
if (matches === null || matches.length !== 3) {
|
if (matches === null || matches.length !== 3) {
|
||||||
throw new Error(`Could not parse array: ${dataItem.type}`);
|
throw new Error(`Could not parse array: ${dataItem.type}`);
|
||||||
} else if (matches[1] === undefined) {
|
} else if (matches[1] === undefined) {
|
||||||
|
@ -7,16 +7,16 @@ import { DataTypeFactory } from '../abstract_data_types';
|
|||||||
import { Number } from './number';
|
import { Number } from './number';
|
||||||
|
|
||||||
export class Int extends Number {
|
export class Int extends Number {
|
||||||
private static readonly _matcher = RegExp(
|
private static readonly _MATCHER = RegExp(
|
||||||
'^int(8|16|24|32|40|48|56|64|72|88|96|104|112|120|128|136|144|152|160|168|176|184|192|200|208|216|224|232|240|248|256){0,1}$',
|
'^int(8|16|24|32|40|48|56|64|72|88|96|104|112|120|128|136|144|152|160|168|176|184|192|200|208|216|224|232|240|248|256){0,1}$',
|
||||||
);
|
);
|
||||||
|
|
||||||
public static matchType(type: string): boolean {
|
public static matchType(type: string): boolean {
|
||||||
return Int._matcher.test(type);
|
return Int._MATCHER.test(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) {
|
public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) {
|
||||||
super(dataItem, Int._matcher, dataTypeFactory);
|
super(dataItem, Int._MATCHER, dataTypeFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getMaxValue(): BigNumber {
|
public getMaxValue(): BigNumber {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { DataItem } from 'ethereum-types';
|
import { DataItem } from 'ethereum-types';
|
||||||
import * as ethUtil from 'ethereumjs-util';
|
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import { BigNumber } from '../../configured_bignumber';
|
import { BigNumber } from '../../configured_bignumber';
|
||||||
import { DataTypeFactory, PayloadDataType } from '../abstract_data_types';
|
import { DataTypeFactory, PayloadDataType } from '../abstract_data_types';
|
||||||
import { RawCalldata } from '../calldata';
|
import { RawCalldata } from '../calldata';
|
||||||
import * as Constants from '../utils/constants';
|
import * as Constants from '../utils/constants';
|
||||||
|
import * as EncoderMath from '../utils/math';
|
||||||
|
|
||||||
export abstract class Number extends PayloadDataType {
|
export abstract class Number extends PayloadDataType {
|
||||||
private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true;
|
private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true;
|
||||||
@ -25,73 +25,14 @@ export abstract class Number extends PayloadDataType {
|
|||||||
: (this._width = Number._DEFAULT_WIDTH);
|
: (this._width = Number._DEFAULT_WIDTH);
|
||||||
}
|
}
|
||||||
|
|
||||||
public encodeValue(value_: BigNumber | string | number): Buffer {
|
public encodeValue(value: BigNumber | string | number): Buffer {
|
||||||
const value = new BigNumber(value_, 10);
|
const encodedValue = EncoderMath.safeEncodeNumericValue(value, this.getMinValue(), this.getMaxValue());
|
||||||
if (value.greaterThan(this.getMaxValue())) {
|
return encodedValue;
|
||||||
throw new Error(`Tried to assign value of ${value}, which exceeds max value of ${this.getMaxValue()}`);
|
|
||||||
} else if (value.lessThan(this.getMinValue())) {
|
|
||||||
throw new Error(`Tried to assign value of ${value}, which exceeds min value of ${this.getMinValue()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let valueBuf: Buffer;
|
|
||||||
if (value.greaterThanOrEqualTo(0)) {
|
|
||||||
valueBuf = ethUtil.setLengthLeft(
|
|
||||||
ethUtil.toBuffer(`0x${value.toString(Constants.HEX_BASE)}`),
|
|
||||||
Constants.EVM_WORD_WIDTH_IN_BYTES,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// BigNumber can't write a negative hex value, so we use twos-complement conversion to do it ourselves.
|
|
||||||
// Step 1/3: Convert value to positive binary string
|
|
||||||
const binBase = 2;
|
|
||||||
const valueBin = value.times(-1).toString(binBase);
|
|
||||||
|
|
||||||
// Step 2/3: Invert binary value
|
|
||||||
let invertedValueBin = '1'.repeat(Constants.EVM_WORD_WIDTH_IN_BITS - valueBin.length);
|
|
||||||
_.each(valueBin, (bit: string) => {
|
|
||||||
invertedValueBin += bit === '1' ? '0' : '1';
|
|
||||||
});
|
|
||||||
const invertedValue = new BigNumber(invertedValueBin, binBase);
|
|
||||||
|
|
||||||
// Step 3/3: Add 1 to inverted value
|
|
||||||
// The result is the two's-complement represent of the input value.
|
|
||||||
const negativeValue = invertedValue.plus(1);
|
|
||||||
|
|
||||||
// Convert the negated value to a hex string
|
|
||||||
valueBuf = ethUtil.setLengthLeft(
|
|
||||||
ethUtil.toBuffer(`0x${negativeValue.toString(Constants.HEX_BASE)}`),
|
|
||||||
Constants.EVM_WORD_WIDTH_IN_BYTES,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return valueBuf;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public decodeValue(calldata: RawCalldata): BigNumber {
|
public decodeValue(calldata: RawCalldata): BigNumber {
|
||||||
const paddedValueBuf = calldata.popWord();
|
const valueBuf = calldata.popWord();
|
||||||
const paddedValueHex = ethUtil.bufferToHex(paddedValueBuf);
|
const value = EncoderMath.safeDecodeNumericValue(valueBuf, this.getMinValue(), this.getMaxValue());
|
||||||
let value = new BigNumber(paddedValueHex, 16);
|
|
||||||
if (this.getMinValue().lessThan(0)) {
|
|
||||||
// Check if we're negative
|
|
||||||
const valueBin = value.toString(Constants.BIN_BASE);
|
|
||||||
if (valueBin.length === Constants.EVM_WORD_WIDTH_IN_BITS && valueBin[0].startsWith('1')) {
|
|
||||||
// Negative
|
|
||||||
// Step 1/3: Invert binary value
|
|
||||||
let invertedValueBin = '';
|
|
||||||
_.each(valueBin, (bit: string) => {
|
|
||||||
invertedValueBin += bit === '1' ? '0' : '1';
|
|
||||||
});
|
|
||||||
const invertedValue = new BigNumber(invertedValueBin, Constants.BIN_BASE);
|
|
||||||
|
|
||||||
// Step 2/3: Add 1 to inverted value
|
|
||||||
// The result is the two's-complement represent of the input value.
|
|
||||||
const positiveValue = invertedValue.plus(1);
|
|
||||||
|
|
||||||
// Step 3/3: Invert positive value
|
|
||||||
const negativeValue = positiveValue.times(-1);
|
|
||||||
value = negativeValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,18 +8,18 @@ import * as Constants from '../utils/constants';
|
|||||||
|
|
||||||
export class StaticBytes extends PayloadDataType {
|
export class StaticBytes extends PayloadDataType {
|
||||||
private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true;
|
private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true;
|
||||||
private static readonly _matcher = RegExp(
|
private static readonly _MATCHER = RegExp(
|
||||||
'^(byte|bytes(1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32))$',
|
'^(byte|bytes(1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32))$',
|
||||||
);
|
);
|
||||||
private static readonly _DEFAULT_WIDTH = 1;
|
private static readonly _DEFAULT_WIDTH = 1;
|
||||||
private readonly _width: number;
|
private readonly _width: number;
|
||||||
|
|
||||||
public static matchType(type: string): boolean {
|
public static matchType(type: string): boolean {
|
||||||
return StaticBytes._matcher.test(type);
|
return StaticBytes._MATCHER.test(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static _decodeWidthFromType(type: string): number {
|
private static _decodeWidthFromType(type: string): number {
|
||||||
const matches = StaticBytes._matcher.exec(type);
|
const matches = StaticBytes._MATCHER.exec(type);
|
||||||
const width = (matches !== null && matches.length === 3 && matches[2] !== undefined)
|
const width = (matches !== null && matches.length === 3 && matches[2] !== undefined)
|
||||||
? parseInt(matches[2], Constants.DEC_BASE)
|
? parseInt(matches[2], Constants.DEC_BASE)
|
||||||
: StaticBytes._DEFAULT_WIDTH;
|
: StaticBytes._DEFAULT_WIDTH;
|
||||||
@ -55,7 +55,7 @@ export class StaticBytes extends PayloadDataType {
|
|||||||
this._sanityCheckValue(value);
|
this._sanityCheckValue(value);
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _sanityCheckValue(value: string | Buffer): void {
|
private _sanityCheckValue(value: string | Buffer): void {
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
if (!value.startsWith('0x')) {
|
if (!value.startsWith('0x')) {
|
||||||
|
@ -13,7 +13,7 @@ export class String extends PayloadDataType {
|
|||||||
public static matchType(type: string): boolean {
|
public static matchType(type: string): boolean {
|
||||||
return type === 'string';
|
return type === 'string';
|
||||||
}
|
}
|
||||||
|
|
||||||
public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) {
|
public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) {
|
||||||
super(dataItem, dataTypeFactory, String._SIZE_KNOWN_AT_COMPILE_TIME);
|
super(dataItem, dataTypeFactory, String._SIZE_KNOWN_AT_COMPILE_TIME);
|
||||||
if (!String.matchType(dataItem.type)) {
|
if (!String.matchType(dataItem.type)) {
|
||||||
|
@ -4,7 +4,7 @@ import { DataTypeFactory, MemberDataType } from '../abstract_data_types';
|
|||||||
|
|
||||||
export class Tuple extends MemberDataType {
|
export class Tuple extends MemberDataType {
|
||||||
private readonly _signature: string;
|
private readonly _signature: string;
|
||||||
|
|
||||||
public static matchType(type: string): boolean {
|
public static matchType(type: string): boolean {
|
||||||
return type === 'tuple';
|
return type === 'tuple';
|
||||||
}
|
}
|
||||||
|
@ -7,16 +7,16 @@ import { DataTypeFactory } from '../abstract_data_types';
|
|||||||
import { Number } from './number';
|
import { Number } from './number';
|
||||||
|
|
||||||
export class UInt extends Number {
|
export class UInt extends Number {
|
||||||
private static readonly _matcher = RegExp(
|
private static readonly _MATCHER = RegExp(
|
||||||
'^uint(8|16|24|32|40|48|56|64|72|88|96|104|112|120|128|136|144|152|160|168|176|184|192|200|208|216|224|232|240|248|256){0,1}$',
|
'^uint(8|16|24|32|40|48|56|64|72|88|96|104|112|120|128|136|144|152|160|168|176|184|192|200|208|216|224|232|240|248|256){0,1}$',
|
||||||
);
|
);
|
||||||
|
|
||||||
public static matchType(type: string): boolean {
|
public static matchType(type: string): boolean {
|
||||||
return UInt._matcher.test(type);
|
return UInt._MATCHER.test(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) {
|
public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) {
|
||||||
super(dataItem, UInt._matcher, dataTypeFactory);
|
super(dataItem, UInt._MATCHER, dataTypeFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getMaxValue(): BigNumber {
|
public getMaxValue(): BigNumber {
|
||||||
|
103
packages/utils/src/abi_encoder/utils/math.ts
Normal file
103
packages/utils/src/abi_encoder/utils/math.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
import * as ethUtil from 'ethereumjs-util';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
|
import * as Constants from '../utils/constants';
|
||||||
|
|
||||||
|
function sanityCheckBigNumberRange(value_: BigNumber | string | number, minValue: BigNumber, maxValue: BigNumber): void {
|
||||||
|
const value = new BigNumber(value_, 10);
|
||||||
|
if (value.greaterThan(maxValue)) {
|
||||||
|
throw new Error(`Tried to assign value of ${value}, which exceeds max value of ${maxValue}`);
|
||||||
|
} else if (value.lessThan(minValue)) {
|
||||||
|
throw new Error(`Tried to assign value of ${value}, which exceeds min value of ${minValue}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function bigNumberToPaddedBuffer(value: BigNumber): Buffer {
|
||||||
|
const valueHex = `0x${value.toString(Constants.HEX_BASE)}`;
|
||||||
|
const valueBuf = ethUtil.toBuffer(valueHex);
|
||||||
|
const valueBufPadded = ethUtil.setLengthLeft(valueBuf, Constants.EVM_WORD_WIDTH_IN_BYTES);
|
||||||
|
return valueBufPadded;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Takes a numeric value and returns its ABI-encoded value
|
||||||
|
* @param value_ The value to encode.
|
||||||
|
* @return ABI Encoded value
|
||||||
|
*/
|
||||||
|
export function encodeNumericValue(value_: BigNumber | string | number): Buffer {
|
||||||
|
const value = new BigNumber(value_, 10);
|
||||||
|
// Case 1/2: value is non-negative
|
||||||
|
if (value.greaterThanOrEqualTo(0)) {
|
||||||
|
const encodedPositiveValue = bigNumberToPaddedBuffer(value);
|
||||||
|
return encodedPositiveValue;
|
||||||
|
}
|
||||||
|
// Case 2/2: Value is negative
|
||||||
|
// Use two's-complement to encode the value
|
||||||
|
// Step 1/3: Convert negative value to positive binary string
|
||||||
|
const valueBin = value.times(-1).toString(Constants.BIN_BASE);
|
||||||
|
// Step 2/3: Invert binary value
|
||||||
|
let invertedValueBin = '1'.repeat(Constants.EVM_WORD_WIDTH_IN_BITS - valueBin.length);
|
||||||
|
_.each(valueBin, (bit: string) => {
|
||||||
|
invertedValueBin += bit === '1' ? '0' : '1';
|
||||||
|
});
|
||||||
|
const invertedValue = new BigNumber(invertedValueBin, Constants.BIN_BASE);
|
||||||
|
// Step 3/3: Add 1 to inverted value
|
||||||
|
const negativeValue = invertedValue.plus(1);
|
||||||
|
const encodedValue = bigNumberToPaddedBuffer(negativeValue);
|
||||||
|
return encodedValue;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Takes a numeric value and returns its ABI-encoded value.
|
||||||
|
* Performs an additional sanity check, given the min/max allowed value.
|
||||||
|
* @param value_ The value to encode.
|
||||||
|
* @return ABI Encoded value
|
||||||
|
*/
|
||||||
|
export function safeEncodeNumericValue(value: BigNumber | string | number, minValue: BigNumber, maxValue: BigNumber): Buffer {
|
||||||
|
sanityCheckBigNumberRange(value, minValue, maxValue);
|
||||||
|
const encodedValue = encodeNumericValue(value);
|
||||||
|
return encodedValue;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Takes an ABI-encoded numeric value and returns its decoded value as a BigNumber.
|
||||||
|
* @param encodedValue The encoded numeric value.
|
||||||
|
* @param minValue The minimum possible decoded value.
|
||||||
|
* @return ABI Decoded value
|
||||||
|
*/
|
||||||
|
export function decodeNumericValue(encodedValue: Buffer, minValue: BigNumber): BigNumber {
|
||||||
|
const valueHex = ethUtil.bufferToHex(encodedValue);
|
||||||
|
// Case 1/3: value is definitely non-negative because of numeric boundaries
|
||||||
|
const value = new BigNumber(valueHex, Constants.HEX_BASE);
|
||||||
|
if (!minValue.lessThan(0)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
// Case 2/3: value is non-negative because there is no leading 1 (encoded as two's-complement)
|
||||||
|
const valueBin = value.toString(Constants.BIN_BASE);
|
||||||
|
const valueIsNegative = valueBin.length === Constants.EVM_WORD_WIDTH_IN_BITS && valueBin[0].startsWith('1');
|
||||||
|
if (!valueIsNegative) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
// Case 3/3: value is negative
|
||||||
|
// Step 1/3: Invert b inary value
|
||||||
|
let invertedValueBin = '';
|
||||||
|
_.each(valueBin, (bit: string) => {
|
||||||
|
invertedValueBin += bit === '1' ? '0' : '1';
|
||||||
|
});
|
||||||
|
const invertedValue = new BigNumber(invertedValueBin, Constants.BIN_BASE);
|
||||||
|
// Step 2/3: Add 1 to inverted value
|
||||||
|
// The result is the two's-complement representation of the input value.
|
||||||
|
const positiveValue = invertedValue.plus(1);
|
||||||
|
// Step 3/3: Invert positive value to get the negative value
|
||||||
|
const negativeValue = positiveValue.times(-1);
|
||||||
|
return negativeValue;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Takes an ABI-encoded numeric value and returns its decoded value as a BigNumber.
|
||||||
|
* Performs an additional sanity check, given the min/max allowed value.
|
||||||
|
* @param encodedValue The encoded numeric value.
|
||||||
|
* @param minValue The minimum possible decoded value.
|
||||||
|
* @return ABI Decoded value
|
||||||
|
*/
|
||||||
|
export function safeDecodeNumericValue(encodedValue: Buffer, minValue: BigNumber, maxValue: BigNumber): BigNumber {
|
||||||
|
const value = decodeNumericValue(encodedValue, minValue);
|
||||||
|
sanityCheckBigNumberRange(value, minValue, maxValue);
|
||||||
|
return value;
|
||||||
|
}
|
@ -32,7 +32,7 @@ export class Queue<T> {
|
|||||||
public getStore(): T[] {
|
public getStore(): T[] {
|
||||||
return this._store;
|
return this._store;
|
||||||
}
|
}
|
||||||
|
|
||||||
public peekFront(): T | undefined {
|
public peekFront(): T | undefined {
|
||||||
return this._store.length >= 0 ? this._store[0] : undefined;
|
return this._store.length >= 0 ? this._store[0] : undefined;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user