import { DataItem } from 'ethereum-types'; import * as ethUtil from 'ethereumjs-util'; import * as _ from 'lodash'; import { BigNumber } from '../configured_bignumber'; import * as Constants from './constants'; import { Calldata, CalldataBlock, DependentCalldataBlock, MemberCalldataBlock, PayloadCalldataBlock, RawCalldata, } from './calldata'; import { DecodingRules, EncodingRules } from './utils/rules'; export interface DataTypeFactory { create: (dataItem: DataItem, parentDataType?: DataType) => DataType; mapDataItemToDataType: (dataItem: DataItem) => DataType; } export interface DataTypeStaticInterface { matchType: (type: string) => boolean; encodeValue: (value: any) => Buffer; decodeValue: (rawCalldata: RawCalldata) => any; } export abstract class DataType { private static readonly _DEFAULT_ENCODING_RULES: EncodingRules = { optimize: false, annotate: false }; private static readonly _DEFAULT_DECODING_RULES: DecodingRules = { structsAsObjects: false }; 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_ = rules ? rules : DataType._DEFAULT_ENCODING_RULES; const calldata = new Calldata(rules_); if (selector) { calldata.setSelector(selector); } const block = this.generateCalldataBlock(value); calldata.setRoot(block); // @TODO CHANGE const calldataHex = calldata.toHexString(); return calldataHex; } public decode(calldata: string, rules?: DecodingRules, hasSelector: boolean = false): any { const rawCalldata = new RawCalldata(calldata, hasSelector); const rules_ = rules ? rules : DataType._DEFAULT_DECODING_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; } export abstract class PayloadDataType extends DataType { protected _hasConstantSize: boolean; public constructor(dataItem: DataItem, factory: DataTypeFactory, hasConstantSize: boolean) { super(dataItem, factory); this._hasConstantSize = hasConstantSize; } public generateCalldataBlock(value: any, parentBlock?: CalldataBlock): PayloadCalldataBlock { const encodedValue = this.encodeValue(value); const name = this.getDataItem().name; const signature = this.getSignature(); const parentName = parentBlock === undefined ? '' : parentBlock.getName(); const block = new PayloadCalldataBlock(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._hasConstantSize; } public abstract encodeValue(value: any): Buffer; public abstract decodeValue(calldata: RawCalldata): any; } export abstract class DependentDataType extends DataType { protected _dependency: DataType; protected _parent: DataType; private readonly _isStatic: boolean; public constructor(dataItem: DataItem, factory: DataTypeFactory, dependency: DataType, parent: DataType) { super(dataItem, factory); this._dependency = dependency; this._parent = parent; this._isStatic = true; } public generateCalldataBlock(value: any, parentBlock?: CalldataBlock): DependentCalldataBlock { if (parentBlock === undefined) { throw new Error(`DependentDataType requires a parent block to generate its block`); } const dependencyBlock = this._dependency.generateCalldataBlock(value, parentBlock); const name = this.getDataItem().name; const signature = this.getSignature(); const parentName = parentBlock === undefined ? '' : parentBlock.getName(); const block = new DependentCalldataBlock(name, signature, parentName, dependencyBlock, parentBlock); return block; } public generateValue(calldata: RawCalldata, rules: DecodingRules): any { const destinationOffsetBuf = calldata.popWord(); const currentOffset = calldata.getOffset(); const destinationOffsetRelative = parseInt(ethUtil.bufferToHex(destinationOffsetBuf), Constants.HEX_BASE); const destinationOffsetAbsolute = calldata.toAbsoluteOffset(destinationOffsetRelative); calldata.setOffset(destinationOffsetAbsolute); const value = this._dependency.generateValue(calldata, rules); calldata.setOffset(currentOffset); return value; } public isStatic(): boolean { return this._isStatic; } } export interface MemberMap { [key: string]: number; } export abstract class MemberDataType extends DataType { protected readonly _arrayLength: number | undefined; protected readonly _arrayElementType: string | undefined; private readonly _memberMap: MemberMap; 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._memberMap = {}; this._members = []; this._isArray = isArray; this._arrayLength = arrayLength; this._arrayElementType = arrayElementType; if (isArray && arrayLength !== undefined) { [this._members, this._memberMap] = this._createMembersWithLength(dataItem, arrayLength); } else if (!isArray) { [this._members, this._memberMap] = this._createMembersWithKeys(dataItem); } } public generateCalldataBlock(value: any[] | object, parentBlock?: CalldataBlock): MemberCalldataBlock { 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; if (this._isArray && this._arrayLength === undefined) { const arrayLengthBuf = calldata.popWord(); const arrayLengthHex = ethUtil.bufferToHex(arrayLengthBuf); const hexBase = 16; const arrayLength = new BigNumber(arrayLengthHex, hexBase); [members] = this._createMembersWithLength(this.getDataItem(), arrayLength.toNumber()); } calldata.startScope(); let value: any[] | object; if (rules.structsAsObjects && !this._isArray) { value = {}; _.each(this._memberMap, (idx: number, key: string) => { const member = this._members[idx]; const memberValue = member.generateValue(calldata, rules); (value as { [key: string]: any })[key] = memberValue; }); } else { value = []; _.each(members, (member: DataType, idx: number) => { const memberValue = member.generateValue(calldata, rules); (value as any[]).push(memberValue); }); } calldata.endScope(); return value; } public isStatic(): boolean { /* For Tuple: const isStaticTuple = this.children.length === 0; return isStaticTuple; // @TODO: True in every case or only when dynamic data? For Array: if isLengthDefined = false then this is false Otherwise if the first element is a Pointer then false */ if (this._isArray && this._arrayLength === undefined) { return false; } // Search for dependent members const dependentMember = _.find(this._members, (member: DataType) => { return member instanceof DependentDataType; }); const isStatic = dependentMember === undefined; // static if we couldn't find a dependent member return isStatic; } protected _generateCalldataBlockFromArray(value: any[], parentBlock?: CalldataBlock): MemberCalldataBlock { // Sanity check length if (this._arrayLength !== undefined && value.length !== this._arrayLength) { throw new Error( `Expected array of ${JSON.stringify( this._arrayLength, )} elements, but got array of length ${JSON.stringify(value.length)}`, ); } const parentName = parentBlock === undefined ? '' : parentBlock.getName(); const methodBlock: MemberCalldataBlock = new MemberCalldataBlock( this.getDataItem().name, this.getSignature(), parentName, ); let members = this._members; if (this._isArray && this._arrayLength === undefined) { [members] = this._createMembersWithLength(this.getDataItem(), value.length); const lenBuf = ethUtil.setLengthLeft( ethUtil.toBuffer(`0x${value.length.toString(Constants.HEX_BASE)}`), Constants.EVM_WORD_WIDTH_IN_BYTES, ); methodBlock.setHeader(lenBuf); } const memberBlocks: CalldataBlock[] = []; _.each(members, (member: DataType, idx: number) => { const block = member.generateCalldataBlock(value[idx], methodBlock); memberBlocks.push(block); }); methodBlock.setMembers(memberBlocks); return methodBlock; } protected _generateCalldataBlockFromObject(obj: object, parentBlock?: CalldataBlock): MemberCalldataBlock { const parentName = parentBlock === undefined ? '' : parentBlock.getName(); const methodBlock: MemberCalldataBlock = new MemberCalldataBlock( this.getDataItem().name, this.getSignature(), parentName, ); const memberBlocks: CalldataBlock[] = []; const childMap = _.cloneDeep(this._memberMap); _.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 block = this._members[this._memberMap[key]].generateCalldataBlock(value, methodBlock); memberBlocks.push(block); delete childMap[key]; }); if (Object.keys(childMap).length !== 0) { throw new Error(`Could not assign tuple to object: missing keys ${Object.keys(childMap)}`); } methodBlock.setMembers(memberBlocks); return methodBlock; } 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[], MemberMap] { // Sanity check if (dataItem.components === undefined) { throw new Error(`Expected components`); } const members: DataType[] = []; const memberMap: MemberMap = {}; _.each(dataItem.components, (memberItem: DataItem) => { const childDataItem: DataItem = { type: memberItem.type, name: `${dataItem.name}.${memberItem.name}`, }; const components = memberItem.components; if (components !== undefined) { childDataItem.components = components; } const child = this.getFactory().create(childDataItem, this); memberMap[memberItem.name] = members.length; members.push(child); }); return [members, memberMap]; } private _createMembersWithLength(dataItem: DataItem, length: number): [DataType[], MemberMap] { const members: DataType[] = []; const memberMap: MemberMap = {}; const range = _.range(length); _.each(range, (idx: number) => { const childDataItem: DataItem = { type: this._arrayElementType ? this._arrayElementType : '', name: `${dataItem.name}[${idx.toString(Constants.DEC_BASE)}]`, }; const components = dataItem.components; if (components !== undefined) { childDataItem.components = components; } const child = this.getFactory().create(childDataItem, this); memberMap[idx.toString(Constants.DEC_BASE)] = members.length; members.push(child); }); return [members, memberMap]; } }