diff options
author | Leonid Logvinov <logvinov.leon@gmail.com> | 2019-01-09 19:02:25 +0800 |
---|---|---|
committer | Leonid Logvinov <logvinov.leon@gmail.com> | 2019-01-09 19:02:25 +0800 |
commit | ea14913b412e78ff458bdfba47182f7363e776e5 (patch) | |
tree | 3ee220bfbbd9923b5e1adc36ee51f9b5d39ad640 /packages/utils/src/abi_encoder/abstract_data_types | |
parent | 5868c91cfb54cfa9177572b201d88d1168bf5b06 (diff) | |
parent | 5dd55491b86bf8577405e37d0f2d668aa1273b10 (diff) | |
download | dexon-sol-tools-ea14913b412e78ff458bdfba47182f7363e776e5.tar dexon-sol-tools-ea14913b412e78ff458bdfba47182f7363e776e5.tar.gz dexon-sol-tools-ea14913b412e78ff458bdfba47182f7363e776e5.tar.bz2 dexon-sol-tools-ea14913b412e78ff458bdfba47182f7363e776e5.tar.lz dexon-sol-tools-ea14913b412e78ff458bdfba47182f7363e776e5.tar.xz dexon-sol-tools-ea14913b412e78ff458bdfba47182f7363e776e5.tar.zst dexon-sol-tools-ea14913b412e78ff458bdfba47182f7363e776e5.zip |
Merge development
Diffstat (limited to 'packages/utils/src/abi_encoder/abstract_data_types')
5 files changed, 389 insertions, 0 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]; + } +} |