From 78498b7019e02a9b280cc1ffe9fafbf30bff24ec Mon Sep 17 00:00:00 2001 From: Greg Hysen Date: Fri, 9 Nov 2018 16:21:31 -0800 Subject: Initial port -- wont compile yet --- packages/order-utils/test/abi/abi_encoder.ts | 1 + packages/order-utils/test/abi/calldata.ts | 177 ++++++++++ packages/order-utils/test/abi/config.ts | 4 + packages/order-utils/test/abi/data_type.ts | 278 +++++++++++++++ packages/order-utils/test/abi/evm_data_types.ts | 439 ++++++++++++++++++++++++ 5 files changed, 899 insertions(+) create mode 100644 packages/order-utils/test/abi/abi_encoder.ts create mode 100644 packages/order-utils/test/abi/calldata.ts create mode 100644 packages/order-utils/test/abi/config.ts create mode 100644 packages/order-utils/test/abi/data_type.ts create mode 100644 packages/order-utils/test/abi/evm_data_types.ts (limited to 'packages') diff --git a/packages/order-utils/test/abi/abi_encoder.ts b/packages/order-utils/test/abi/abi_encoder.ts new file mode 100644 index 000000000..12755209a --- /dev/null +++ b/packages/order-utils/test/abi/abi_encoder.ts @@ -0,0 +1 @@ +export * from './calldata_block'; \ No newline at end of file diff --git a/packages/order-utils/test/abi/calldata.ts b/packages/order-utils/test/abi/calldata.ts new file mode 100644 index 000000000..dc0fcfb2b --- /dev/null +++ b/packages/order-utils/test/abi/calldata.ts @@ -0,0 +1,177 @@ +import ethUtil = require('ethereumjs-util'); +var _ = require('lodash'); + +export abstract class CalldataBlock { + private name: string; + private signature: string; + private offsetInBytes: number; + private headerSizeInBytes: number; + private bodySizeInBytes: number; + private relocatable: boolean; + + constructor(name: string, signature: string, offsetInBytes: number, headerSizeInBytes: number, bodySizeInBytes: number, relocatable: boolean) { + this.name = name; + this.signature = signature; + this.offsetInBytes = offsetInBytes; + this.headerSizeInBytes = headerSizeInBytes; + this.bodySizeInBytes = bodySizeInBytes; + this.relocatable = relocatable; + } + + public getName(): string { + return this.name; + } + + public getSignature(): string { + return this.signature; + } + + public isRelocatable(): boolean { + return this.relocatable; + } + + public getHeaderSizeInBytes(): number { + return this.headerSizeInBytes; + } + + public getBodySizeInBytes(): number { + return this.bodySizeInBytes; + } + + public getSizeInBytes(): number { + return this.headerSizeInBytes + this.bodySizeInBytes; + } + + public getOffsetInBytes(): number { + return this.offsetInBytes; + } + + public abstract toHexString(): string; +} + +export class PayloadCalldataBlock extends CalldataBlock { + private payload: Buffer; + + constructor(name: string, signature: string, offsetInBytes: number, relocatable: boolean, payload: Buffer) { + const headerSizeInBytes = 0; + const bodySizeInBytes = payload.byteLength; + super(name, signature, offsetInBytes, headerSizeInBytes, bodySizeInBytes, relocatable); + this.payload = payload; + } + + public toHexString(): string { + const payloadHex = ethUtil.bufferToHex(this.payload); + return payloadHex; + } +} + +export class DependentCalldataBlock extends CalldataBlock { + public static DEPENDENT_PAYLOAD_SIZE_IN_BYTES = 32; + private parent: CalldataBlock; + private dependency: CalldataBlock; + + constructor(name: string, signature: string, offsetInBytes: number, relocatable: boolean, parent: CalldataBlock, dependency: CalldataBlock) { + const headerSizeInBytes = 0; + const bodySizeInBytes = DependentCalldataBlock.DEPENDENT_PAYLOAD_SIZE_IN_BYTES; + super(name, signature, offsetInBytes, headerSizeInBytes, bodySizeInBytes, relocatable); + this.parent = parent; + this.dependency = dependency; + } + + public toHexString(): string { + const dependencyOffset = this.dependency.getOffsetInBytes(); + const parentOffset = this.parent.getOffsetInBytes(); + const parentHeaderSize = this.parent.getHeaderSizeInBytes(); + const pointer = dependencyOffset - parentOffset + parentHeaderSize; + const pointerBuf = new Buffer(`0x${pointer.toString(16)}`); + const evmWordWidthInBytes = 32; + const pointerBufPadded = ethUtil.setLengthLeft(pointerBuf, evmWordWidthInBytes); + const pointerHex = ethUtil.bufferToHex(pointerBufPadded); + return pointerHex; + } +} + +export class MemberCalldataBlock extends CalldataBlock { + private static DEPENDENT_PAYLOAD_SIZE_IN_BYTES = 32; + private header: Buffer | undefined; + private members: CalldataBlock[]; + + constructor(name: string, signature: string, offsetInBytes: number, relocatable: boolean, members: CalldataBlock[], header?: Buffer) { + const headerSizeInBytes = (header === undefined) ? 0 : header.byteLength; + let bodySizeInBytes = 0; + _.each(members, (member: Memblock) => { + bodySizeInBytes += member.getSizeInBytes(); + }); + + super(name, signature, offsetInBytes, headerSizeInBytes, bodySizeInBytes, relocatable); + this.members = members; + this.header = header; + } + + public toHexString(): string { + let valueBuffers: Buffer[] = []; + if (this.header !== undefined) valueBuffers.push(this.header); + _.each(this.members, (member: CalldataBlock) => { + const memberHexString = member.toHexString(); + const memberBuf = ethUtil.toBuffer(memberHexString); + valueBuffers.push(memberBuf); + }); + const combinedValueBufs = Buffer.concat(valueBuffers); + const combinedValuesAsHex = ethUtil.bufferToHex(combinedValueBufs); + return combinedValuesAsHex; + } +} + +export class Calldata { + private selector: string; + private sizeInBytes: number; + private blocks: CalldataBlock[]; + + constructor() { + this.selector = '0x'; + this.sizeInBytes = 0; + this.blocks = []; + } + + public toHexString(): string { + let calldataString = `${this.selector}`; + _.each(this.blocks, (block: CalldataBlock) => { + const blockAsHexString = block.toHexString(); + const blockAsHexWithoutPrefix = ethUtil.stripHexPrefix(blockAsHexString); + calldataString += blockAsHexWithoutPrefix; + }); + return calldataString; + } + + public getSelectorHex(): string { + return this.selector; + } + + public getSizeInBytes(): number { + return this.sizeInBytes; + } + + public toAnnotatedString(): string { + return ""; + } + + public pushBlock(block: CalldataBlock) { + this.blocks.push(block); + this.sizeInBytes += block.getSizeInBytes(); + } + + public setSelector(selector: string) { + // Ensure we have a 0x prefix + if (selector.startsWith('0x')) { + this.selector = selector; + } else { + this.selector = `$0x${selector}`; + } + + // The selector must be 10 characters: '0x' followed by 4 bytes (two hex chars per byte) + if (this.selector.length !== 10) { + throw new Error(`Invalid selector '${this.selector}'`); + } + this.sizeInBytes += 8; + } +} \ No newline at end of file diff --git a/packages/order-utils/test/abi/config.ts b/packages/order-utils/test/abi/config.ts new file mode 100644 index 000000000..015cee59a --- /dev/null +++ b/packages/order-utils/test/abi/config.ts @@ -0,0 +1,4 @@ +import { DataTypeFactory } from './data_type'; +import { EvmDataTypeFactoryImpl } from './evm_data_types'; + +DataTypeFactory.setImpl(new EvmDataTypeFactoryImpl()); \ No newline at end of file diff --git a/packages/order-utils/test/abi/data_type.ts b/packages/order-utils/test/abi/data_type.ts new file mode 100644 index 000000000..f74621085 --- /dev/null +++ b/packages/order-utils/test/abi/data_type.ts @@ -0,0 +1,278 @@ +import { Calldata, CalldataBlock, PayloadCalldataBlock, DependentCalldataBlock } from "./calldata"; +import { MethodAbi, DataItem } from 'ethereum-types'; +import { BigNumber } from '@0x/utils'; +import ethUtil = require('ethereumjs-util'); +var _ = require('lodash'); + + + +export abstract class DataType { + private dataItem: DataItem; + + constructor(dataItem: DataItem) { + this.dataItem = dataItem; + } + + public getDataItem(): DataItem { + return this.dataItem; + } + + protected abstract createCalldataBlock(): CalldataBlock; + public abstract encode(value: any, calldata: Calldata): void; + public abstract getSignature(): string; + public abstract isStatic(): boolean; +} + +export abstract class PayloadDataType extends DataType { + protected hasConstantSize: boolean; + + public constructor(dataItem: DataItem, hasConstantSize: boolean) { + super(dataItem); + this.hasConstantSize = hasConstantSize; + } + + protected generateCalldataBlock(payload: Buffer, calldata: Calldata): void { + const name = this.getDataItem().name; + const signature = this.getSignature(); + const offsetInBytes = calldata.getSizeInBytes(); + const relocatable = false; + const block = new PayloadCalldataBlock(name, signature, offsetInBytes, relocatable, payload); + calldata.pushBlock(block); + } + + public isStatic(): boolean { + // If a payload has a constant size then it's static + return this.hasConstantSize; + } +} + +export abstract class DependentDataType extends DataType { + protected dependency: DataType; + protected parent: DataType; + + public constructor(dataItem: DataItem, dependency: DataType, parent: DataType) { + super(dataItem); + this.dependency = dependency; + this.parent = parent; + } + + protected generateCalldataBlock(offsetInBytes: number, calldata: Calldata): CalldataBlock { + const name = this.getDataItem().name; + const signature = this.getSignature(); + const relocatable = false; + const parentBlock = calldata.lookupBlockByName(this.parent.getDataItem().name); + const dependencyBlock = calldata.lookupBlockByName(this.parent.getDataItem().name); + const block = new DependentCalldataBlock(name, signature, offsetInBytes, relocatable, dependencyBlock, parentBlock); + calldata.pushBlock(block); + } + + public encode(value: any, calldata: Calldata = new Calldata()): void { + const offsetInBytes = calldata.reserveSpace(DependentCalldataBlock.DEPENDENT_PAYLOAD_SIZE_IN_BYTES); + this.dependency.encode(value, calldata); + this.generateCalldataBlock(offsetInBytes, calldata); + } + + public isStatic(): boolean { + return true; + } +} + +export interface MemberMap { + [key: string]: number; +} + +export abstract class MemberDataType extends DataType { + private memberMap: MemberMap; + private members: DataType[]; + private isArray: boolean; + protected arrayLength: number | undefined; + + + public constructor(dataItem: DataItem, isArray: boolean = false, arrayLength?: number) { + super(dataItem); + + this.memberMap = {}; + this.members = []; + this.isArray = isArray; + this.arrayLength = arrayLength; + if (isArray && arrayLength !== undefined) { + [this.members, this.memberMap] = MemberDataType.createMembersWithLength(arrayLength); + } else if (!isArray) { + [this.members, this.memberMap] = MemberDataType.createMembersWithKeys(dataItem); + } + } + + private static createMembersWithKeys(dataItem: DataItem): [DataType[], MemberMap] { + // Sanity check + if (dataItem.components === undefined) { + throw new Error(`Expected components`); + } + + let members: DataType[] = []; + let memberMap: MemberMap = {}; + _.each(dataItem.components, (dataItem: DataItem) => { + const childDataItem = { + type: dataItem.type, + name: `${dataItem.name}.${dataItem.name}`, + } as DataItem; + const child = DataTypeFactory.create(childDataItem, this); + members.push(child); + memberMap[dataItem.name] = members.length; + }); + + return [members, memberMap]; + } + + private static createMembersWithLength(dataItem: DataItem, length: number): [DataType[], MemberMap] { + let members: DataType[] = []; + let memberMap: MemberMap = {}; + const range = _.range(length); + _.each(range, (idx: number) => { + const childDataItem = { + type: this.type, + name: `${dataItem.name}[${idx.toString(10)}]`, + } as DataItem; + const components = dataItem.components; + if (components !== undefined) { + childDataItem.components = components; + } + const child = DataTypeFactory.create(childDataItem, this); + members.push(child); + memberMap[idx.toString(10)] = members.length; + }); + + return [members, memberMap]; + } + + protected encodeFromArray(value: any[], calldata: Calldata) { + // Sanity check length + const valueLength = new BigNumber(value.length); + if (this.length !== SolArray.UNDEFINED_LENGTH && valueLength.equals(this.length) === false) { + throw new Error( + `Expected array of ${JSON.stringify( + this.length, + )} elements, but got array of length ${JSON.stringify(valueLength)}`, + ); + } + + // Assign values to children + for (let idx = new BigNumber(0); idx.lessThan(this.length); idx = idx.plus(1)) { + const idxNumber = idx.toNumber(); + this.members[idxNumber].assignValue(value[idxNumber]); + } + } + + protected encodeFromObject(obj: object, calldata: Calldata) { + let childMap = _.cloneDeep(this.memberMap); + _.forOwn(obj, (value: any, key: string) => { + if (key in childMap === false) { + throw new Error(`Could not assign tuple to object: unrecognized key '${key}'`); + } + this.members[this.childMap[key]].assignValue(value); + delete childMap[key]; + }); + + if (Object.keys(childMap).length !== 0) { + throw new Error(`Could not assign tuple to object: missing keys ${Object.keys(childMap)}`); + } + } + + public encode(value: any[] | object, calldata = new Calldata()) { + if (value instanceof Array) { + this.encodeFromArray(value, calldata); + } else if (typeof value === 'object') { + this.encodeFromObject(value, encodeFromObject); + } else { + throw new Error(`Unexpected type for ${value}`); + } + } + + protected generateCalldataBlock(offsetInBytes: number, calldata: Calldata): CalldataBlock { + const name = this.getDataItem().name; + const signature = this.getSignature(); + const relocatable = false; + const parentBlock = calldata.lookupBlockByName(this.parent.getDataItem().name); + const dependencyBlock = calldata.lookupBlockByName(this.parent.getDataItem().name); + const block = new DependentCalldataBlock(name, signature, offsetInBytes, relocatable, dependencyBlock, parentBlock); + calldata.pushBlock(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; + } + + 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 true; + } + + // 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; + } +} + +export interface DataTypeFactoryImpl { + create: (dataItem: DataItem, parentDataType: DataType) => DataType; + mapDataItemToDataType: (dataItem: DataItem) => DataType; +} + +export class DataTypeFactory { + private static instance: DataTypeFactory; + private provider: DataTypeFactoryImpl | undefined; + + private constructor() { } + + private static getInstance(): DataTypeFactory { + if (!DataTypeFactory.instance) { + DataTypeFactory.instance = new DataTypeFactory(); + } + return DataTypeFactory.instance; + } + + public static setImpl(provider: DataTypeFactoryImpl) { + const instance = DataTypeFactory.getInstance(); + if (instance.provider !== undefined) { + throw new Error(`Tried to set implementation more than once`); + } + DataTypeFactory.getInstance().provider = provider; + } + + public static create(dataItem: DataItem, parentDataType: DataType): DataType { + const instance = DataTypeFactory.getInstance(); + if (instance.provider === undefined) { + throw new Error(`Tried to create before implementation is set`); + } + return instance.provider.create(dataItem, parentDataType); + } + + public static mapDataItemToDataType(dataItem: DataItem): DataType { + const instance = DataTypeFactory.getInstance(); + if (instance.provider === undefined) { + throw new Error(`Tried to create before implementation is set`); + } + return instance.provider.mapDataItemToDataType(dataItem); + } +} \ No newline at end of file diff --git a/packages/order-utils/test/abi/evm_data_types.ts b/packages/order-utils/test/abi/evm_data_types.ts new file mode 100644 index 000000000..4e42a992a --- /dev/null +++ b/packages/order-utils/test/abi/evm_data_types.ts @@ -0,0 +1,439 @@ +import { DataType, DataTypeFactory, DataTypeFactoryImpl, PayloadDataType, DependentDataType, MemberDataType } from './data_type'; + +import { MethodAbi, DataItem } from 'ethereum-types'; + +import ethUtil = require('ethereumjs-util'); + +import { BigNumber } from '@0x/utils'; + +export interface DataTypeStaticInterface { + matchGrammar: (type: string) => boolean; + encodeValue: (value: any) => Buffer; +} + +export class Address extends PayloadDataType { + private static SIZE_KNOWN_AT_COMPILE_TIME: boolean = true; + + constructor(dataItem: DataItem) { + super(dataItem, Address.SIZE_KNOWN_AT_COMPILE_TIME); + if (!Address.matchGrammar(dataItem.type)) { + throw new Error(`Tried to instantiate Address with bad input: ${dataItem}`); + } + } + + public getSignature(): string { + return 'address'; + } + + public static matchGrammar(type: string): boolean { + return type === 'address'; + } + + public static encodeValue(value: boolean): Buffer { + const evmWordWidth = 32; + const encodedValueBuf = ethUtil.setLengthLeft(ethUtil.toBuffer(value), evmWordWidth); + return encodedValueBuf; + } +} + +export class Bool extends PayloadDataType { + private static SIZE_KNOWN_AT_COMPILE_TIME: boolean = true; + + constructor(dataItem: DataItem) { + super(dataItem, Bool.SIZE_KNOWN_AT_COMPILE_TIME); + if (!Bool.matchGrammar(dataItem.type)) { + throw new Error(`Tried to instantiate Bool with bad input: ${dataItem}`); + } + } + + public getSignature(): string { + return 'bool'; + } + + public static matchGrammar(type: string): boolean { + return type === 'bool'; + } + + public static encodeValue(value: boolean): Buffer { + const evmWordWidth = 32; + const encodedValue = value === true ? '0x1' : '0x0'; + const encodedValueBuf = ethUtil.setLengthLeft(ethUtil.toBuffer(encodedValue), evmWordWidth); + return encodedValueBuf; + } +} + +abstract class Number extends PayloadDataType { + private static SIZE_KNOWN_AT_COMPILE_TIME: boolean = true; + static MAX_WIDTH: number = 256; + static DEFAULT_WIDTH: number = Number.MAX_WIDTH; + width: number = Number.DEFAULT_WIDTH; + + constructor(dataItem: DataItem, matcher: RegExp) { + super(dataItem, Number.SIZE_KNOWN_AT_COMPILE_TIME); + const matches = matcher.exec(dataItem.type); + if (matches === null) { + throw new Error(`Tried to instantiate Number with bad input: ${dataItem}`); + } + if (matches !== null && matches.length === 2 && matches[1] !== undefined) { + this.width = parseInt(matches[1]); + } else { + this.width = 256; + } + } + + public static encodeValue(value: BigNumber): Buffer { + if (value.greaterThan(this.getMaxValue())) { + throw `tried to assign value of ${value}, which exceeds max value of ${this.getMaxValue()}`; + } else if (value.lessThan(this.getMinValue())) { + throw `tried to assign value of ${value}, which exceeds min value of ${this.getMinValue()}`; + } + + const hexBase = 16; + const evmWordWidth = 32; + let valueBuf: Buffer; + if (value.greaterThanOrEqualTo(0)) { + valueBuf = ethUtil.setLengthLeft(ethUtil.toBuffer(`0x${value.toString(hexBase)}`), evmWordWidth); + } else { + // BigNumber can't write a negative hex value, so we use twos-complement conversion to do it ourselves. + // Step 1/3: Convert value to positive binary string + const binBase = 2; + const valueBin = value.times(-1).toString(binBase); + + // Step 2/3: Invert binary value + const bitsInEvmWord = 256; + let invertedValueBin = '1'.repeat(bitsInEvmWord - valueBin.length); + _.each(valueBin, (bit: string) => { + invertedValueBin += bit === '1' ? '0' : '1'; + }); + const invertedValue = new BigNumber(invertedValueBin, binBase); + + // Step 3/3: Add 1 to inverted value + // The result is the two's-complement represent of the input value. + const negativeValue = invertedValue.plus(1); + + // Convert the negated value to a hex string + valueBuf = ethUtil.setLengthLeft( + ethUtil.toBuffer(`0x${negativeValue.toString(hexBase)}`), + evmWordWidth, + ); + } + + return valueBuf; + } + + public abstract getMaxValue(): BigNumber; + public abstract getMinValue(): BigNumber; +} + +export class Int extends Number { + static 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}$', + ); + + constructor(dataItem: DataItem) { + super(dataItem, Int.matcher); + } + + public getMaxValue(): BigNumber { + return new BigNumber(2).toPower(this.width - 1).sub(1); + } + + public getMinValue(): BigNumber { + return new BigNumber(2).toPower(this.width - 1).times(-1); + } + + public getSignature(): string { + return `int${this.width}`; + } + + public static matchGrammar(type: string): boolean { + return this.matcher.test(type); + } +} + +export class UInt extends Number { + static 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}$', + ); + + constructor(dataItem: DataItem) { + super(dataItem, UInt.matcher); + } + + public getMaxValue(): BigNumber { + return new BigNumber(2).toPower(this.width).sub(1); + } + + public getMinValue(): BigNumber { + return new BigNumber(0); + } + + public getSignature(): string { + return `uint${this.width}`; + } + + public static matchGrammar(type: string): boolean { + return this.matcher.test(type); + } +} + +export class Byte extends PayloadDataType { + private static SIZE_KNOWN_AT_COMPILE_TIME: boolean = true; + static 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))$', + ); + + static DEFAULT_WIDTH = 1; + width: number = Byte.DEFAULT_WIDTH; + + constructor(dataItem: DataItem) { + super(dataItem, Byte.SIZE_KNOWN_AT_COMPILE_TIME); + const matches = Byte.matcher.exec(dataItem.type); + if (!Byte.matchGrammar(dataItem.type)) { + throw new Error(`Tried to instantiate Byte with bad input: ${dataItem}`); + } + if (matches !== null && matches.length === 3 && matches[2] !== undefined) { + this.width = parseInt(matches[2]); + } else { + this.width = Byte.DEFAULT_WIDTH; + } + } + + public getSignature(): string { + // Note that `byte` reduces to `bytes1` + return `bytes${this.width}`; + } + + public static encodeValue(value: string | Buffer): Buffer { + // Convert value into a buffer and do bounds checking + const valueBuf = ethUtil.toBuffer(value); + if (valueBuf.byteLength > this.width) { + throw new Error( + `Tried to assign ${value} (${ + valueBuf.byteLength + } bytes), which exceeds max bytes that can be stored in a ${this.getSignature()}`, + ); + } else if (value.length % 2 !== 0) { + throw new Error(`Tried to assign ${value}, which is contains a half-byte. Use full bytes only.`); + } + + // Store value as hex + const evmWordWidth = 32; + const paddedValue = ethUtil.setLengthRight(valueBuf, evmWordWidth); + return paddedValue; + } + + public static matchGrammar(type: string): boolean { + return this.matcher.test(type); + } +} + +export class Bytes extends PayloadDataType { + private static SIZE_KNOWN_AT_COMPILE_TIME: boolean = false; + static UNDEFINED_LENGTH = new BigNumber(-1); + length: BigNumber = Bytes.UNDEFINED_LENGTH; + + constructor(dataItem: DataItem) { + super(dataItem, Bytes.SIZE_KNOWN_AT_COMPILE_TIME); + if (!Bytes.matchGrammar(dataItem.type)) { + throw new Error(`Tried to instantiate Bytes with bad input: ${dataItem}`); + } + } + + public encodeValue(value: string | Buffer): Buffer { + if (typeof value === 'string' && !value.startsWith('0x')) { + throw new Error(`Input value must be hex (prefixed with 0x). Actual value is '${value}'`); + } + const valueBuf = ethUtil.toBuffer(value); + if (value.length % 2 !== 0) { + throw new Error(`Tried to assign ${value}, which is contains a half-byte. Use full bytes only.`); + } + + const wordsForValue = Math.ceil(valueBuf.byteLength / 32); + const paddedBytesForValue = wordsForValue * 32; + const paddedValueBuf = ethUtil.setLengthRight(valueBuf, paddedBytesForValue); + const paddedLengthBuf = ethUtil.setLengthLeft(ethUtil.toBuffer(valueBuf.byteLength), 32); + const encodedValueBuf = Buffer.concat([paddedLengthBuf, paddedValueBuf]); + return encodedValueBuf; + } + + public getSignature(): string { + return 'bytes'; + } + + public static matchGrammar(type: string): boolean { + return type === 'bytes'; + } +} + +export class SolString extends PayloadDataType { + private static SIZE_KNOWN_AT_COMPILE_TIME: boolean = false; + constructor(dataItem: DataItem) { + super(dataItem, SolString.SIZE_KNOWN_AT_COMPILE_TIME); + if (!SolString.matchGrammar(dataItem.type)) { + throw new Error(`Tried to instantiate String with bad input: ${dataItem}`); + } + } + + public static encodeValue(value: string): Buffer { + const wordsForValue = Math.ceil(value.length / 32); + const paddedBytesForValue = wordsForValue * 32; + const valueBuf = ethUtil.setLengthRight(ethUtil.toBuffer(value), paddedBytesForValue); + const lengthBuf = ethUtil.setLengthLeft(ethUtil.toBuffer(value.length), 32); + const encodedValueBuf = Buffer.concat([lengthBuf, valueBuf]); + return encodedValueBuf; + } + + public getSignature(): string { + return 'string'; + } + + public static matchGrammar(type: string): boolean { + return type === 'string'; + } +} + +export class Pointer extends DependentDataType { + + constructor(destDataType: DataType, parentDataType: DataType) { + const destDataItem = destDataType.getDataItem(); + const dataItem = { name: `ptr<${destDataItem.name}>`, type: `ptr<${destDataItem.type}>` } as DataItem; + super(dataItem, destDataType, parentDataType); + } + + public getSignature(): string { + return this.dependency.getSignature(); + } +} + +export class Tuple extends MemberDataType { + private tupleSignature: string; + + constructor(dataItem: DataItem) { + super(dataItem); + if (!Tuple.matchGrammar(dataItem.type)) { + throw new Error(`Tried to instantiate Tuple with bad input: ${dataItem}`); + } + this.tupleSignature = this.computeSignatureOfMembers(); + } + + public getSignature(): string { + return this.tupleSignature; + } + + public static matchGrammar(type: string): boolean { + return type === 'tuple'; + } +} + +export class SolArray extends MemberDataType { + static matcher = RegExp('^(.+)\\[([0-9]*)\\]$'); + private arraySignature: string; + private elementType: string; + + constructor(dataItem: DataItem) { + // Sanity check + const matches = SolArray.matcher.exec(dataItem.type); + if (matches === null || matches.length !== 3) { + throw new Error(`Could not parse array: ${dataItem.type}`); + } else if (matches[1] === undefined) { + throw new Error(`Could not parse array type: ${dataItem.type}`); + } else if (matches[2] === undefined) { + throw new Error(`Could not parse array length: ${dataItem.type}`); + } + + const isArray = true; + const arrayLength = (matches[2] === '') ? undefined : parseInt(matches[2], 10); + super(dataItem, isArray, arrayLength); + this.elementType = matches[1]; + this.arraySignature = this.computeSignature(); + } + + private computeSignature(): string { + let dataItem = { + type: this.elementType, + name: 'N/A', + } as DataItem; + const components = this.getDataItem().components; + if (components !== undefined) { + dataItem.components = components; + } + const elementDataType = DataTypeFactory.mapDataItemToDataType(dataItem); + const type = elementDataType.getSignature(); + if (this.arrayLength === undefined) { + return `${type}[]`; + } else { + return `${type}[${this.arrayLength}]`; + } + } + + public getSignature(): string { + return this.arraySignature; + } + + public static matchGrammar(type: string): boolean { + return this.matcher.test(type); + } +} + +export class Method extends MemberDataType { + private methodSignature: string; + private methodSelector: string; + + constructor(abi: MethodAbi) { + super({ type: 'method', name: abi.name }); + this.methodSignature = this.computeSignature(); + this.methodSelector = this.computeSelector(); + } + + 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(0, 4))); + return selector; + } + + public getSignature(): string { + return this.methodSignature; + } + + public getSelector(): string { + return this.methodSelector; + } +} + +export class EvmDataTypeFactoryImpl implements DataTypeFactoryImpl { + + public mapDataItemToDataType(dataItem: DataItem): DataType { + console.log(`Type: ${dataItem.type}`); + + if (SolArray.matchGrammar(dataItem.type)) return new SolArray(dataItem); + if (Address.matchGrammar(dataItem.type)) return new Address(dataItem); + if (Bool.matchGrammar(dataItem.type)) return new Bool(dataItem); + if (Int.matchGrammar(dataItem.type)) return new Int(dataItem); + if (UInt.matchGrammar(dataItem.type)) return new UInt(dataItem); + if (Byte.matchGrammar(dataItem.type)) return new Byte(dataItem); + if (Tuple.matchGrammar(dataItem.type)) return new Tuple(dataItem); + if (Bytes.matchGrammar(dataItem.type)) return new Bytes(dataItem); + if (SolString.matchGrammar(dataItem.type)) return new SolString(dataItem); + //if (Fixed.matchGrammar(dataItem.type)) return Fixed(dataItem); + //if (UFixed.matchGrammar(dataItem.type)) return UFixed(dataItem); + + throw new Error(`Unrecognized data type: '${dataItem.type}'`); + } + + public create(dataItem: DataItem, parentDataType: DataType): DataType { + const dataType = this.mapDataItemToDataType(dataItem); + if (dataType.isStatic()) { + return dataType; + } + + const pointer = new Pointer(dataType, parentDataType); + return pointer; + } +} \ No newline at end of file -- cgit v1.2.3