From e6ab6f38bacdec90c960ff1db4781d161b1f4103 Mon Sep 17 00:00:00 2001 From: Greg Hysen Date: Tue, 20 Nov 2018 12:58:49 -0800 Subject: Split EVM data types and factory into separate files --- packages/utils/src/abi_encoder/calldata.ts | 1 - packages/utils/src/abi_encoder/data_type.ts | 11 +- .../utils/src/abi_encoder/evm_data_type_factory.ts | 124 +++++ packages/utils/src/abi_encoder/evm_data_types.ts | 584 --------------------- .../src/abi_encoder/evm_data_types/address.ts | 51 ++ .../utils/src/abi_encoder/evm_data_types/array.ts | 55 ++ .../utils/src/abi_encoder/evm_data_types/bool.ts | 50 ++ .../abi_encoder/evm_data_types/dynamic_bytes.ts | 58 ++ .../utils/src/abi_encoder/evm_data_types/index.ts | 11 + .../utils/src/abi_encoder/evm_data_types/int.ts | 33 ++ .../utils/src/abi_encoder/evm_data_types/method.ts | 90 ++++ .../utils/src/abi_encoder/evm_data_types/number.ts | 100 ++++ .../src/abi_encoder/evm_data_types/pointer.ts | 15 + .../src/abi_encoder/evm_data_types/static_bytes.ts | 68 +++ .../utils/src/abi_encoder/evm_data_types/string.ts | 47 ++ .../utils/src/abi_encoder/evm_data_types/tuple.ts | 23 + .../utils/src/abi_encoder/evm_data_types/uint.ts | 33 ++ packages/utils/src/abi_encoder/index.ts | 2 +- 18 files changed, 769 insertions(+), 587 deletions(-) create mode 100644 packages/utils/src/abi_encoder/evm_data_type_factory.ts delete mode 100644 packages/utils/src/abi_encoder/evm_data_types.ts create mode 100644 packages/utils/src/abi_encoder/evm_data_types/address.ts create mode 100644 packages/utils/src/abi_encoder/evm_data_types/array.ts create mode 100644 packages/utils/src/abi_encoder/evm_data_types/bool.ts create mode 100644 packages/utils/src/abi_encoder/evm_data_types/dynamic_bytes.ts create mode 100644 packages/utils/src/abi_encoder/evm_data_types/index.ts create mode 100644 packages/utils/src/abi_encoder/evm_data_types/int.ts create mode 100644 packages/utils/src/abi_encoder/evm_data_types/method.ts create mode 100644 packages/utils/src/abi_encoder/evm_data_types/number.ts create mode 100644 packages/utils/src/abi_encoder/evm_data_types/pointer.ts create mode 100644 packages/utils/src/abi_encoder/evm_data_types/static_bytes.ts create mode 100644 packages/utils/src/abi_encoder/evm_data_types/string.ts create mode 100644 packages/utils/src/abi_encoder/evm_data_types/tuple.ts create mode 100644 packages/utils/src/abi_encoder/evm_data_types/uint.ts (limited to 'packages/utils') diff --git a/packages/utils/src/abi_encoder/calldata.ts b/packages/utils/src/abi_encoder/calldata.ts index 994b0fb81..d108ef0a7 100644 --- a/packages/utils/src/abi_encoder/calldata.ts +++ b/packages/utils/src/abi_encoder/calldata.ts @@ -1,4 +1,3 @@ - import * as ethUtil from 'ethereumjs-util'; import * as _ from 'lodash'; diff --git a/packages/utils/src/abi_encoder/data_type.ts b/packages/utils/src/abi_encoder/data_type.ts index ce7b11ef6..9890619e5 100644 --- a/packages/utils/src/abi_encoder/data_type.ts +++ b/packages/utils/src/abi_encoder/data_type.ts @@ -22,6 +22,12 @@ export interface DataTypeFactory { mapDataItemToDataType: (dataItem: DataItem) => DataType; } +export interface DataTypeStaticInterface { + matchType: (type: string) => boolean; + encodeValue: (value: any) => Buffer; + decodeValue: (rawCalldata: RawCalldata) => any; +} + export abstract class DataType { private static readonly _DEFAULT_ENCODING_RULES: EncodingRules = { optimize: false, annotate: false }; private static readonly _DEFAULT_DECODING_RULES: DecodingRules = { structsAsObjects: false }; @@ -250,7 +256,10 @@ export abstract class MemberDataType extends DataType { if (this._isArray && this._arrayLength === undefined) { [members] = this._createMembersWithLength(this.getDataItem(), value.length); - const lenBuf = ethUtil.setLengthLeft(ethUtil.toBuffer(`0x${value.length.toString(Constants.HEX_BASE)}`), Constants.EVM_WORD_WIDTH_IN_BYTES); + const lenBuf = ethUtil.setLengthLeft( + ethUtil.toBuffer(`0x${value.length.toString(Constants.HEX_BASE)}`), + Constants.EVM_WORD_WIDTH_IN_BYTES, + ); methodBlock.setHeader(lenBuf); } diff --git a/packages/utils/src/abi_encoder/evm_data_type_factory.ts b/packages/utils/src/abi_encoder/evm_data_type_factory.ts new file mode 100644 index 000000000..0f8dfb4a3 --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_type_factory.ts @@ -0,0 +1,124 @@ +/* tslint:disable prefer-function-over-method */ +/* tslint:disable max-classes-per-file */ +/* tslint:disable no-construct */ +import { DataItem, MethodAbi } from 'ethereum-types'; + +import { DataType, DataTypeFactory } from './data_type'; +import * as Impl from './evm_data_types'; + +export class Address extends Impl.Address { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class Bool extends Impl.Bool { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class Int extends Impl.Int { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class UInt extends Impl.UInt { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class StaticBytes extends Impl.StaticBytes { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class DynamicBytes extends Impl.DynamicBytes { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class String extends Impl.String { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class Pointer extends Impl.Pointer { + public constructor(destDataType: DataType, parentDataType: DataType) { + super(destDataType, parentDataType, EvmDataTypeFactory.getInstance()); + } +} + +export class Tuple extends Impl.Tuple { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class Array extends Impl.Array { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class Method extends Impl.Method { + public constructor(abi: MethodAbi) { + super(abi, EvmDataTypeFactory.getInstance()); + } +} + +export class EvmDataTypeFactory implements DataTypeFactory { + private static _instance: DataTypeFactory; + + public static getInstance(): DataTypeFactory { + if (!EvmDataTypeFactory._instance) { + EvmDataTypeFactory._instance = new EvmDataTypeFactory(); + } + return EvmDataTypeFactory._instance; + } + + public mapDataItemToDataType(dataItem: DataItem): DataType { + if (Array.matchType(dataItem.type)) { + return new Array(dataItem); + } else if (Address.matchType(dataItem.type)) { + return new Address(dataItem); + } else if (Bool.matchType(dataItem.type)) { + return new Bool(dataItem); + } else if (Int.matchType(dataItem.type)) { + return new Int(dataItem); + } else if (UInt.matchType(dataItem.type)) { + return new UInt(dataItem); + } else if (StaticBytes.matchType(dataItem.type)) { + return new StaticBytes(dataItem); + } else if (Tuple.matchType(dataItem.type)) { + return new Tuple(dataItem); + } else if (DynamicBytes.matchType(dataItem.type)) { + return new DynamicBytes(dataItem); + } else if (String.matchType(dataItem.type)) { + return new String(dataItem); + } + // @TODO: Implement Fixed/UFixed types + throw new Error(`Unrecognized data type: '${dataItem.type}'`); + } + + public create(dataItem: DataItem, parentDataType?: DataType): DataType { + const dataType = this.mapDataItemToDataType(dataItem); + if (dataType.isStatic()) { + return dataType; + } + + if (parentDataType === undefined) { + // @Todo -- will this work for return values? + throw new Error(`Trying to create a pointer`); + } + const pointer = new Pointer(dataType, parentDataType); + return pointer; + } + + private constructor() { } +} diff --git a/packages/utils/src/abi_encoder/evm_data_types.ts b/packages/utils/src/abi_encoder/evm_data_types.ts deleted file mode 100644 index 7ad7f403e..000000000 --- a/packages/utils/src/abi_encoder/evm_data_types.ts +++ /dev/null @@ -1,584 +0,0 @@ -import { DataItem, MethodAbi } from 'ethereum-types'; -import * as ethUtil from 'ethereumjs-util'; -import * as _ from 'lodash'; - -import { BigNumber } from '../configured_bignumber'; - -import { DecodingRules, EncodingRules, RawCalldata } from './calldata'; -import * as Constants from './constants'; -import { DataType, DataTypeFactory, DependentDataType, MemberDataType, PayloadDataType } from './data_type'; - -export interface DataTypeStaticInterface { - matchType: (type: string) => boolean; - encodeValue: (value: any) => Buffer; - decodeValue: (rawCalldata: RawCalldata) => any; -} - -export class Address extends PayloadDataType { - public static ERROR_MESSAGE_ADDRESS_MUST_START_WITH_0X = "Address must start with '0x'"; - public static ERROR_MESSAGE_ADDRESS_MUST_BE_20_BYTES = 'Address must be 20 bytes'; - private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true; - private static readonly _ADDRESS_SIZE_IN_BYTES = 20; - private static readonly _DECODED_ADDRESS_OFFSET_IN_BYTES = Constants.EVM_WORD_WIDTH_IN_BYTES - Address._ADDRESS_SIZE_IN_BYTES; - - public static matchType(type: string): boolean { - return type === 'address'; - } - - public constructor(dataItem: DataItem) { - super(dataItem, EvmDataTypeFactory.getInstance(), Address._SIZE_KNOWN_AT_COMPILE_TIME); - if (!Address.matchType(dataItem.type)) { - throw new Error(`Tried to instantiate Address with bad input: ${dataItem}`); - } - } - - public getSignature(): string { - return 'address'; - } - - public encodeValue(value: string): Buffer { - if (!value.startsWith('0x')) { - throw new Error(Address.ERROR_MESSAGE_ADDRESS_MUST_START_WITH_0X); - } - const valueAsBuffer = ethUtil.toBuffer(value); - if (valueAsBuffer.byteLength !== Address._ADDRESS_SIZE_IN_BYTES) { - throw new Error(Address.ERROR_MESSAGE_ADDRESS_MUST_BE_20_BYTES); - } - const encodedValueBuf = ethUtil.setLengthLeft(valueAsBuffer, Constants.EVM_WORD_WIDTH_IN_BYTES); - return encodedValueBuf; - } - - public decodeValue(calldata: RawCalldata): string { - const paddedValueBuf = calldata.popWord(); - const valueBuf = paddedValueBuf.slice(Address._DECODED_ADDRESS_OFFSET_IN_BYTES); - const value = ethUtil.bufferToHex(valueBuf); - return value; - } -} - -export class Bool extends PayloadDataType { - private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true; - - public static matchType(type: string): boolean { - return type === 'bool'; - } - - public constructor(dataItem: DataItem) { - super(dataItem, EvmDataTypeFactory.getInstance(), Bool._SIZE_KNOWN_AT_COMPILE_TIME); - if (!Bool.matchType(dataItem.type)) { - throw new Error(`Tried to instantiate Bool with bad input: ${dataItem}`); - } - } - - public getSignature(): string { - return 'bool'; - } - - public encodeValue(value: boolean): Buffer { - const encodedValue = value ? '0x1' : '0x0'; - const encodedValueBuf = ethUtil.setLengthLeft(ethUtil.toBuffer(encodedValue), Constants.EVM_WORD_WIDTH_IN_BYTES); - return encodedValueBuf; - } - - public decodeValue(calldata: RawCalldata): boolean { - const valueBuf = calldata.popWord(); - const valueHex = ethUtil.bufferToHex(valueBuf); - const valueNumber = new BigNumber(valueHex, 16); - if (!(valueNumber.equals(0) || valueNumber.equals(1))) { - throw new Error(`Failed to decode boolean. Expected 0x0 or 0x1, got ${valueHex}`); - } - /* tslint:disable boolean-naming */ - const value: boolean = valueNumber.equals(0) ? false : true; - /* tslint:enable boolean-naming */ - return value; - } -} - -abstract class Number extends PayloadDataType { - private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true; - private static readonly _MAX_WIDTH: number = 256; - private static readonly _DEFAULT_WIDTH: number = Number._MAX_WIDTH; - protected _width: number; - - constructor(dataItem: DataItem, matcher: RegExp) { - super(dataItem, EvmDataTypeFactory.getInstance(), Number._SIZE_KNOWN_AT_COMPILE_TIME); - const matches = matcher.exec(dataItem.type); - if (matches === null) { - throw new Error(`Tried to instantiate Number with bad input: ${dataItem}`); - } - this._width = (matches !== null && matches.length === 2 && matches[1] !== undefined) ? - parseInt(matches[1], Constants.DEC_BASE) : - this._width = Number._DEFAULT_WIDTH; - } - - public encodeValue(value_: BigNumber | string | number): Buffer { - const value = new BigNumber(value_, 10); - if (value.greaterThan(this.getMaxValue())) { - 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 { - const paddedValueBuf = calldata.popWord(); - const paddedValueHex = ethUtil.bufferToHex(paddedValueBuf); - let value = new BigNumber(paddedValueHex, 16); - if (this instanceof Int) { - // 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; - } - - public abstract getMaxValue(): BigNumber; - public abstract getMinValue(): BigNumber; -} - -export class Int extends Number { - 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}$', - ); - - public static matchType(type: string): boolean { - return Int._matcher.test(type); - } - - public constructor(dataItem: DataItem) { - super(dataItem, Int._matcher); - } - - public getMaxValue(): BigNumber { - return new BigNumber(2).toPower(this._width - 1).sub(1); - } - - public getMinValue(): BigNumber { - return new BigNumber(2).toPower(this._width - 1).times(-1); - } - - public getSignature(): string { - return `int${this._width}`; - } -} - -export class UInt extends Number { - 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}$', - ); - - public static matchType(type: string): boolean { - return UInt._matcher.test(type); - } - - public constructor(dataItem: DataItem) { - super(dataItem, UInt._matcher); - } - - public getMaxValue(): BigNumber { - return new BigNumber(2).toPower(this._width).sub(1); - } - - public getMinValue(): BigNumber { - return new BigNumber(0); - } - - public getSignature(): string { - return `uint${this._width}`; - } -} - -export class StaticBytes extends PayloadDataType { - private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true; - 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))$', - ); - - private static readonly _DEFAULT_WIDTH = 1; - private readonly _width: number; - - public static matchType(type: string): boolean { - return StaticBytes._matcher.test(type); - } - - public constructor(dataItem: DataItem) { - super(dataItem, EvmDataTypeFactory.getInstance(), StaticBytes._SIZE_KNOWN_AT_COMPILE_TIME); - const matches = StaticBytes._matcher.exec(dataItem.type); - if (!StaticBytes.matchType(dataItem.type)) { - throw new Error(`Tried to instantiate Byte with bad input: ${dataItem}`); - } - this._width = (matches !== null && matches.length === 3 && matches[2] !== undefined) ? parseInt(matches[2], Constants.DEC_BASE) : StaticBytes._DEFAULT_WIDTH; - } - - public getSignature(): string { - // Note that `byte` reduces to `bytes1` - return `bytes${this._width}`; - } - - public encodeValue(value: string | Buffer): Buffer { - // Sanity check if string - if (typeof value === 'string' && !value.startsWith('0x')) { - throw new Error(`Tried to encode non-hex value. Value must inlcude '0x' prefix.`); - } - // Convert value into a buffer and do bounds checking - const valueBuf = ethUtil.toBuffer(value); - if (valueBuf.byteLength > this._width) { - throw new Error( - `Tried to assign ${value} (${ - valueBuf.byteLength - } bytes), which exceeds max bytes that can be stored in a ${this.getSignature()}`, - ); - } else if (value.length % 2 !== 0) { - throw new Error(`Tried to assign ${value}, which is contains a half-byte. Use full bytes only.`); - } - - // Store value as hex - const evmWordWidth = 32; - const paddedValue = ethUtil.setLengthRight(valueBuf, evmWordWidth); - return paddedValue; - } - - public decodeValue(calldata: RawCalldata): string { - const paddedValueBuf = calldata.popWord(); - const valueBuf = paddedValueBuf.slice(0, this._width); - const value = ethUtil.bufferToHex(valueBuf); - return value; - } -} - -export class DynamicBytes extends PayloadDataType { - private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = false; - - public static matchType(type: string): boolean { - return type === 'bytes'; - } - - public constructor(dataItem: DataItem) { - super(dataItem, EvmDataTypeFactory.getInstance(), DynamicBytes._SIZE_KNOWN_AT_COMPILE_TIME); - if (!DynamicBytes.matchType(dataItem.type)) { - throw new Error(`Tried to instantiate DynamicBytes with bad input: ${dataItem}`); - } - } - - public encodeValue(value: string | Buffer): Buffer { - if (typeof value === 'string' && !value.startsWith('0x')) { - throw new Error(`Tried to encode non-hex value. Value must inlcude '0x' prefix. Got '${value}'`); - } - const valueBuf = ethUtil.toBuffer(value); - if (value.length % 2 !== 0) { - throw new Error(`Tried to assign ${value}, which is contains a half-byte. Use full bytes only.`); - } - - const wordsForValue = Math.ceil(valueBuf.byteLength / Constants.EVM_WORD_WIDTH_IN_BYTES); - const paddedDynamicBytesForValue = wordsForValue * Constants.EVM_WORD_WIDTH_IN_BYTES; - const paddedValueBuf = ethUtil.setLengthRight(valueBuf, paddedDynamicBytesForValue); - const paddedLengthBuf = ethUtil.setLengthLeft(ethUtil.toBuffer(valueBuf.byteLength), Constants.EVM_WORD_WIDTH_IN_BYTES); - const encodedValueBuf = Buffer.concat([paddedLengthBuf, paddedValueBuf]); - return encodedValueBuf; - } - - public decodeValue(calldata: RawCalldata): string { - const lengthBuf = calldata.popWord(); - const lengthHex = ethUtil.bufferToHex(lengthBuf); - const length = parseInt(lengthHex, Constants.HEX_BASE); - const wordsForValue = Math.ceil(length / Constants.EVM_WORD_WIDTH_IN_BYTES); - const paddedValueBuf = calldata.popWords(wordsForValue); - const valueBuf = paddedValueBuf.slice(0, length); - const decodedValue = ethUtil.bufferToHex(valueBuf); - return decodedValue; - } - - public getSignature(): string { - return 'bytes'; - } -} - -export class String extends PayloadDataType { - private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = false; - - public static matchType(type: string): boolean { - return type === 'string'; - } - - public constructor(dataItem: DataItem) { - super(dataItem, EvmDataTypeFactory.getInstance(), String._SIZE_KNOWN_AT_COMPILE_TIME); - if (!String.matchType(dataItem.type)) { - throw new Error(`Tried to instantiate String with bad input: ${dataItem}`); - } - } - - public encodeValue(value: string): Buffer { - const wordsForValue = Math.ceil(value.length / Constants.EVM_WORD_WIDTH_IN_BYTES); - const paddedDynamicBytesForValue = wordsForValue * Constants.EVM_WORD_WIDTH_IN_BYTES; - const valueBuf = ethUtil.setLengthRight(new Buffer(value), paddedDynamicBytesForValue); - const lengthBuf = ethUtil.setLengthLeft(ethUtil.toBuffer(value.length), Constants.EVM_WORD_WIDTH_IN_BYTES); - const encodedValueBuf = Buffer.concat([lengthBuf, valueBuf]); - return encodedValueBuf; - } - - public decodeValue(calldata: RawCalldata): string { - const lengthBuf = calldata.popWord(); - const lengthHex = ethUtil.bufferToHex(lengthBuf); - const length = parseInt(lengthHex, Constants.HEX_BASE); - const wordsForValue = Math.ceil(length / Constants.EVM_WORD_WIDTH_IN_BYTES); - const paddedValueBuf = calldata.popWords(wordsForValue); - const valueBuf = paddedValueBuf.slice(0, length); - const value = valueBuf.toString('ascii'); - return value; - } - - public getSignature(): string { - return 'string'; - } -} - -export class Pointer extends DependentDataType { - constructor(destDataType: DataType, parentDataType: DataType) { - const destDataItem = destDataType.getDataItem(); - const dataItem: DataItem = { name: `ptr<${destDataItem.name}>`, type: `ptr<${destDataItem.type}>` }; - super(dataItem, EvmDataTypeFactory.getInstance(), destDataType, parentDataType); - } - - public getSignature(): string { - return this._dependency.getSignature(); - } -} - -export class Tuple extends MemberDataType { - private readonly _tupleSignature: string; - - public static matchType(type: string): boolean { - return type === 'tuple'; - } - - public constructor(dataItem: DataItem) { - super(dataItem, EvmDataTypeFactory.getInstance()); - if (!Tuple.matchType(dataItem.type)) { - throw new Error(`Tried to instantiate Tuple with bad input: ${dataItem}`); - } - this._tupleSignature = this._computeSignatureOfMembers(); - } - - public getSignature(): string { - return this._tupleSignature; - } -} - -export class Array extends MemberDataType { - private static readonly _matcher = RegExp('^(.+)\\[([0-9]*)\\]$'); - private readonly _arraySignature: string; - private readonly _elementType: string; - - public static matchType(type: string): boolean { - return Array._matcher.test(type); - } - - public constructor(dataItem: DataItem) { - // Sanity check - const matches = Array._matcher.exec(dataItem.type); - if (matches === null || matches.length !== 3) { - throw new Error(`Could not parse array: ${dataItem.type}`); - } else if (matches[1] === undefined) { - throw new Error(`Could not parse array type: ${dataItem.type}`); - } else if (matches[2] === undefined) { - throw new Error(`Could not parse array length: ${dataItem.type}`); - } - - const isArray = true; - const arrayElementType = matches[1]; - const arrayLength = matches[2] === '' ? undefined : parseInt(matches[2], Constants.DEC_BASE); - super(dataItem, EvmDataTypeFactory.getInstance(), isArray, arrayLength, arrayElementType); - this._elementType = arrayElementType; - this._arraySignature = this._computeSignature(); - } - - public getSignature(): string { - return this._arraySignature; - } - - private _computeSignature(): string { - const dataItem: DataItem = { - type: this._elementType, - name: 'N/A', - }; - const components = this.getDataItem().components; - if (components !== undefined) { - dataItem.components = components; - } - const elementDataType = this.getFactory().mapDataItemToDataType(dataItem); - const type = elementDataType.getSignature(); - if (this._arrayLength === undefined) { - return `${type}[]`; - } else { - return `${type}[${this._arrayLength}]`; - } - } -} - -export class Method extends MemberDataType { - // TMP - public selector: string; - - private readonly _methodSignature: string; - private readonly _methodSelector: string; - private readonly _returnDataTypes: DataType[]; - private readonly _returnDataItem: DataItem; - - public constructor(abi: MethodAbi) { - super({ type: 'method', name: abi.name, components: abi.inputs }, EvmDataTypeFactory.getInstance()); - this._methodSignature = this._computeSignature(); - this.selector = this._methodSelector = this._computeSelector(); - this._returnDataTypes = []; - this._returnDataItem = { type: 'tuple', name: abi.name, components: abi.outputs }; - const dummy = new StaticBytes({ type: 'byte', name: 'DUMMY' }); // @TODO TMP - _.each(abi.outputs, (dataItem: DataItem) => { - this._returnDataTypes.push(this.getFactory().create(dataItem, dummy)); - }); - } - - public encode(value: any, rules?: EncodingRules): string { - const calldata = super.encode(value, rules, this.selector); - return calldata; - } - - public decode(calldata: string, rules?: DecodingRules): any[] | object { - if (!calldata.startsWith(this.selector)) { - throw new Error( - `Tried to decode calldata, but it was missing the function selector. Expected '${this.selector}'.`, - ); - } - const hasSelector = true; - const value = super.decode(calldata, rules, hasSelector); - return value; - } - - public encodeReturnValues(value: any, rules?: EncodingRules): string { - const returnDataType = new Tuple(this._returnDataItem); - const returndata = returnDataType.encode(value, rules); - return returndata; - } - - public decodeReturnValues(returndata: string, rules?: DecodingRules): any { - const returnValues: any[] = []; - const rules_: DecodingRules = rules ? rules : { structsAsObjects: false }; - const rawReturnData = new RawCalldata(returndata, false); - _.each(this._returnDataTypes, (dataType: DataType) => { - returnValues.push(dataType.generateValue(rawReturnData, rules_)); - }); - return returnValues; - } - - public getSignature(): string { - return this._methodSignature; - } - - public getSelector(): string { - return this._methodSelector; - } - - private _computeSignature(): string { - const memberSignature = this._computeSignatureOfMembers(); - const methodSignature = `${this.getDataItem().name}${memberSignature}`; - return methodSignature; - } - - private _computeSelector(): string { - const signature = this._computeSignature(); - const selector = ethUtil.bufferToHex(ethUtil.toBuffer(ethUtil.sha3(signature).slice(Constants.HEX_SELECTOR_BYTE_OFFSET_IN_CALLDATA, Constants.HEX_SELECTOR_LENGTH_IN_BYTES))); - return selector; - } -} - -export class EvmDataTypeFactory implements DataTypeFactory { - private static _instance: DataTypeFactory; - - public static getInstance(): DataTypeFactory { - if (!EvmDataTypeFactory._instance) { - EvmDataTypeFactory._instance = new EvmDataTypeFactory(); - } - return EvmDataTypeFactory._instance; - } - - public mapDataItemToDataType(dataItem: DataItem): DataType { - if (Array.matchType(dataItem.type)) { - return new Array(dataItem); - } else if (Address.matchType(dataItem.type)) { - return new Address(dataItem); - } else if (Bool.matchType(dataItem.type)) { - return new Bool(dataItem); - } else if (Int.matchType(dataItem.type)) { - return new Int(dataItem); - } else if (UInt.matchType(dataItem.type)) { - return new UInt(dataItem); - } else if (StaticBytes.matchType(dataItem.type)) { - return new StaticBytes(dataItem); - } else if (Tuple.matchType(dataItem.type)) { - return new Tuple(dataItem); - } else if (DynamicBytes.matchType(dataItem.type)) { - return new DynamicBytes(dataItem); - } else if (String.matchType(dataItem.type)) { - return new String(dataItem); - } - // @TODO: Implement Fixed/UFixed types - throw new Error(`Unrecognized data type: '${dataItem.type}'`); - } - - public create(dataItem: DataItem, parentDataType?: DataType): DataType { - const dataType = this.mapDataItemToDataType(dataItem); - if (dataType.isStatic()) { - return dataType; - } - - if (parentDataType === undefined) { - // @Todo -- will this work for return values? - throw new Error(`Trying to create a pointer`); - } - const pointer = new Pointer(dataType, parentDataType); - return pointer; - } - - private constructor() { } -} diff --git a/packages/utils/src/abi_encoder/evm_data_types/address.ts b/packages/utils/src/abi_encoder/evm_data_types/address.ts new file mode 100644 index 000000000..4bd992cab --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/address.ts @@ -0,0 +1,51 @@ +/* tslint:disable prefer-function-over-method */ +import { DataItem } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { RawCalldata } from '../calldata'; +import * as Constants from '../constants'; +import { DataTypeFactory, PayloadDataType } from '../data_type'; + +export class Address extends PayloadDataType { + public static ERROR_MESSAGE_ADDRESS_MUST_START_WITH_0X = "Address must start with '0x'"; + public static ERROR_MESSAGE_ADDRESS_MUST_BE_20_BYTES = 'Address must be 20 bytes'; + private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true; + private static readonly _ADDRESS_SIZE_IN_BYTES = 20; + private static readonly _DECODED_ADDRESS_OFFSET_IN_BYTES = Constants.EVM_WORD_WIDTH_IN_BYTES - + Address._ADDRESS_SIZE_IN_BYTES; + + public static matchType(type: string): boolean { + return type === 'address'; + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + super(dataItem, dataTypeFactory, Address._SIZE_KNOWN_AT_COMPILE_TIME); + if (!Address.matchType(dataItem.type)) { + throw new Error(`Tried to instantiate Address with bad input: ${dataItem}`); + } + } + + public getSignature(): string { + return 'address'; + } + + public encodeValue(value: string): Buffer { + if (!value.startsWith('0x')) { + throw new Error(Address.ERROR_MESSAGE_ADDRESS_MUST_START_WITH_0X); + } + const valueAsBuffer = ethUtil.toBuffer(value); + if (valueAsBuffer.byteLength !== Address._ADDRESS_SIZE_IN_BYTES) { + throw new Error(Address.ERROR_MESSAGE_ADDRESS_MUST_BE_20_BYTES); + } + const encodedValueBuf = ethUtil.setLengthLeft(valueAsBuffer, Constants.EVM_WORD_WIDTH_IN_BYTES); + return encodedValueBuf; + } + + public decodeValue(calldata: RawCalldata): string { + const paddedValueBuf = calldata.popWord(); + const valueBuf = paddedValueBuf.slice(Address._DECODED_ADDRESS_OFFSET_IN_BYTES); + const value = ethUtil.bufferToHex(valueBuf); + return value; + } +} diff --git a/packages/utils/src/abi_encoder/evm_data_types/array.ts b/packages/utils/src/abi_encoder/evm_data_types/array.ts new file mode 100644 index 000000000..707af7c7e --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/array.ts @@ -0,0 +1,55 @@ +import { DataItem } from 'ethereum-types'; + +import * as Constants from '../constants'; +import { DataTypeFactory, MemberDataType } from '../data_type'; + +export class Array extends MemberDataType { + private static readonly _matcher = RegExp('^(.+)\\[([0-9]*)\\]$'); + private readonly _arraySignature: string; + private readonly _elementType: string; + + public static matchType(type: string): boolean { + return Array._matcher.test(type); + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + // Sanity check + const matches = Array._matcher.exec(dataItem.type); + if (matches === null || matches.length !== 3) { + throw new Error(`Could not parse array: ${dataItem.type}`); + } else if (matches[1] === undefined) { + throw new Error(`Could not parse array type: ${dataItem.type}`); + } else if (matches[2] === undefined) { + throw new Error(`Could not parse array length: ${dataItem.type}`); + } + + const isArray = true; + const arrayElementType = matches[1]; + const arrayLength = matches[2] === '' ? undefined : parseInt(matches[2], Constants.DEC_BASE); + super(dataItem, dataTypeFactory, isArray, arrayLength, arrayElementType); + this._elementType = arrayElementType; + this._arraySignature = this._computeSignature(); + } + + public getSignature(): string { + return this._arraySignature; + } + + private _computeSignature(): string { + const dataItem: DataItem = { + type: this._elementType, + name: 'N/A', + }; + const components = this.getDataItem().components; + if (components !== undefined) { + dataItem.components = components; + } + const elementDataType = this.getFactory().mapDataItemToDataType(dataItem); + const type = elementDataType.getSignature(); + if (this._arrayLength === undefined) { + return `${type}[]`; + } else { + return `${type}[${this._arrayLength}]`; + } + } +} diff --git a/packages/utils/src/abi_encoder/evm_data_types/bool.ts b/packages/utils/src/abi_encoder/evm_data_types/bool.ts new file mode 100644 index 000000000..aee2727c7 --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/bool.ts @@ -0,0 +1,50 @@ +/* tslint:disable prefer-function-over-method */ +import { DataItem } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { BigNumber } from '../../configured_bignumber'; +import { RawCalldata } from '../calldata'; +import * as Constants from '../constants'; +import { DataTypeFactory, PayloadDataType } from '../data_type'; + +export class Bool extends PayloadDataType { + private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true; + + public static matchType(type: string): boolean { + return type === 'bool'; + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + super(dataItem, dataTypeFactory, Bool._SIZE_KNOWN_AT_COMPILE_TIME); + if (!Bool.matchType(dataItem.type)) { + throw new Error(`Tried to instantiate Bool with bad input: ${dataItem}`); + } + } + + public getSignature(): string { + return 'bool'; + } + + public encodeValue(value: boolean): Buffer { + const encodedValue = value ? '0x1' : '0x0'; + const encodedValueBuf = ethUtil.setLengthLeft( + ethUtil.toBuffer(encodedValue), + Constants.EVM_WORD_WIDTH_IN_BYTES, + ); + return encodedValueBuf; + } + + public decodeValue(calldata: RawCalldata): boolean { + const valueBuf = calldata.popWord(); + const valueHex = ethUtil.bufferToHex(valueBuf); + const valueNumber = new BigNumber(valueHex, Constants.HEX_BASE); + if (!(valueNumber.equals(0) || valueNumber.equals(1))) { + throw new Error(`Failed to decode boolean. Expected 0x0 or 0x1, got ${valueHex}`); + } + /* tslint:disable boolean-naming */ + const value: boolean = valueNumber.equals(0) ? false : true; + /* tslint:enable boolean-naming */ + return value; + } +} diff --git a/packages/utils/src/abi_encoder/evm_data_types/dynamic_bytes.ts b/packages/utils/src/abi_encoder/evm_data_types/dynamic_bytes.ts new file mode 100644 index 000000000..51165881a --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/dynamic_bytes.ts @@ -0,0 +1,58 @@ +/* tslint:disable prefer-function-over-method */ +import { DataItem } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { RawCalldata } from '../calldata'; +import * as Constants from '../constants'; +import { DataTypeFactory, PayloadDataType } from '../data_type'; + +export class DynamicBytes extends PayloadDataType { + private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = false; + + public static matchType(type: string): boolean { + return type === 'bytes'; + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + super(dataItem, dataTypeFactory, DynamicBytes._SIZE_KNOWN_AT_COMPILE_TIME); + if (!DynamicBytes.matchType(dataItem.type)) { + throw new Error(`Tried to instantiate DynamicBytes with bad input: ${dataItem}`); + } + } + + public encodeValue(value: string | Buffer): Buffer { + if (typeof value === 'string' && !value.startsWith('0x')) { + throw new Error(`Tried to encode non-hex value. Value must inlcude '0x' prefix. Got '${value}'`); + } + const valueBuf = ethUtil.toBuffer(value); + if (value.length % 2 !== 0) { + throw new Error(`Tried to assign ${value}, which is contains a half-byte. Use full bytes only.`); + } + + const wordsForValue = Math.ceil(valueBuf.byteLength / Constants.EVM_WORD_WIDTH_IN_BYTES); + const paddedDynamicBytesForValue = wordsForValue * Constants.EVM_WORD_WIDTH_IN_BYTES; + const paddedValueBuf = ethUtil.setLengthRight(valueBuf, paddedDynamicBytesForValue); + const paddedLengthBuf = ethUtil.setLengthLeft( + ethUtil.toBuffer(valueBuf.byteLength), + Constants.EVM_WORD_WIDTH_IN_BYTES, + ); + const encodedValueBuf = Buffer.concat([paddedLengthBuf, paddedValueBuf]); + return encodedValueBuf; + } + + public decodeValue(calldata: RawCalldata): string { + const lengthBuf = calldata.popWord(); + const lengthHex = ethUtil.bufferToHex(lengthBuf); + const length = parseInt(lengthHex, Constants.HEX_BASE); + const wordsForValue = Math.ceil(length / Constants.EVM_WORD_WIDTH_IN_BYTES); + const paddedValueBuf = calldata.popWords(wordsForValue); + const valueBuf = paddedValueBuf.slice(0, length); + const decodedValue = ethUtil.bufferToHex(valueBuf); + return decodedValue; + } + + public getSignature(): string { + return 'bytes'; + } +} diff --git a/packages/utils/src/abi_encoder/evm_data_types/index.ts b/packages/utils/src/abi_encoder/evm_data_types/index.ts new file mode 100644 index 000000000..fc0edabf1 --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/index.ts @@ -0,0 +1,11 @@ +export * from './address'; +export * from './bool'; +export * from './int'; +export * from './uint'; +export * from './static_bytes'; +export * from './dynamic_bytes'; +export * from './string'; +export * from './pointer'; +export * from './tuple'; +export * from './array'; +export * from './method'; diff --git a/packages/utils/src/abi_encoder/evm_data_types/int.ts b/packages/utils/src/abi_encoder/evm_data_types/int.ts new file mode 100644 index 000000000..ba5b4cac9 --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/int.ts @@ -0,0 +1,33 @@ +/* tslint:disable prefer-function-over-method */ +import { DataItem } from 'ethereum-types'; + +import { BigNumber } from '../../configured_bignumber'; +import { DataTypeFactory } from '../data_type'; + +import { Number } from './number'; + +export class Int extends Number { + 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}$', + ); + + public static matchType(type: string): boolean { + return Int._matcher.test(type); + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + super(dataItem, Int._matcher, dataTypeFactory); + } + + public getMaxValue(): BigNumber { + return new BigNumber(2).toPower(this._width - 1).sub(1); + } + + public getMinValue(): BigNumber { + return new BigNumber(2).toPower(this._width - 1).times(-1); + } + + public getSignature(): string { + return `int${this._width}`; + } +} diff --git a/packages/utils/src/abi_encoder/evm_data_types/method.ts b/packages/utils/src/abi_encoder/evm_data_types/method.ts new file mode 100644 index 000000000..e8e717bf1 --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/method.ts @@ -0,0 +1,90 @@ +import { DataItem, MethodAbi } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { DecodingRules, EncodingRules, RawCalldata } from '../calldata'; +import * as Constants from '../constants'; +import { DataType, DataTypeFactory, MemberDataType } from '../data_type'; + +import { StaticBytes } from './static_bytes'; +import { Tuple } from './tuple'; + +export class Method extends MemberDataType { + // TMP + public selector: string; + + private readonly _methodSignature: string; + private readonly _methodSelector: string; + private readonly _returnDataTypes: DataType[]; + private readonly _returnDataItem: DataItem; + + public constructor(abi: MethodAbi, dataTypeFactory: DataTypeFactory) { + super({ type: 'method', name: abi.name, components: abi.inputs }, dataTypeFactory); + this._methodSignature = this._computeSignature(); + this.selector = this._methodSelector = this._computeSelector(); + this._returnDataTypes = []; + this._returnDataItem = { type: 'tuple', name: abi.name, components: abi.outputs }; + const dummy = new StaticBytes({ type: 'byte', name: 'DUMMY' }, dataTypeFactory); // @TODO TMP + _.each(abi.outputs, (dataItem: DataItem) => { + this._returnDataTypes.push(this.getFactory().create(dataItem, dummy)); + }); + } + + public encode(value: any, rules?: EncodingRules): string { + const calldata = super.encode(value, rules, this.selector); + return calldata; + } + + public decode(calldata: string, rules?: DecodingRules): any[] | object { + if (!calldata.startsWith(this.selector)) { + throw new Error( + `Tried to decode calldata, but it was missing the function selector. Expected '${this.selector}'.`, + ); + } + const hasSelector = true; + const value = super.decode(calldata, rules, hasSelector); + return value; + } + + public encodeReturnValues(value: any, rules?: EncodingRules): string { + const returnDataType = new Tuple(this._returnDataItem, this.getFactory()); + const returndata = returnDataType.encode(value, rules); + return returndata; + } + + public decodeReturnValues(returndata: string, rules?: DecodingRules): any { + const returnValues: any[] = []; + const rules_: DecodingRules = rules ? rules : { structsAsObjects: false }; + const rawReturnData = new RawCalldata(returndata, false); + _.each(this._returnDataTypes, (dataType: DataType) => { + returnValues.push(dataType.generateValue(rawReturnData, rules_)); + }); + return returnValues; + } + + public getSignature(): string { + return this._methodSignature; + } + + public getSelector(): string { + return this._methodSelector; + } + + private _computeSignature(): string { + const memberSignature = this._computeSignatureOfMembers(); + const methodSignature = `${this.getDataItem().name}${memberSignature}`; + return methodSignature; + } + + private _computeSelector(): string { + const signature = this._computeSignature(); + const selector = ethUtil.bufferToHex( + ethUtil.toBuffer( + ethUtil + .sha3(signature) + .slice(Constants.HEX_SELECTOR_BYTE_OFFSET_IN_CALLDATA, Constants.HEX_SELECTOR_LENGTH_IN_BYTES), + ), + ); + return selector; + } +} diff --git a/packages/utils/src/abi_encoder/evm_data_types/number.ts b/packages/utils/src/abi_encoder/evm_data_types/number.ts new file mode 100644 index 000000000..17201362e --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/number.ts @@ -0,0 +1,100 @@ +import { DataItem } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { BigNumber } from '../../configured_bignumber'; +import { RawCalldata } from '../calldata'; +import * as Constants from '../constants'; +import { DataTypeFactory, PayloadDataType } from '../data_type'; + +export abstract class Number extends PayloadDataType { + private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true; + private static readonly _MAX_WIDTH: number = 256; + private static readonly _DEFAULT_WIDTH: number = Number._MAX_WIDTH; + protected _width: number; + + constructor(dataItem: DataItem, matcher: RegExp, dataTypeFactory: DataTypeFactory) { + super(dataItem, dataTypeFactory, Number._SIZE_KNOWN_AT_COMPILE_TIME); + const matches = matcher.exec(dataItem.type); + if (matches === null) { + throw new Error(`Tried to instantiate Number with bad input: ${dataItem}`); + } + this._width = + matches !== null && matches.length === 2 && matches[1] !== undefined + ? parseInt(matches[1], Constants.DEC_BASE) + : (this._width = Number._DEFAULT_WIDTH); + } + + public encodeValue(value_: BigNumber | string | number): Buffer { + const value = new BigNumber(value_, 10); + if (value.greaterThan(this.getMaxValue())) { + 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 { + const paddedValueBuf = calldata.popWord(); + const paddedValueHex = ethUtil.bufferToHex(paddedValueBuf); + 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; + } + + public abstract getMaxValue(): BigNumber; + public abstract getMinValue(): BigNumber; +} diff --git a/packages/utils/src/abi_encoder/evm_data_types/pointer.ts b/packages/utils/src/abi_encoder/evm_data_types/pointer.ts new file mode 100644 index 000000000..e0bd3509c --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/pointer.ts @@ -0,0 +1,15 @@ +import { DataItem } from 'ethereum-types'; + +import { DataType, DataTypeFactory, DependentDataType } from '../data_type'; + +export class Pointer extends DependentDataType { + constructor(destDataType: DataType, parentDataType: DataType, dataTypeFactory: DataTypeFactory) { + const destDataItem = destDataType.getDataItem(); + const dataItem: DataItem = { name: `ptr<${destDataItem.name}>`, type: `ptr<${destDataItem.type}>` }; + super(dataItem, dataTypeFactory, destDataType, parentDataType); + } + + public getSignature(): string { + return this._dependency.getSignature(); + } +} diff --git a/packages/utils/src/abi_encoder/evm_data_types/static_bytes.ts b/packages/utils/src/abi_encoder/evm_data_types/static_bytes.ts new file mode 100644 index 000000000..309dca234 --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/static_bytes.ts @@ -0,0 +1,68 @@ +import { DataItem } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { RawCalldata } from '../calldata'; +import * as Constants from '../constants'; +import { DataTypeFactory, PayloadDataType } from '../data_type'; + +export class StaticBytes extends PayloadDataType { + private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true; + 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))$', + ); + + private static readonly _DEFAULT_WIDTH = 1; + private readonly _width: number; + + public static matchType(type: string): boolean { + return StaticBytes._matcher.test(type); + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + super(dataItem, dataTypeFactory, StaticBytes._SIZE_KNOWN_AT_COMPILE_TIME); + const matches = StaticBytes._matcher.exec(dataItem.type); + if (!StaticBytes.matchType(dataItem.type)) { + throw new Error(`Tried to instantiate Byte with bad input: ${dataItem}`); + } + this._width = + matches !== null && matches.length === 3 && matches[2] !== undefined + ? parseInt(matches[2], Constants.DEC_BASE) + : StaticBytes._DEFAULT_WIDTH; + } + + public getSignature(): string { + // Note that `byte` reduces to `bytes1` + return `bytes${this._width}`; + } + + public encodeValue(value: string | Buffer): Buffer { + // Sanity check if string + if (typeof value === 'string' && !value.startsWith('0x')) { + throw new Error(`Tried to encode non-hex value. Value must inlcude '0x' prefix.`); + } + // Convert value into a buffer and do bounds checking + const valueBuf = ethUtil.toBuffer(value); + if (valueBuf.byteLength > this._width) { + throw new Error( + `Tried to assign ${value} (${ + valueBuf.byteLength + } bytes), which exceeds max bytes that can be stored in a ${this.getSignature()}`, + ); + } else if (value.length % 2 !== 0) { + throw new Error(`Tried to assign ${value}, which is contains a half-byte. Use full bytes only.`); + } + + // Store value as hex + const evmWordWidth = 32; + const paddedValue = ethUtil.setLengthRight(valueBuf, evmWordWidth); + return paddedValue; + } + + public decodeValue(calldata: RawCalldata): string { + const paddedValueBuf = calldata.popWord(); + const valueBuf = paddedValueBuf.slice(0, this._width); + const value = ethUtil.bufferToHex(valueBuf); + return value; + } +} diff --git a/packages/utils/src/abi_encoder/evm_data_types/string.ts b/packages/utils/src/abi_encoder/evm_data_types/string.ts new file mode 100644 index 000000000..96b36e735 --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/string.ts @@ -0,0 +1,47 @@ +/* tslint:disable prefer-function-over-method */ +import { DataItem } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { RawCalldata } from '../calldata'; +import * as Constants from '../constants'; +import { DataTypeFactory, PayloadDataType } from '../data_type'; + +export class String extends PayloadDataType { + private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = false; + + public static matchType(type: string): boolean { + return type === 'string'; + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + super(dataItem, dataTypeFactory, String._SIZE_KNOWN_AT_COMPILE_TIME); + if (!String.matchType(dataItem.type)) { + throw new Error(`Tried to instantiate String with bad input: ${dataItem}`); + } + } + + public encodeValue(value: string): Buffer { + const wordsForValue = Math.ceil(value.length / Constants.EVM_WORD_WIDTH_IN_BYTES); + const paddedDynamicBytesForValue = wordsForValue * Constants.EVM_WORD_WIDTH_IN_BYTES; + const valueBuf = ethUtil.setLengthRight(new Buffer(value), paddedDynamicBytesForValue); + const lengthBuf = ethUtil.setLengthLeft(ethUtil.toBuffer(value.length), Constants.EVM_WORD_WIDTH_IN_BYTES); + const encodedValueBuf = Buffer.concat([lengthBuf, valueBuf]); + return encodedValueBuf; + } + + public decodeValue(calldata: RawCalldata): string { + const lengthBuf = calldata.popWord(); + const lengthHex = ethUtil.bufferToHex(lengthBuf); + const length = parseInt(lengthHex, Constants.HEX_BASE); + const wordsForValue = Math.ceil(length / Constants.EVM_WORD_WIDTH_IN_BYTES); + const paddedValueBuf = calldata.popWords(wordsForValue); + const valueBuf = paddedValueBuf.slice(0, length); + const value = valueBuf.toString('ascii'); + return value; + } + + public getSignature(): string { + return 'string'; + } +} diff --git a/packages/utils/src/abi_encoder/evm_data_types/tuple.ts b/packages/utils/src/abi_encoder/evm_data_types/tuple.ts new file mode 100644 index 000000000..0db29c1eb --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/tuple.ts @@ -0,0 +1,23 @@ +import { DataItem } from 'ethereum-types'; + +import { DataTypeFactory, MemberDataType } from '../data_type'; + +export class Tuple extends MemberDataType { + private readonly _tupleSignature: string; + + public static matchType(type: string): boolean { + return type === 'tuple'; + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + super(dataItem, dataTypeFactory); + if (!Tuple.matchType(dataItem.type)) { + throw new Error(`Tried to instantiate Tuple with bad input: ${dataItem}`); + } + this._tupleSignature = this._computeSignatureOfMembers(); + } + + public getSignature(): string { + return this._tupleSignature; + } +} diff --git a/packages/utils/src/abi_encoder/evm_data_types/uint.ts b/packages/utils/src/abi_encoder/evm_data_types/uint.ts new file mode 100644 index 000000000..86b31ab4c --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/uint.ts @@ -0,0 +1,33 @@ +/* tslint:disable prefer-function-over-method */ +import { DataItem } from 'ethereum-types'; + +import { BigNumber } from '../../configured_bignumber'; +import { DataTypeFactory } from '../data_type'; + +import { Number } from './number'; + +export class UInt extends Number { + 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}$', + ); + + public static matchType(type: string): boolean { + return UInt._matcher.test(type); + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + super(dataItem, UInt._matcher, dataTypeFactory); + } + + public getMaxValue(): BigNumber { + return new BigNumber(2).toPower(this._width).sub(1); + } + + public getMinValue(): BigNumber { + return new BigNumber(0); + } + + public getSignature(): string { + return `uint${this._width}`; + } +} diff --git a/packages/utils/src/abi_encoder/index.ts b/packages/utils/src/abi_encoder/index.ts index 95ad84ac9..a62569fab 100644 --- a/packages/utils/src/abi_encoder/index.ts +++ b/packages/utils/src/abi_encoder/index.ts @@ -1,2 +1,2 @@ export { EncodingRules, DecodingRules } from './calldata'; -export * from './evm_data_types'; +export * from './evm_data_type_factory'; -- cgit