diff options
Diffstat (limited to 'packages/utils/src')
32 files changed, 1969 insertions, 2 deletions
diff --git a/packages/utils/src/abi_encoder/abstract_data_types/data_type.ts b/packages/utils/src/abi_encoder/abstract_data_types/data_type.ts new file mode 100644 index 000000000..13cc87e2a --- /dev/null +++ b/packages/utils/src/abi_encoder/abstract_data_types/data_type.ts @@ -0,0 +1,58 @@ +import { DataItem } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { Calldata } from '../calldata/calldata'; +import { CalldataBlock } from '../calldata/calldata_block'; +import { RawCalldata } from '../calldata/raw_calldata'; +import { constants } from '../utils/constants'; +import { DecodingRules, EncodingRules } from '../utils/rules'; + +import { DataTypeFactory } from './interfaces'; + +export abstract class DataType { + private readonly _dataItem: DataItem; + private readonly _factory: DataTypeFactory; + + constructor(dataItem: DataItem, factory: DataTypeFactory) { + this._dataItem = dataItem; + this._factory = factory; + } + + public getDataItem(): DataItem { + return this._dataItem; + } + + public getFactory(): DataTypeFactory { + return this._factory; + } + + public encode(value: any, rules?: EncodingRules, selector?: string): string { + const rules_ = _.isUndefined(rules) ? constants.DEFAULT_ENCODING_RULES : rules; + const calldata = new Calldata(rules_); + if (!_.isUndefined(selector)) { + calldata.setSelector(selector); + } + const block = this.generateCalldataBlock(value); + calldata.setRoot(block); + const encodedCalldata = calldata.toString(); + return encodedCalldata; + } + + public decode(calldata: string, rules?: DecodingRules, selector?: string): any { + if (!_.isUndefined(selector) && !_.startsWith(calldata, selector)) { + throw new Error( + `Tried to decode calldata, but it was missing the function selector. Expected prefix '${selector}'. Got '${calldata}'.`, + ); + } + const hasSelector = !_.isUndefined(selector); + const rawCalldata = new RawCalldata(calldata, hasSelector); + const rules_ = _.isUndefined(rules) ? constants.DEFAULT_DECODING_RULES : rules; + const value = this.generateValue(rawCalldata, rules_); + return value; + } + + public abstract generateCalldataBlock(value: any, parentBlock?: CalldataBlock): CalldataBlock; + public abstract generateValue(calldata: RawCalldata, rules: DecodingRules): any; + public abstract getSignature(): string; + public abstract isStatic(): boolean; +} diff --git a/packages/utils/src/abi_encoder/abstract_data_types/interfaces.ts b/packages/utils/src/abi_encoder/abstract_data_types/interfaces.ts new file mode 100644 index 000000000..2f2f60871 --- /dev/null +++ b/packages/utils/src/abi_encoder/abstract_data_types/interfaces.ts @@ -0,0 +1,19 @@ +import { DataItem } from 'ethereum-types'; + +import { RawCalldata } from '../calldata/raw_calldata'; + +import { DataType } from './data_type'; + +export interface DataTypeFactory { + create: (dataItem: DataItem, parentDataType?: DataType) => DataType; +} + +export interface DataTypeStaticInterface { + matchType: (type: string) => boolean; + encodeValue: (value: any) => Buffer; + decodeValue: (rawCalldata: RawCalldata) => any; +} + +export interface MemberIndexByName { + [key: string]: number; +} diff --git a/packages/utils/src/abi_encoder/abstract_data_types/types/blob.ts b/packages/utils/src/abi_encoder/abstract_data_types/types/blob.ts new file mode 100644 index 000000000..a091e55b9 --- /dev/null +++ b/packages/utils/src/abi_encoder/abstract_data_types/types/blob.ts @@ -0,0 +1,40 @@ +import { DataItem } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { BlobCalldataBlock } from '../../calldata/blocks/blob'; +import { CalldataBlock } from '../../calldata/calldata_block'; +import { RawCalldata } from '../../calldata/raw_calldata'; +import { DecodingRules } from '../../utils/rules'; + +import { DataType } from '../data_type'; +import { DataTypeFactory } from '../interfaces'; + +export abstract class AbstractBlobDataType extends DataType { + protected _sizeKnownAtCompileTime: boolean; + + public constructor(dataItem: DataItem, factory: DataTypeFactory, sizeKnownAtCompileTime: boolean) { + super(dataItem, factory); + this._sizeKnownAtCompileTime = sizeKnownAtCompileTime; + } + + public generateCalldataBlock(value: any, parentBlock?: CalldataBlock): BlobCalldataBlock { + const encodedValue = this.encodeValue(value); + const name = this.getDataItem().name; + const signature = this.getSignature(); + const parentName = _.isUndefined(parentBlock) ? '' : parentBlock.getName(); + const block = new BlobCalldataBlock(name, signature, parentName, encodedValue); + return block; + } + + public generateValue(calldata: RawCalldata, rules: DecodingRules): any { + const value = this.decodeValue(calldata); + return value; + } + + public isStatic(): boolean { + return this._sizeKnownAtCompileTime; + } + + public abstract encodeValue(value: any): Buffer; + public abstract decodeValue(calldata: RawCalldata): any; +} diff --git a/packages/utils/src/abi_encoder/abstract_data_types/types/pointer.ts b/packages/utils/src/abi_encoder/abstract_data_types/types/pointer.ts new file mode 100644 index 000000000..0f3c55280 --- /dev/null +++ b/packages/utils/src/abi_encoder/abstract_data_types/types/pointer.ts @@ -0,0 +1,54 @@ +import { DataItem } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { PointerCalldataBlock } from '../../calldata/blocks/pointer'; +import { CalldataBlock } from '../../calldata/calldata_block'; +import { RawCalldata } from '../../calldata/raw_calldata'; +import { constants } from '../../utils/constants'; +import { DecodingRules } from '../../utils/rules'; + +import { DataType } from '../data_type'; +import { DataTypeFactory } from '../interfaces'; + +export abstract class AbstractPointerDataType extends DataType { + protected _destination: DataType; + protected _parent: DataType; + + public constructor(dataItem: DataItem, factory: DataTypeFactory, destination: DataType, parent: DataType) { + super(dataItem, factory); + this._destination = destination; + this._parent = parent; + } + + public generateCalldataBlock(value: any, parentBlock?: CalldataBlock): PointerCalldataBlock { + if (_.isUndefined(parentBlock)) { + throw new Error(`DependentDataType requires a parent block to generate its block`); + } + const destinationBlock = this._destination.generateCalldataBlock(value, parentBlock); + const name = this.getDataItem().name; + const signature = this.getSignature(); + const parentName = parentBlock.getName(); + const block = new PointerCalldataBlock(name, signature, parentName, destinationBlock, parentBlock); + return block; + } + + public generateValue(calldata: RawCalldata, rules: DecodingRules): any { + const destinationOffsetBuf = calldata.popWord(); + const destinationOffsetHex = ethUtil.bufferToHex(destinationOffsetBuf); + const destinationOffsetRelative = parseInt(destinationOffsetHex, constants.HEX_BASE); + const destinationOffsetAbsolute = calldata.toAbsoluteOffset(destinationOffsetRelative); + const currentOffset = calldata.getOffset(); + calldata.setOffset(destinationOffsetAbsolute); + const value = this._destination.generateValue(calldata, rules); + calldata.setOffset(currentOffset); + return value; + } + + // Disable prefer-function-over-method for inherited abstract method. + /* tslint:disable prefer-function-over-method */ + public isStatic(): boolean { + return true; + } + /* tslint:enable prefer-function-over-method */ +} diff --git a/packages/utils/src/abi_encoder/abstract_data_types/types/set.ts b/packages/utils/src/abi_encoder/abstract_data_types/types/set.ts new file mode 100644 index 000000000..00059a4b6 --- /dev/null +++ b/packages/utils/src/abi_encoder/abstract_data_types/types/set.ts @@ -0,0 +1,218 @@ +import { DataItem } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { BigNumber } from '../../../configured_bignumber'; +import { SetCalldataBlock } from '../../calldata/blocks/set'; +import { CalldataBlock } from '../../calldata/calldata_block'; +import { RawCalldata } from '../../calldata/raw_calldata'; +import { constants } from '../../utils/constants'; +import { DecodingRules } from '../../utils/rules'; + +import { DataType } from '../data_type'; +import { DataTypeFactory, MemberIndexByName } from '../interfaces'; + +import { AbstractPointerDataType } from './pointer'; + +export abstract class AbstractSetDataType extends DataType { + protected readonly _arrayLength: number | undefined; + protected readonly _arrayElementType: string | undefined; + private readonly _memberIndexByName: MemberIndexByName; + private readonly _members: DataType[]; + private readonly _isArray: boolean; + + public constructor( + dataItem: DataItem, + factory: DataTypeFactory, + isArray: boolean = false, + arrayLength?: number, + arrayElementType?: string, + ) { + super(dataItem, factory); + this._memberIndexByName = {}; + this._members = []; + this._isArray = isArray; + this._arrayLength = arrayLength; + this._arrayElementType = arrayElementType; + if (isArray && !_.isUndefined(arrayLength)) { + [this._members, this._memberIndexByName] = this._createMembersWithLength(dataItem, arrayLength); + } else if (!isArray) { + [this._members, this._memberIndexByName] = this._createMembersWithKeys(dataItem); + } + } + + public generateCalldataBlock(value: any[] | object, parentBlock?: CalldataBlock): SetCalldataBlock { + const block = + value instanceof Array + ? this._generateCalldataBlockFromArray(value, parentBlock) + : this._generateCalldataBlockFromObject(value, parentBlock); + return block; + } + + public generateValue(calldata: RawCalldata, rules: DecodingRules): any[] | object { + let members = this._members; + // Case 1: This is an array of undefined length, which means that `this._members` was not + // populated in the constructor. So we must construct the set of members now. + if (this._isArray && _.isUndefined(this._arrayLength)) { + const arrayLengthBuf = calldata.popWord(); + const arrayLengthHex = ethUtil.bufferToHex(arrayLengthBuf); + const arrayLength = new BigNumber(arrayLengthHex, constants.HEX_BASE); + [members] = this._createMembersWithLength(this.getDataItem(), arrayLength.toNumber()); + } + // Create a new scope in the calldata, before descending into the members of this set. + calldata.startScope(); + let value: any[] | object; + if (rules.shouldConvertStructsToObjects && !this._isArray) { + // Construct an object with values for each member of the set. + value = {}; + _.each(this._memberIndexByName, (idx: number, key: string) => { + const member = this._members[idx]; + const memberValue = member.generateValue(calldata, rules); + (value as { [key: string]: any })[key] = memberValue; + }); + } else { + // Construct an array with values for each member of the set. + value = []; + _.each(members, (member: DataType, idx: number) => { + const memberValue = member.generateValue(calldata, rules); + (value as any[]).push(memberValue); + }); + } + // Close this scope and return tetheh value. + calldata.endScope(); + return value; + } + + public isStatic(): boolean { + // An array with an undefined length is never static. + if (this._isArray && _.isUndefined(this._arrayLength)) { + return false; + } + // If any member of the set is a pointer then the set is not static. + const dependentMember = _.find(this._members, (member: DataType) => { + return member instanceof AbstractPointerDataType; + }); + const isStatic = _.isUndefined(dependentMember); + return isStatic; + } + + protected _generateCalldataBlockFromArray(value: any[], parentBlock?: CalldataBlock): SetCalldataBlock { + // Sanity check: if the set has a defined length then `value` must have the same length. + if (!_.isUndefined(this._arrayLength) && value.length !== this._arrayLength) { + throw new Error( + `Expected array of ${JSON.stringify( + this._arrayLength, + )} elements, but got array of length ${JSON.stringify(value.length)}`, + ); + } + // Create a new calldata block for this set. + const parentName = _.isUndefined(parentBlock) ? '' : parentBlock.getName(); + const block = new SetCalldataBlock(this.getDataItem().name, this.getSignature(), parentName); + // If this set has an undefined length then set its header to be the number of elements. + let members = this._members; + if (this._isArray && _.isUndefined(this._arrayLength)) { + [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, + ); + block.setHeader(lenBuf); + } + // Create blocks for members of set. + const memberCalldataBlocks: CalldataBlock[] = []; + _.each(members, (member: DataType, idx: number) => { + const memberBlock = member.generateCalldataBlock(value[idx], block); + memberCalldataBlocks.push(memberBlock); + }); + block.setMembers(memberCalldataBlocks); + return block; + } + + protected _generateCalldataBlockFromObject(obj: object, parentBlock?: CalldataBlock): SetCalldataBlock { + // Create a new calldata block for this set. + const parentName = _.isUndefined(parentBlock) ? '' : parentBlock.getName(); + const block = new SetCalldataBlock(this.getDataItem().name, this.getSignature(), parentName); + // Create blocks for members of set. + const memberCalldataBlocks: CalldataBlock[] = []; + const childMap = _.cloneDeep(this._memberIndexByName); + _.forOwn(obj, (value: any, key: string) => { + if (!(key in childMap)) { + throw new Error( + `Could not assign tuple to object: unrecognized key '${key}' in object ${this.getDataItem().name}`, + ); + } + const memberBlock = this._members[this._memberIndexByName[key]].generateCalldataBlock(value, block); + memberCalldataBlocks.push(memberBlock); + delete childMap[key]; + }); + // Sanity check that all members have been included. + if (Object.keys(childMap).length !== 0) { + throw new Error(`Could not assign tuple to object: missing keys ${Object.keys(childMap)}`); + } + // Associate member blocks with Set block. + block.setMembers(memberCalldataBlocks); + return block; + } + + protected _computeSignatureOfMembers(): string { + // Compute signature of members + let signature = `(`; + _.each(this._members, (member: DataType, i: number) => { + signature += member.getSignature(); + if (i < this._members.length - 1) { + signature += ','; + } + }); + signature += ')'; + return signature; + } + + private _createMembersWithKeys(dataItem: DataItem): [DataType[], MemberIndexByName] { + // Sanity check + if (_.isUndefined(dataItem.components)) { + throw new Error( + `Tried to create a set using key/value pairs, but no components were defined by the input DataItem '${ + dataItem.name + }'.`, + ); + } + // Create one member for each component of `dataItem` + const members: DataType[] = []; + const memberIndexByName: MemberIndexByName = {}; + _.each(dataItem.components, (memberItem: DataItem) => { + const childDataItem: DataItem = { + type: memberItem.type, + name: `${dataItem.name}.${memberItem.name}`, + }; + const components = memberItem.components; + if (!_.isUndefined(components)) { + childDataItem.components = components; + } + const child = this.getFactory().create(childDataItem, this); + memberIndexByName[memberItem.name] = members.length; + members.push(child); + }); + return [members, memberIndexByName]; + } + + private _createMembersWithLength(dataItem: DataItem, length: number): [DataType[], MemberIndexByName] { + // Create `length` members, deriving the type from `dataItem` + const members: DataType[] = []; + const memberIndexByName: MemberIndexByName = {}; + const range = _.range(length); + _.each(range, (idx: number) => { + const memberDataItem: DataItem = { + type: _.isUndefined(this._arrayElementType) ? '' : this._arrayElementType, + name: `${dataItem.name}[${idx.toString(constants.DEC_BASE)}]`, + }; + const components = dataItem.components; + if (!_.isUndefined(components)) { + memberDataItem.components = components; + } + const memberType = this.getFactory().create(memberDataItem, this); + memberIndexByName[idx.toString(constants.DEC_BASE)] = members.length; + members.push(memberType); + }); + return [members, memberIndexByName]; + } +} diff --git a/packages/utils/src/abi_encoder/calldata/blocks/blob.ts b/packages/utils/src/abi_encoder/calldata/blocks/blob.ts new file mode 100644 index 000000000..219ea6c61 --- /dev/null +++ b/packages/utils/src/abi_encoder/calldata/blocks/blob.ts @@ -0,0 +1,20 @@ +import { CalldataBlock } from '../calldata_block'; + +export class BlobCalldataBlock extends CalldataBlock { + private readonly _blob: Buffer; + + constructor(name: string, signature: string, parentName: string, blob: Buffer) { + const headerSizeInBytes = 0; + const bodySizeInBytes = blob.byteLength; + super(name, signature, parentName, headerSizeInBytes, bodySizeInBytes); + this._blob = blob; + } + + public toBuffer(): Buffer { + return this._blob; + } + + public getRawData(): Buffer { + return this._blob; + } +} diff --git a/packages/utils/src/abi_encoder/calldata/blocks/pointer.ts b/packages/utils/src/abi_encoder/calldata/blocks/pointer.ts new file mode 100644 index 000000000..72d6a3173 --- /dev/null +++ b/packages/utils/src/abi_encoder/calldata/blocks/pointer.ts @@ -0,0 +1,61 @@ +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { constants } from '../../utils/constants'; + +import { CalldataBlock } from '../calldata_block'; + +export class PointerCalldataBlock extends CalldataBlock { + public static readonly RAW_DATA_START = new Buffer('<'); + public static readonly RAW_DATA_END = new Buffer('>'); + private static readonly _DEPENDENT_PAYLOAD_SIZE_IN_BYTES = 32; + private static readonly _EMPTY_HEADER_SIZE = 0; + private readonly _parent: CalldataBlock; + private readonly _dependency: CalldataBlock; + private _aliasFor: CalldataBlock | undefined; + + constructor(name: string, signature: string, parentName: string, dependency: CalldataBlock, parent: CalldataBlock) { + const headerSizeInBytes = PointerCalldataBlock._EMPTY_HEADER_SIZE; + const bodySizeInBytes = PointerCalldataBlock._DEPENDENT_PAYLOAD_SIZE_IN_BYTES; + super(name, signature, parentName, headerSizeInBytes, bodySizeInBytes); + this._parent = parent; + this._dependency = dependency; + this._aliasFor = undefined; + } + + public toBuffer(): Buffer { + const destinationOffset = !_.isUndefined(this._aliasFor) + ? this._aliasFor.getOffsetInBytes() + : this._dependency.getOffsetInBytes(); + const parentOffset = this._parent.getOffsetInBytes(); + const parentHeaderSize = this._parent.getHeaderSizeInBytes(); + const pointer: number = destinationOffset - (parentOffset + parentHeaderSize); + const pointerHex = `0x${pointer.toString(constants.HEX_BASE)}`; + const pointerBuf = ethUtil.toBuffer(pointerHex); + const pointerBufPadded = ethUtil.setLengthLeft(pointerBuf, constants.EVM_WORD_WIDTH_IN_BYTES); + return pointerBufPadded; + } + + public getDependency(): CalldataBlock { + return this._dependency; + } + + public setAlias(block: CalldataBlock): void { + this._aliasFor = block; + this._setName(`${this.getName()} (alias for ${block.getName()})`); + } + + public getAlias(): CalldataBlock | undefined { + return this._aliasFor; + } + + public getRawData(): Buffer { + const dependencyRawData = this._dependency.getRawData(); + const rawDataComponents: Buffer[] = []; + rawDataComponents.push(PointerCalldataBlock.RAW_DATA_START); + rawDataComponents.push(dependencyRawData); + rawDataComponents.push(PointerCalldataBlock.RAW_DATA_END); + const rawData = Buffer.concat(rawDataComponents); + return rawData; + } +} diff --git a/packages/utils/src/abi_encoder/calldata/blocks/set.ts b/packages/utils/src/abi_encoder/calldata/blocks/set.ts new file mode 100644 index 000000000..d1abc4986 --- /dev/null +++ b/packages/utils/src/abi_encoder/calldata/blocks/set.ts @@ -0,0 +1,47 @@ +import * as _ from 'lodash'; + +import { CalldataBlock } from '../calldata_block'; + +export class SetCalldataBlock extends CalldataBlock { + private _header: Buffer | undefined; + private _members: CalldataBlock[]; + + constructor(name: string, signature: string, parentName: string) { + super(name, signature, parentName, 0, 0); + this._members = []; + this._header = undefined; + } + + public getRawData(): Buffer { + const rawDataComponents: Buffer[] = []; + if (!_.isUndefined(this._header)) { + rawDataComponents.push(this._header); + } + _.each(this._members, (member: CalldataBlock) => { + const memberBuffer = member.getRawData(); + rawDataComponents.push(memberBuffer); + }); + const rawData = Buffer.concat(rawDataComponents); + return rawData; + } + + public setMembers(members: CalldataBlock[]): void { + this._members = members; + } + + public setHeader(header: Buffer): void { + this._setHeaderSize(header.byteLength); + this._header = header; + } + + public toBuffer(): Buffer { + if (!_.isUndefined(this._header)) { + return this._header; + } + return new Buffer(''); + } + + public getMembers(): CalldataBlock[] { + return this._members; + } +} diff --git a/packages/utils/src/abi_encoder/calldata/calldata.ts b/packages/utils/src/abi_encoder/calldata/calldata.ts new file mode 100644 index 000000000..b08fb71ce --- /dev/null +++ b/packages/utils/src/abi_encoder/calldata/calldata.ts @@ -0,0 +1,245 @@ +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { constants } from '../utils/constants'; +import { EncodingRules } from '../utils/rules'; + +import { PointerCalldataBlock } from './blocks/pointer'; +import { SetCalldataBlock } from './blocks/set'; +import { CalldataBlock } from './calldata_block'; +import { CalldataIterator, ReverseCalldataIterator } from './iterator'; + +export class Calldata { + private readonly _rules: EncodingRules; + private _selector: string; + private _root: CalldataBlock | undefined; + + public constructor(rules: EncodingRules) { + this._rules = rules; + this._selector = ''; + this._root = undefined; + } + /** + * Sets the root calldata block. This block usually corresponds to a Method. + */ + public setRoot(block: CalldataBlock): void { + this._root = block; + } + /** + * Sets the selector to be prepended onto the calldata. + * If the root block was created by a Method then a selector will likely be set. + */ + public setSelector(selector: string): void { + if (!_.startsWith(selector, '0x')) { + throw new Error(`Expected selector to be hex. Missing prefix '0x'`); + } else if (selector.length !== constants.HEX_SELECTOR_LENGTH_IN_CHARS) { + throw new Error(`Invalid selector '${selector}'`); + } + this._selector = selector; + } + /** + * Iterates through the calldata blocks, starting from the root block, to construct calldata as a hex string. + * If the `optimize` flag is set then this calldata will be condensed, to save gas. + * If the `annotate` flag is set then this will return human-readable calldata. + * If the `annotate` flag is *not* set then this will return EVM-compatible calldata. + */ + public toString(): string { + // Sanity check: root block must be set + if (_.isUndefined(this._root)) { + throw new Error('expected root'); + } + // Optimize, if flag set + if (this._rules.shouldOptimize) { + this._optimize(); + } + // Set offsets + const iterator = new CalldataIterator(this._root); + let offset = 0; + for (const block of iterator) { + block.setOffset(offset); + offset += block.getSizeInBytes(); + } + // Generate hex string + const hexString = this._rules.shouldAnnotate + ? this._toHumanReadableCallData() + : this._toEvmCompatibeCallDataHex(); + return hexString; + } + /** + * There are three types of calldata blocks: Blob, Set and Pointer. + * Scenarios arise where distinct pointers resolve to identical values. + * We optimize by keeping only one such instance of the identical value, and redirecting all pointers here. + * We keep the last such duplicate value because pointers can only be positive (they cannot point backwards). + * + * Example #1: + * function f(string[], string[]) + * f(["foo", "bar", "blitz"], ["foo", "bar", "blitz"]) + * The array ["foo", "bar", "blitz"] will only be included in the calldata once. + * + * Example #2: + * function f(string[], string) + * f(["foo", "bar", "blitz"], "foo") + * The string "foo" will only be included in the calldata once. + * + * Example #3: + * function f((string, uint, bytes), string, uint, bytes) + * f(("foo", 5, "0x05"), "foo", 5, "0x05") + * The string "foo" and bytes "0x05" will only be included in the calldata once. + * The duplicate `uint 5` values cannot be optimized out because they are static values (no pointer points to them). + * + * @TODO #1: + * This optimization strategy handles blocks that are exact duplicates of one another. + * But what if some block is a combination of two other blocks? Or a subset of another block? + * This optimization problem is not much different from the current implemetation. + * Instead of tracking "observed" hashes, at each node we would simply do pattern-matching on the calldata. + * This strategy would be applied after assigning offsets to the tree, rather than before (as in this strategy). + * Note that one consequence of this strategy is pointers may resolve to offsets that are not word-aligned. + * This shouldn't be a problem but further investigation should be done. + * + * @TODO #2: + * To be done as a follow-up to @TODO #1. + * Since we optimize from the bottom-up, we could be affecting the outcome of a later potential optimization. + * For example, what if by removing one duplicate value we miss out on optimizing another block higher in the tree. + * To handle this case, at each node we can store a candidate optimization in a priority queue (sorted by calldata size). + * At the end of traversing the tree, the candidate at the front of the queue will be the most optimal output. + * + */ + private _optimize(): void { + // Step 1/1 Create a reverse iterator (starts from the end of the calldata to the beginning) + if (_.isUndefined(this._root)) { + throw new Error('expected root'); + } + const iterator = new ReverseCalldataIterator(this._root); + // Step 2/2 Iterate over each block, keeping track of which blocks have been seen and pruning redundant blocks. + const blocksByHash: { [key: string]: CalldataBlock } = {}; + for (const block of iterator) { + // If a block is a pointer and its value has already been observed, then update + // the pointer to resolve to the existing value. + if (block instanceof PointerCalldataBlock) { + const dependencyBlockHashBuf = block.getDependency().computeHash(); + const dependencyBlockHash = ethUtil.bufferToHex(dependencyBlockHashBuf); + if (dependencyBlockHash in blocksByHash) { + const blockWithSameHash = blocksByHash[dependencyBlockHash]; + if (blockWithSameHash !== block.getDependency()) { + block.setAlias(blockWithSameHash); + } + } + continue; + } + // This block has not been seen. Record its hash. + const blockHashBuf = block.computeHash(); + const blockHash = ethUtil.bufferToHex(blockHashBuf); + if (!(blockHash in blocksByHash)) { + blocksByHash[blockHash] = block; + } + } + } + private _toEvmCompatibeCallDataHex(): string { + // Sanity check: must have a root block. + if (_.isUndefined(this._root)) { + throw new Error('expected root'); + } + // Construct an array of buffers (one buffer for each block). + const selectorBuffer = ethUtil.toBuffer(this._selector); + const valueBufs: Buffer[] = [selectorBuffer]; + const iterator = new CalldataIterator(this._root); + for (const block of iterator) { + valueBufs.push(block.toBuffer()); + } + // Create hex from buffer array. + const combinedBuffers = Buffer.concat(valueBufs); + const hexValue = ethUtil.bufferToHex(combinedBuffers); + return hexValue; + } + /** + * Returns human-readable calldata. + * + * Example: + * simpleFunction(string[], string[]) + * strings = ["Hello", "World"] + * simpleFunction(strings, strings) + * + * Output: + * 0xbb4f12e3 + * ### simpleFunction + * 0x0 0000000000000000000000000000000000000000000000000000000000000040 ptr<array1> (alias for array2) + * 0x20 0000000000000000000000000000000000000000000000000000000000000040 ptr<array2> + * + * 0x40 0000000000000000000000000000000000000000000000000000000000000002 ### array2 + * 0x60 0000000000000000000000000000000000000000000000000000000000000040 ptr<array2[0]> + * 0x80 0000000000000000000000000000000000000000000000000000000000000080 ptr<array2[1]> + * 0xa0 0000000000000000000000000000000000000000000000000000000000000005 array2[0] + * 0xc0 48656c6c6f000000000000000000000000000000000000000000000000000000 + * 0xe0 0000000000000000000000000000000000000000000000000000000000000005 array2[1] + * 0x100 576f726c64000000000000000000000000000000000000000000000000000000 + */ + private _toHumanReadableCallData(): string { + // Sanity check: must have a root block. + if (_.isUndefined(this._root)) { + throw new Error('expected root'); + } + // Constants for constructing annotated string + const offsetPadding = 10; + const valuePadding = 74; + const namePadding = 80; + const evmWordStartIndex = 0; + const emptySize = 0; + // Construct annotated calldata + let hexValue = `${this._selector}`; + let offset = 0; + const functionName: string = this._root.getName(); + const iterator = new CalldataIterator(this._root); + for (const block of iterator) { + // Process each block 1 word at a time + const size = block.getSizeInBytes(); + const name = block.getName(); + const parentName = block.getParentName(); + const prettyName = name.replace(`${parentName}.`, '').replace(`${functionName}.`, ''); + // Resulting line will be <offsetStr><valueStr><nameStr> + let offsetStr = ''; + let valueStr = ''; + let nameStr = ''; + let lineStr = ''; + if (size === emptySize) { + // This is a Set block with no header. + // For example, a tuple or an array with a defined length. + offsetStr = ' '.repeat(offsetPadding); + valueStr = ' '.repeat(valuePadding); + nameStr = `### ${prettyName.padEnd(namePadding)}`; + lineStr = `\n${offsetStr}${valueStr}${nameStr}`; + } else { + // This block has at least one word of value. + offsetStr = `0x${offset.toString(constants.HEX_BASE)}`.padEnd(offsetPadding); + valueStr = ethUtil + .stripHexPrefix( + ethUtil.bufferToHex( + block.toBuffer().slice(evmWordStartIndex, constants.EVM_WORD_WIDTH_IN_BYTES), + ), + ) + .padEnd(valuePadding); + if (block instanceof SetCalldataBlock) { + nameStr = `### ${prettyName.padEnd(namePadding)}`; + lineStr = `\n${offsetStr}${valueStr}${nameStr}`; + } else { + nameStr = ` ${prettyName.padEnd(namePadding)}`; + lineStr = `${offsetStr}${valueStr}${nameStr}`; + } + } + // This block has a value that is more than 1 word. + for (let j = constants.EVM_WORD_WIDTH_IN_BYTES; j < size; j += constants.EVM_WORD_WIDTH_IN_BYTES) { + offsetStr = `0x${(offset + j).toString(constants.HEX_BASE)}`.padEnd(offsetPadding); + valueStr = ethUtil + .stripHexPrefix( + ethUtil.bufferToHex(block.toBuffer().slice(j, j + constants.EVM_WORD_WIDTH_IN_BYTES)), + ) + .padEnd(valuePadding); + nameStr = ' '.repeat(namePadding); + lineStr = `${lineStr}\n${offsetStr}${valueStr}${nameStr}`; + } + // Append to hex value + hexValue = `${hexValue}\n${lineStr}`; + offset += size; + } + return hexValue; + } +} diff --git a/packages/utils/src/abi_encoder/calldata/calldata_block.ts b/packages/utils/src/abi_encoder/calldata/calldata_block.ts new file mode 100644 index 000000000..35bd994e5 --- /dev/null +++ b/packages/utils/src/abi_encoder/calldata/calldata_block.ts @@ -0,0 +1,77 @@ +import * as ethUtil from 'ethereumjs-util'; + +export abstract class CalldataBlock { + private readonly _signature: string; + private readonly _parentName: string; + private _name: string; + private _offsetInBytes: number; + private _headerSizeInBytes: number; + private _bodySizeInBytes: number; + + constructor( + name: string, + signature: string, + parentName: string, + headerSizeInBytes: number, + bodySizeInBytes: number, + ) { + this._name = name; + this._signature = signature; + this._parentName = parentName; + this._offsetInBytes = 0; + this._headerSizeInBytes = headerSizeInBytes; + this._bodySizeInBytes = bodySizeInBytes; + } + + protected _setHeaderSize(headerSizeInBytes: number): void { + this._headerSizeInBytes = headerSizeInBytes; + } + + protected _setBodySize(bodySizeInBytes: number): void { + this._bodySizeInBytes = bodySizeInBytes; + } + + protected _setName(name: string): void { + this._name = name; + } + + public getName(): string { + return this._name; + } + + public getParentName(): string { + return this._parentName; + } + + public getSignature(): string { + return this._signature; + } + public getHeaderSizeInBytes(): number { + return this._headerSizeInBytes; + } + + public getBodySizeInBytes(): number { + return this._bodySizeInBytes; + } + + public getSizeInBytes(): number { + return this.getHeaderSizeInBytes() + this.getBodySizeInBytes(); + } + + public getOffsetInBytes(): number { + return this._offsetInBytes; + } + + public setOffset(offsetInBytes: number): void { + this._offsetInBytes = offsetInBytes; + } + + public computeHash(): Buffer { + const rawData = this.getRawData(); + const hash = ethUtil.sha3(rawData); + return hash; + } + + public abstract toBuffer(): Buffer; + public abstract getRawData(): Buffer; +} diff --git a/packages/utils/src/abi_encoder/calldata/iterator.ts b/packages/utils/src/abi_encoder/calldata/iterator.ts new file mode 100644 index 000000000..333b32b4f --- /dev/null +++ b/packages/utils/src/abi_encoder/calldata/iterator.ts @@ -0,0 +1,114 @@ +/* tslint:disable max-classes-per-file */ +import * as _ from 'lodash'; + +import { Queue } from '../utils/queue'; + +import { BlobCalldataBlock } from './blocks/blob'; +import { PointerCalldataBlock } from './blocks/pointer'; +import { SetCalldataBlock } from './blocks/set'; +import { CalldataBlock } from './calldata_block'; + +/** + * Iterator class for Calldata Blocks. Blocks follows the order + * they should be put into calldata that is passed to he EVM. + * + * Example #1: + * Let root = Set { + * Blob{} A, + * Pointer { + * Blob{} a + * } B, + * Blob{} C + * } + * It will iterate as follows: [A, B, C, B.a] + * + * Example #2: + * Let root = Set { + * Blob{} A, + * Pointer { + * Blob{} a + * Pointer { + * Blob{} b + * } + * } B, + * Pointer { + * Blob{} c + * } C + * } + * It will iterate as follows: [A, B, C, B.a, B.b, C.c] + */ +abstract class BaseIterator implements Iterable<CalldataBlock> { + protected readonly _root: CalldataBlock; + protected readonly _queue: Queue<CalldataBlock>; + + private static _createQueue(block: CalldataBlock): Queue<CalldataBlock> { + const queue = new Queue<CalldataBlock>(); + // Base case + if (!(block instanceof SetCalldataBlock)) { + queue.pushBack(block); + return queue; + } + // This is a set; add members + const set = block; + _.eachRight(set.getMembers(), (member: CalldataBlock) => { + queue.mergeFront(BaseIterator._createQueue(member)); + }); + // Add children + _.each(set.getMembers(), (member: CalldataBlock) => { + // Traverse child if it is a unique pointer. + // A pointer that is an alias for another pointer is ignored. + if (member instanceof PointerCalldataBlock && _.isUndefined(member.getAlias())) { + const dependency = member.getDependency(); + queue.mergeBack(BaseIterator._createQueue(dependency)); + } + }); + // Put set block at the front of the queue + queue.pushFront(set); + return queue; + } + + public constructor(root: CalldataBlock) { + this._root = root; + this._queue = BaseIterator._createQueue(root); + } + + public [Symbol.iterator](): { next: () => IteratorResult<CalldataBlock> } { + return { + next: () => { + const nextBlock = this.nextBlock(); + if (!_.isUndefined(nextBlock)) { + return { + value: nextBlock, + done: false, + }; + } + return { + done: true, + value: new BlobCalldataBlock('', '', '', new Buffer('')), + }; + }, + }; + } + + public abstract nextBlock(): CalldataBlock | undefined; +} + +export class CalldataIterator extends BaseIterator { + public constructor(root: CalldataBlock) { + super(root); + } + + public nextBlock(): CalldataBlock | undefined { + return this._queue.popFront(); + } +} + +export class ReverseCalldataIterator extends BaseIterator { + public constructor(root: CalldataBlock) { + super(root); + } + + public nextBlock(): CalldataBlock | undefined { + return this._queue.popBack(); + } +} diff --git a/packages/utils/src/abi_encoder/calldata/raw_calldata.ts b/packages/utils/src/abi_encoder/calldata/raw_calldata.ts new file mode 100644 index 000000000..189841989 --- /dev/null +++ b/packages/utils/src/abi_encoder/calldata/raw_calldata.ts @@ -0,0 +1,82 @@ +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { constants } from '../utils/constants'; +import { Queue } from '../utils/queue'; + +export class RawCalldata { + private static readonly _INITIAL_OFFSET = 0; + private readonly _value: Buffer; + private readonly _selector: string; + private readonly _scopes: Queue<number>; + private _offset: number; + + public constructor(value: string | Buffer, hasSelector: boolean = true) { + // Sanity check + if (typeof value === 'string' && !_.startsWith(value, '0x')) { + throw new Error(`Expected raw calldata to start with '0x'`); + } + // Construct initial values + this._value = ethUtil.toBuffer(value); + this._selector = '0x'; + this._scopes = new Queue<number>(); + this._scopes.pushBack(RawCalldata._INITIAL_OFFSET); + this._offset = RawCalldata._INITIAL_OFFSET; + // If there's a selector then slice it + if (hasSelector) { + const selectorBuf = this._value.slice(constants.HEX_SELECTOR_LENGTH_IN_BYTES); + this._value = this._value.slice(constants.HEX_SELECTOR_LENGTH_IN_BYTES); + this._selector = ethUtil.bufferToHex(selectorBuf); + } + } + + public popBytes(lengthInBytes: number): Buffer { + const value = this._value.slice(this._offset, this._offset + lengthInBytes); + this.setOffset(this._offset + lengthInBytes); + return value; + } + + public popWord(): Buffer { + const wordInBytes = 32; + return this.popBytes(wordInBytes); + } + + public popWords(length: number): Buffer { + const wordInBytes = 32; + return this.popBytes(length * wordInBytes); + } + + public readBytes(from: number, to: number): Buffer { + const value = this._value.slice(from, to); + return value; + } + + public setOffset(offsetInBytes: number): void { + this._offset = offsetInBytes; + } + + public startScope(): void { + this._scopes.pushFront(this._offset); + } + + public endScope(): void { + this._scopes.popFront(); + } + + public getOffset(): number { + return this._offset; + } + + public toAbsoluteOffset(relativeOffset: number): number { + const scopeOffset = this._scopes.peekFront(); + if (_.isUndefined(scopeOffset)) { + throw new Error(`Tried to access undefined scope.`); + } + const absoluteOffset = relativeOffset + scopeOffset; + return absoluteOffset; + } + + public getSelector(): string { + return this._selector; + } +} 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..4cc124e0a --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_type_factory.ts @@ -0,0 +1,132 @@ +/* tslint:disable max-classes-per-file */ +import { DataItem, MethodAbi } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { DataType } from './abstract_data_types/data_type'; +import { DataTypeFactory } from './abstract_data_types/interfaces'; +import { AddressDataType } from './evm_data_types/address'; +import { ArrayDataType } from './evm_data_types/array'; +import { BoolDataType } from './evm_data_types/bool'; +import { DynamicBytesDataType } from './evm_data_types/dynamic_bytes'; +import { IntDataType } from './evm_data_types/int'; +import { MethodDataType } from './evm_data_types/method'; +import { PointerDataType } from './evm_data_types/pointer'; +import { StaticBytesDataType } from './evm_data_types/static_bytes'; +import { StringDataType } from './evm_data_types/string'; +import { TupleDataType } from './evm_data_types/tuple'; +import { UIntDataType } from './evm_data_types/uint'; + +export class Address extends AddressDataType { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class Bool extends BoolDataType { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class Int extends IntDataType { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class UInt extends UIntDataType { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class StaticBytes extends StaticBytesDataType { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class DynamicBytes extends DynamicBytesDataType { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class String extends StringDataType { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class Pointer extends PointerDataType { + public constructor(destDataType: DataType, parentDataType: DataType) { + super(destDataType, parentDataType, EvmDataTypeFactory.getInstance()); + } +} + +export class Tuple extends TupleDataType { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class Array extends ArrayDataType { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class Method extends MethodDataType { + public constructor(abi: MethodAbi) { + super(abi, EvmDataTypeFactory.getInstance()); + } +} + +/* tslint:disable no-construct */ +export class EvmDataTypeFactory implements DataTypeFactory { + private static _instance: DataTypeFactory; + + public static getInstance(): DataTypeFactory { + if (!EvmDataTypeFactory._instance) { + EvmDataTypeFactory._instance = new EvmDataTypeFactory(); + } + return EvmDataTypeFactory._instance; + } + + /* tslint:disable prefer-function-over-method */ + public create(dataItem: DataItem, parentDataType?: DataType): DataType { + // Create data type + let dataType: undefined | DataType; + if (Array.matchType(dataItem.type)) { + dataType = new Array(dataItem); + } else if (Address.matchType(dataItem.type)) { + dataType = new Address(dataItem); + } else if (Bool.matchType(dataItem.type)) { + dataType = new Bool(dataItem); + } else if (Int.matchType(dataItem.type)) { + dataType = new Int(dataItem); + } else if (UInt.matchType(dataItem.type)) { + dataType = new UInt(dataItem); + } else if (StaticBytes.matchType(dataItem.type)) { + dataType = new StaticBytes(dataItem); + } else if (Tuple.matchType(dataItem.type)) { + dataType = new Tuple(dataItem); + } else if (DynamicBytes.matchType(dataItem.type)) { + dataType = new DynamicBytes(dataItem); + } else if (String.matchType(dataItem.type)) { + dataType = new String(dataItem); + } + // @TODO: DataTypeement Fixed/UFixed types + if (_.isUndefined(dataType)) { + throw new Error(`Unrecognized data type: '${dataItem.type}'`); + } else if (!_.isUndefined(parentDataType) && !dataType.isStatic()) { + const pointerToDataType = new Pointer(dataType, parentDataType); + return pointerToDataType; + } + return dataType; + } + /* tslint:enable prefer-function-over-method */ + + private constructor() {} +} +/* tslint:enable no-construct */ 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..88846b1fa --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/address.ts @@ -0,0 +1,49 @@ +import { DataItem, SolidityTypes } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { DataTypeFactory } from '../abstract_data_types/interfaces'; +import { AbstractBlobDataType } from '../abstract_data_types/types/blob'; +import { RawCalldata } from '../calldata/raw_calldata'; +import { constants } from '../utils/constants'; + +export class AddressDataType extends AbstractBlobDataType { + 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 - + AddressDataType._ADDRESS_SIZE_IN_BYTES; + + public static matchType(type: string): boolean { + return type === SolidityTypes.Address; + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + super(dataItem, dataTypeFactory, AddressDataType._SIZE_KNOWN_AT_COMPILE_TIME); + if (!AddressDataType.matchType(dataItem.type)) { + throw new Error(`Tried to instantiate Address with bad input: ${dataItem}`); + } + } + + // Disable prefer-function-over-method for inherited abstract methods. + /* tslint:disable prefer-function-over-method */ + public encodeValue(value: string): Buffer { + if (!ethUtil.isValidAddress(value)) { + throw new Error(`Invalid address: '${value}'`); + } + const valueBuf = ethUtil.toBuffer(value); + const encodedValueBuf = ethUtil.setLengthLeft(valueBuf, constants.EVM_WORD_WIDTH_IN_BYTES); + return encodedValueBuf; + } + + public decodeValue(calldata: RawCalldata): string { + const valueBufPadded = calldata.popWord(); + const valueBuf = valueBufPadded.slice(AddressDataType._DECODED_ADDRESS_OFFSET_IN_BYTES); + const value = ethUtil.bufferToHex(valueBuf); + return value; + } + + public getSignature(): string { + return SolidityTypes.Address; + } + /* tslint:enable prefer-function-over-method */ +} 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..7595cb667 --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/array.ts @@ -0,0 +1,64 @@ +import { DataItem } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { DataTypeFactory } from '../abstract_data_types/interfaces'; +import { AbstractSetDataType } from '../abstract_data_types/types/set'; +import { constants } from '../utils/constants'; + +export class ArrayDataType extends AbstractSetDataType { + private static readonly _MATCHER = RegExp('^(.+)\\[([0-9]*)\\]$'); + private readonly _arraySignature: string; + private readonly _elementType: string; + + public static matchType(type: string): boolean { + return ArrayDataType._MATCHER.test(type); + } + + private static _decodeElementTypeAndLengthFromType(type: string): [string, undefined | number] { + const matches = ArrayDataType._MATCHER.exec(type); + if (_.isNull(matches) || matches.length !== 3) { + throw new Error(`Could not parse array: ${type}`); + } else if (_.isUndefined(matches[1])) { + throw new Error(`Could not parse array type: ${type}`); + } else if (_.isUndefined(matches[2])) { + throw new Error(`Could not parse array length: ${type}`); + } + const arrayElementType = matches[1]; + const arrayLength = _.isEmpty(matches[2]) ? undefined : parseInt(matches[2], constants.DEC_BASE); + return [arrayElementType, arrayLength]; + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + // Construct parent + const isArray = true; + const [arrayElementType, arrayLength] = ArrayDataType._decodeElementTypeAndLengthFromType(dataItem.type); + super(dataItem, dataTypeFactory, isArray, arrayLength, arrayElementType); + // Set array properties + this._elementType = arrayElementType; + this._arraySignature = this._computeSignature(); + } + + public getSignature(): string { + return this._arraySignature; + } + + private _computeSignature(): string { + // Compute signature for a single array element + const elementDataItem: DataItem = { + type: this._elementType, + name: 'N/A', + }; + const elementComponents = this.getDataItem().components; + if (!_.isUndefined(elementComponents)) { + elementDataItem.components = elementComponents; + } + const elementDataType = this.getFactory().create(elementDataItem); + const elementSignature = elementDataType.getSignature(); + // Construct signature for array of type `element` + if (_.isUndefined(this._arrayLength)) { + return `${elementSignature}[]`; + } else { + return `${elementSignature}[${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..d713d5a94 --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/bool.ts @@ -0,0 +1,53 @@ +import { DataItem, SolidityTypes } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { BigNumber } from '../../configured_bignumber'; +import { DataTypeFactory } from '../abstract_data_types/interfaces'; +import { AbstractBlobDataType } from '../abstract_data_types/types/blob'; +import { RawCalldata } from '../calldata/raw_calldata'; +import { constants } from '../utils/constants'; + +export class BoolDataType extends AbstractBlobDataType { + private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true; + + public static matchType(type: string): boolean { + return type === SolidityTypes.Bool; + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + super(dataItem, dataTypeFactory, BoolDataType._SIZE_KNOWN_AT_COMPILE_TIME); + if (!BoolDataType.matchType(dataItem.type)) { + throw new Error(`Tried to instantiate Bool with bad input: ${dataItem}`); + } + } + + // Disable prefer-function-over-method for inherited abstract methods. + /* tslint:disable prefer-function-over-method */ + 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); + /* tslint:enable boolean-naming */ + return value; + } + + public getSignature(): string { + return SolidityTypes.Bool; + } + /* tslint:enable prefer-function-over-method */ +} 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..5277efd6c --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/dynamic_bytes.ts @@ -0,0 +1,72 @@ +import { DataItem, SolidityTypes } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { DataTypeFactory } from '../abstract_data_types/interfaces'; +import { AbstractBlobDataType } from '../abstract_data_types/types/blob'; +import { RawCalldata } from '../calldata/raw_calldata'; +import { constants } from '../utils/constants'; + +export class DynamicBytesDataType extends AbstractBlobDataType { + private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = false; + + public static matchType(type: string): boolean { + return type === SolidityTypes.Bytes; + } + + private static _sanityCheckValue(value: string | Buffer): void { + if (typeof value !== 'string') { + return; + } + if (!_.startsWith(value, '0x')) { + throw new Error(`Tried to encode non-hex value. Value must inlcude '0x' prefix.`); + } else if (value.length % 2 !== 0) { + throw new Error(`Tried to assign ${value}, which is contains a half-byte. Use full bytes only.`); + } + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + super(dataItem, dataTypeFactory, DynamicBytesDataType._SIZE_KNOWN_AT_COMPILE_TIME); + if (!DynamicBytesDataType.matchType(dataItem.type)) { + throw new Error(`Tried to instantiate Dynamic Bytes with bad input: ${dataItem}`); + } + } + + // Disable prefer-function-over-method for inherited abstract methods. + /* tslint:disable prefer-function-over-method */ + public encodeValue(value: string | Buffer): Buffer { + // Encoded value is of the form: <length><value>, with each field padded to be word-aligned. + // 1/3 Construct the length + const valueBuf = ethUtil.toBuffer(value); + const wordsToStoreValuePadded = Math.ceil(valueBuf.byteLength / constants.EVM_WORD_WIDTH_IN_BYTES); + const bytesToStoreValuePadded = wordsToStoreValuePadded * constants.EVM_WORD_WIDTH_IN_BYTES; + const lengthBuf = ethUtil.toBuffer(valueBuf.byteLength); + const lengthBufPadded = ethUtil.setLengthLeft(lengthBuf, constants.EVM_WORD_WIDTH_IN_BYTES); + // 2/3 Construct the value + DynamicBytesDataType._sanityCheckValue(value); + const valueBufPadded = ethUtil.setLengthRight(valueBuf, bytesToStoreValuePadded); + // 3/3 Combine length and value + const encodedValue = Buffer.concat([lengthBufPadded, valueBufPadded]); + return encodedValue; + } + + public decodeValue(calldata: RawCalldata): string { + // Encoded value is of the form: <length><value>, with each field padded to be word-aligned. + // 1/2 Decode length + const lengthBuf = calldata.popWord(); + const lengthHex = ethUtil.bufferToHex(lengthBuf); + const length = parseInt(lengthHex, constants.HEX_BASE); + // 2/2 Decode value + const wordsToStoreValuePadded = Math.ceil(length / constants.EVM_WORD_WIDTH_IN_BYTES); + const valueBufPadded = calldata.popWords(wordsToStoreValuePadded); + const valueBuf = valueBufPadded.slice(0, length); + const value = ethUtil.bufferToHex(valueBuf); + DynamicBytesDataType._sanityCheckValue(value); + return value; + } + + public getSignature(): string { + return SolidityTypes.Bytes; + } + /* tslint:enable prefer-function-over-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..f1dcf5ea1 --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/int.ts @@ -0,0 +1,59 @@ +import { DataItem, SolidityTypes } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { BigNumber } from '../../configured_bignumber'; +import { DataTypeFactory } from '../abstract_data_types/interfaces'; +import { AbstractBlobDataType } from '../abstract_data_types/types/blob'; +import { RawCalldata } from '../calldata/raw_calldata'; +import { constants } from '../utils/constants'; +import * as EncoderMath from '../utils/math'; + +export class IntDataType extends AbstractBlobDataType { + 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}$', + ); + private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true; + private static readonly _MAX_WIDTH: number = 256; + private static readonly _DEFAULT_WIDTH: number = IntDataType._MAX_WIDTH; + private readonly _width: number; + private readonly _minValue: BigNumber; + private readonly _maxValue: BigNumber; + + public static matchType(type: string): boolean { + return IntDataType._MATCHER.test(type); + } + + private static _decodeWidthFromType(type: string): number { + const matches = IntDataType._MATCHER.exec(type); + const width = + !_.isNull(matches) && matches.length === 2 && !_.isUndefined(matches[1]) + ? parseInt(matches[1], constants.DEC_BASE) + : IntDataType._DEFAULT_WIDTH; + return width; + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + super(dataItem, dataTypeFactory, IntDataType._SIZE_KNOWN_AT_COMPILE_TIME); + if (!IntDataType.matchType(dataItem.type)) { + throw new Error(`Tried to instantiate Int with bad input: ${dataItem}`); + } + this._width = IntDataType._decodeWidthFromType(dataItem.type); + this._minValue = new BigNumber(2).toPower(this._width - 1).times(-1); + this._maxValue = new BigNumber(2).toPower(this._width - 1).sub(1); + } + + public encodeValue(value: BigNumber | string | number): Buffer { + const encodedValue = EncoderMath.safeEncodeNumericValue(value, this._minValue, this._maxValue); + return encodedValue; + } + + public decodeValue(calldata: RawCalldata): BigNumber { + const valueBuf = calldata.popWord(); + const value = EncoderMath.safeDecodeNumericValue(valueBuf, this._minValue, this._maxValue); + return value; + } + + public getSignature(): string { + return `${SolidityTypes.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..b1cd1377f --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/method.ts @@ -0,0 +1,72 @@ +import { DataItem, MethodAbi } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { DataType } from '../abstract_data_types/data_type'; +import { DataTypeFactory } from '../abstract_data_types/interfaces'; +import { AbstractSetDataType } from '../abstract_data_types/types/set'; +import { constants } from '../utils/constants'; +import { DecodingRules, EncodingRules } from '../utils/rules'; + +import { TupleDataType } from './tuple'; + +export class MethodDataType extends AbstractSetDataType { + private readonly _methodSignature: string; + private readonly _methodSelector: string; + private readonly _returnDataType: DataType; + + public constructor(abi: MethodAbi, dataTypeFactory: DataTypeFactory) { + const methodDataItem = { type: 'method', name: abi.name, components: abi.inputs }; + super(methodDataItem, dataTypeFactory); + this._methodSignature = this._computeSignature(); + this._methodSelector = this._computeSelector(); + const returnDataItem: DataItem = { type: 'tuple', name: abi.name, components: abi.outputs }; + this._returnDataType = new TupleDataType(returnDataItem, this.getFactory()); + } + + public encode(value: any, rules?: EncodingRules): string { + const calldata = super.encode(value, rules, this._methodSelector); + return calldata; + } + + public decode(calldata: string, rules?: DecodingRules): any[] | object { + const value = super.decode(calldata, rules, this._methodSelector); + return value; + } + + public encodeReturnValues(value: any, rules?: EncodingRules): string { + const returnData = this._returnDataType.encode(value, rules); + return returnData; + } + + public decodeReturnValues(returndata: string, rules?: DecodingRules): any { + const returnValues = this._returnDataType.decode(returndata, 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/pointer.ts b/packages/utils/src/abi_encoder/evm_data_types/pointer.ts new file mode 100644 index 000000000..389e75927 --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/pointer.ts @@ -0,0 +1,17 @@ +import { DataItem } from 'ethereum-types'; + +import { DataType } from '../abstract_data_types/data_type'; +import { DataTypeFactory } from '../abstract_data_types/interfaces'; +import { AbstractPointerDataType } from '../abstract_data_types/types/pointer'; + +export class PointerDataType extends AbstractPointerDataType { + 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._destination.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..2e371c505 --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/static_bytes.ts @@ -0,0 +1,78 @@ +import { DataItem, SolidityTypes } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { DataTypeFactory } from '../abstract_data_types/interfaces'; +import { AbstractBlobDataType } from '../abstract_data_types/types/blob'; +import { RawCalldata } from '../calldata/raw_calldata'; +import { constants } from '../utils/constants'; + +export class StaticBytesDataType extends AbstractBlobDataType { + 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 StaticBytesDataType._MATCHER.test(type); + } + + private static _decodeWidthFromType(type: string): number { + const matches = StaticBytesDataType._MATCHER.exec(type); + const width = + !_.isNull(matches) && matches.length === 3 && !_.isUndefined(matches[2]) + ? parseInt(matches[2], constants.DEC_BASE) + : StaticBytesDataType._DEFAULT_WIDTH; + return width; + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + super(dataItem, dataTypeFactory, StaticBytesDataType._SIZE_KNOWN_AT_COMPILE_TIME); + if (!StaticBytesDataType.matchType(dataItem.type)) { + throw new Error(`Tried to instantiate Static Bytes with bad input: ${dataItem}`); + } + this._width = StaticBytesDataType._decodeWidthFromType(dataItem.type); + } + + public getSignature(): string { + // Note that `byte` reduces to `bytes1` + return `${SolidityTypes.Bytes}${this._width}`; + } + + public encodeValue(value: string | Buffer): Buffer { + // 1/2 Convert value into a buffer and do bounds checking + this._sanityCheckValue(value); + const valueBuf = ethUtil.toBuffer(value); + // 2/2 Store value as hex + const valuePadded = ethUtil.setLengthRight(valueBuf, constants.EVM_WORD_WIDTH_IN_BYTES); + return valuePadded; + } + + public decodeValue(calldata: RawCalldata): string { + const valueBufPadded = calldata.popWord(); + const valueBuf = valueBufPadded.slice(0, this._width); + const value = ethUtil.bufferToHex(valueBuf); + this._sanityCheckValue(value); + return value; + } + + private _sanityCheckValue(value: string | Buffer): void { + if (typeof value === 'string') { + if (!_.startsWith(value, '0x')) { + throw new Error(`Tried to encode non-hex value. Value must inlcude '0x' prefix.`); + } else if (value.length % 2 !== 0) { + throw new Error(`Tried to assign ${value}, which is contains a half-byte. Use full bytes only.`); + } + } + 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()}`, + ); + } + } +} 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..91a72ad3f --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/string.ts @@ -0,0 +1,59 @@ +import { DataItem, SolidityTypes } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { DataTypeFactory } from '../abstract_data_types/interfaces'; +import { AbstractBlobDataType } from '../abstract_data_types/types/blob'; +import { RawCalldata } from '../calldata/raw_calldata'; +import { constants } from '../utils/constants'; + +export class StringDataType extends AbstractBlobDataType { + private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = false; + + public static matchType(type: string): boolean { + return type === SolidityTypes.String; + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + super(dataItem, dataTypeFactory, StringDataType._SIZE_KNOWN_AT_COMPILE_TIME); + if (!StringDataType.matchType(dataItem.type)) { + throw new Error(`Tried to instantiate String with bad input: ${dataItem}`); + } + } + + // Disable prefer-function-over-method for inherited abstract methods. + /* tslint:disable prefer-function-over-method */ + public encodeValue(value: string): Buffer { + // Encoded value is of the form: <length><value>, with each field padded to be word-aligned. + // 1/3 Construct the length + const wordsToStoreValuePadded = Math.ceil(value.length / constants.EVM_WORD_WIDTH_IN_BYTES); + const bytesToStoreValuePadded = wordsToStoreValuePadded * constants.EVM_WORD_WIDTH_IN_BYTES; + const lengthBuf = ethUtil.toBuffer(value.length); + const lengthBufPadded = ethUtil.setLengthLeft(lengthBuf, constants.EVM_WORD_WIDTH_IN_BYTES); + // 2/3 Construct the value + const valueBuf = new Buffer(value); + const valueBufPadded = ethUtil.setLengthRight(valueBuf, bytesToStoreValuePadded); + // 3/3 Combine length and value + const encodedValue = Buffer.concat([lengthBufPadded, valueBufPadded]); + return encodedValue; + } + + public decodeValue(calldata: RawCalldata): string { + // Encoded value is of the form: <length><value>, with each field padded to be word-aligned. + // 1/2 Decode length + const lengthBufPadded = calldata.popWord(); + const lengthHexPadded = ethUtil.bufferToHex(lengthBufPadded); + const length = parseInt(lengthHexPadded, constants.HEX_BASE); + // 2/2 Decode value + const wordsToStoreValuePadded = Math.ceil(length / constants.EVM_WORD_WIDTH_IN_BYTES); + const valueBufPadded = calldata.popWords(wordsToStoreValuePadded); + const valueBuf = valueBufPadded.slice(0, length); + const value = valueBuf.toString('ascii'); + return value; + } + + public getSignature(): string { + return SolidityTypes.String; + } + /* tslint:enable prefer-function-over-method */ +} 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..31593c882 --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/tuple.ts @@ -0,0 +1,24 @@ +import { DataItem, SolidityTypes } from 'ethereum-types'; + +import { DataTypeFactory } from '../abstract_data_types/interfaces'; +import { AbstractSetDataType } from '../abstract_data_types/types/set'; + +export class TupleDataType extends AbstractSetDataType { + private readonly _signature: string; + + public static matchType(type: string): boolean { + return type === SolidityTypes.Tuple; + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + super(dataItem, dataTypeFactory); + if (!TupleDataType.matchType(dataItem.type)) { + throw new Error(`Tried to instantiate Tuple with bad input: ${dataItem}`); + } + this._signature = this._computeSignatureOfMembers(); + } + + public getSignature(): string { + return this._signature; + } +} 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..5180f0cf3 --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/uint.ts @@ -0,0 +1,58 @@ +import { DataItem, SolidityTypes } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { BigNumber } from '../../configured_bignumber'; +import { DataTypeFactory } from '../abstract_data_types/interfaces'; +import { AbstractBlobDataType } from '../abstract_data_types/types/blob'; +import { RawCalldata } from '../calldata/raw_calldata'; +import { constants } from '../utils/constants'; +import * as EncoderMath from '../utils/math'; + +export class UIntDataType extends AbstractBlobDataType { + 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}$', + ); + private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true; + private static readonly _MAX_WIDTH: number = 256; + private static readonly _DEFAULT_WIDTH: number = UIntDataType._MAX_WIDTH; + private static readonly _MIN_VALUE = new BigNumber(0); + private readonly _width: number; + private readonly _maxValue: BigNumber; + + public static matchType(type: string): boolean { + return UIntDataType._MATCHER.test(type); + } + + private static _decodeWidthFromType(type: string): number { + const matches = UIntDataType._MATCHER.exec(type); + const width = + !_.isNull(matches) && matches.length === 2 && !_.isUndefined(matches[1]) + ? parseInt(matches[1], constants.DEC_BASE) + : UIntDataType._DEFAULT_WIDTH; + return width; + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + super(dataItem, dataTypeFactory, UIntDataType._SIZE_KNOWN_AT_COMPILE_TIME); + if (!UIntDataType.matchType(dataItem.type)) { + throw new Error(`Tried to instantiate UInt with bad input: ${dataItem}`); + } + this._width = UIntDataType._decodeWidthFromType(dataItem.type); + this._maxValue = new BigNumber(2).toPower(this._width).sub(1); + } + + public encodeValue(value: BigNumber | string | number): Buffer { + const encodedValue = EncoderMath.safeEncodeNumericValue(value, UIntDataType._MIN_VALUE, this._maxValue); + return encodedValue; + } + + public decodeValue(calldata: RawCalldata): BigNumber { + const valueBuf = calldata.popWord(); + const value = EncoderMath.safeDecodeNumericValue(valueBuf, UIntDataType._MIN_VALUE, this._maxValue); + return value; + } + + public getSignature(): string { + return `${SolidityTypes.Uint}${this._width}`; + } +} diff --git a/packages/utils/src/abi_encoder/index.ts b/packages/utils/src/abi_encoder/index.ts new file mode 100644 index 000000000..baf844ac6 --- /dev/null +++ b/packages/utils/src/abi_encoder/index.ts @@ -0,0 +1,14 @@ +export { EncodingRules, DecodingRules } from './utils/rules'; +export { + Address, + Array, + Bool, + DynamicBytes, + Int, + Method, + Pointer, + StaticBytes, + String, + Tuple, + UInt, +} from './evm_data_type_factory'; diff --git a/packages/utils/src/abi_encoder/utils/constants.ts b/packages/utils/src/abi_encoder/utils/constants.ts new file mode 100644 index 000000000..36de2dd4f --- /dev/null +++ b/packages/utils/src/abi_encoder/utils/constants.ts @@ -0,0 +1,17 @@ +import { DecodingRules, EncodingRules } from './rules'; + +export const constants = { + EVM_WORD_WIDTH_IN_BYTES: 32, + EVM_WORD_WIDTH_IN_BITS: 256, + HEX_BASE: 16, + DEC_BASE: 10, + BIN_BASE: 2, + HEX_SELECTOR_LENGTH_IN_CHARS: 10, + HEX_SELECTOR_LENGTH_IN_BYTES: 4, + HEX_SELECTOR_BYTE_OFFSET_IN_CALLDATA: 0, + // Disable no-object-literal-type-assertion so we can enforce cast + /* tslint:disable no-object-literal-type-assertion */ + DEFAULT_DECODING_RULES: { shouldConvertStructsToObjects: false } as DecodingRules, + DEFAULT_ENCODING_RULES: { shouldOptimize: true, shouldAnnotate: false } as EncodingRules, + /* tslint:enable no-object-literal-type-assertion */ +}; diff --git a/packages/utils/src/abi_encoder/utils/math.ts b/packages/utils/src/abi_encoder/utils/math.ts new file mode 100644 index 000000000..d84983c5b --- /dev/null +++ b/packages/utils/src/abi_encoder/utils/math.ts @@ -0,0 +1,111 @@ +import BigNumber from 'bignumber.js'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { 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 isValueNegative = valueBin.length === constants.EVM_WORD_WIDTH_IN_BITS && _.startsWith(valueBin[0], '1'); + if (!isValueNegative) { + 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; +} diff --git a/packages/utils/src/abi_encoder/utils/queue.ts b/packages/utils/src/abi_encoder/utils/queue.ts new file mode 100644 index 000000000..53afb7e11 --- /dev/null +++ b/packages/utils/src/abi_encoder/utils/queue.ts @@ -0,0 +1,39 @@ +export class Queue<T> { + private _store: T[] = []; + + public pushBack(val: T): void { + this._store.push(val); + } + + public pushFront(val: T): void { + this._store.unshift(val); + } + + public popFront(): T | undefined { + return this._store.shift(); + } + + public popBack(): T | undefined { + if (this._store.length === 0) { + return undefined; + } + const backElement = this._store.splice(-1, 1)[0]; + return backElement; + } + + public mergeBack(q: Queue<T>): void { + this._store = this._store.concat(q._store); + } + + public mergeFront(q: Queue<T>): void { + this._store = q._store.concat(this._store); + } + + public getStore(): T[] { + return this._store; + } + + public peekFront(): T | undefined { + return this._store.length >= 0 ? this._store[0] : undefined; + } +} diff --git a/packages/utils/src/abi_encoder/utils/rules.ts b/packages/utils/src/abi_encoder/utils/rules.ts new file mode 100644 index 000000000..c8d83c3ba --- /dev/null +++ b/packages/utils/src/abi_encoder/utils/rules.ts @@ -0,0 +1,8 @@ +export interface DecodingRules { + shouldConvertStructsToObjects: boolean; +} + +export interface EncodingRules { + shouldOptimize?: boolean; + shouldAnnotate?: boolean; +} diff --git a/packages/utils/src/abi_utils.ts b/packages/utils/src/abi_utils.ts index 598ea5fcc..3e6fc9665 100644 --- a/packages/utils/src/abi_utils.ts +++ b/packages/utils/src/abi_utils.ts @@ -26,7 +26,7 @@ function parseEthersParams(params: DataItem[]): { names: ParamName[]; types: str const result = parseEthersParams(param.components); names.push({ name: param.name || null, names: result.names }); - types.push('tuple(' + result.types.join(',') + ')' + suffix); + types.push(`tuple(${result.types.join(',')})${suffix}`); } else { names.push(param.name || null); types.push(param.type); @@ -120,7 +120,7 @@ function splitTupleTypes(type: string): string[] { if (_.endsWith(type, '[]')) { throw new Error('Internal error: array types are not supported'); } else if (!_.startsWith(type, 'tuple(')) { - throw new Error('Internal error: expected tuple type but got non-tuple type: ' + type); + throw new Error(`Internal error: expected tuple type but got non-tuple type: ${type}`); } // Trim the outtermost tuple(). const trimmedType = type.substring('tuple('.length, type.length - 1); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 0723e5788..082aff6bb 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -10,3 +10,4 @@ export { NULL_BYTES } from './constants'; export { errorUtils } from './error_utils'; export { fetchAsync } from './fetch_async'; export { signTypedDataUtils } from './sign_typed_data_utils'; +export import AbiEncoder = require('./abi_encoder'); diff --git a/packages/utils/src/log_utils.ts b/packages/utils/src/log_utils.ts index 87f8479b5..6d9996c67 100644 --- a/packages/utils/src/log_utils.ts +++ b/packages/utils/src/log_utils.ts @@ -1,3 +1,5 @@ +import chalk from 'chalk'; + export const logUtils = { log(...args: any[]): void { console.log(...args); // tslint:disable-line:no-console @@ -5,4 +7,7 @@ export const logUtils = { warn(...args: any[]): void { console.warn(...args); // tslint:disable-line:no-console }, + logWithTime(arg: string): void { + logUtils.log(`[${chalk.gray(new Date().toLocaleTimeString())}] ${arg}`); + }, }; |