import { DataType, DataTypeFactory, PayloadDataType, DependentDataType, MemberDataType } from './data_type';
import { DecodingRules, EncodingRules } from './calldata';
import { MethodAbi, DataItem } from 'ethereum-types';
import ethUtil = require('ethereumjs-util');
import { Calldata, RawCalldata } from './calldata';
import { BigNumber } from '../configured_bignumber';
var _ = require('lodash');
export interface DataTypeStaticInterface {
matchGrammar: (type: string) => boolean;
encodeValue: (value: any) => Buffer;
decodeValue: (rawCalldata: RawCalldata) => any;
}
export class Address extends PayloadDataType {
private static SIZE_KNOWN_AT_COMPILE_TIME: boolean = true;
public static ERROR_MESSAGE_ADDRESS_MUST_START_WITH_0X = "Address must start with '0x'";
public static ERROR_MESSAGE_ADDRESS_MUST_BE_20_BYTES = 'Address must be 20 bytes';
constructor(dataItem: DataItem) {
super(dataItem, EvmDataTypeFactory.getInstance(), 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 encodeValue(value: string): Buffer {
if (value.startsWith('0x') === false) {
throw new Error(Address.ERROR_MESSAGE_ADDRESS_MUST_START_WITH_0X);
}
const valueAsBuffer = ethUtil.toBuffer(value);
if (valueAsBuffer.byteLength !== 20) {
throw new Error(Address.ERROR_MESSAGE_ADDRESS_MUST_BE_20_BYTES);
}
const evmWordWidth = 32;
const encodedValueBuf = ethUtil.setLengthLeft(valueAsBuffer, evmWordWidth);
return encodedValueBuf;
}
public decodeValue(calldata: RawCalldata): string {
const paddedValueBuf = calldata.popWord();
const valueBuf = paddedValueBuf.slice(12);
const value = ethUtil.bufferToHex(valueBuf);
return value;
}
}
export class Bool extends PayloadDataType {
private static SIZE_KNOWN_AT_COMPILE_TIME: boolean = true;
constructor(dataItem: DataItem) {
super(dataItem, EvmDataTypeFactory.getInstance(), 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 encodeValue(value: boolean): Buffer {
const evmWordWidth = 32;
const encodedValue = value === true ? '0x1' : '0x0';
const encodedValueBuf = ethUtil.setLengthLeft(ethUtil.toBuffer(encodedValue), evmWordWidth);
return encodedValueBuf;
}
public decodeValue(calldata: RawCalldata): boolean {
const valueBuf = calldata.popWord();
const valueHex = ethUtil.bufferToHex(valueBuf);
const valueNumber = new BigNumber(valueHex, 16);
let value: boolean = valueNumber.equals(0) ? false : true;
if (!(valueNumber.equals(0) || valueNumber.equals(1))) {
throw new Error(`Failed to decode boolean. Expected 0x0 or 0x1, got ${valueHex}`);
}
return value;
}
}
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, EvmDataTypeFactory.getInstance(), Number.SIZE_KNOWN_AT_COMPILE_TIME);
const matches = matcher.exec(dataItem.type);
if (matches === null) {
throw new Error(`Tried to instantiate Number with bad input: ${dataItem}`);
}
if (matches !== null && matches.length === 2 && matches[1] !== undefined) {
this.width = parseInt(matches[1]);
} else {
this.width = 256;
}
}
public encodeValue(value_: BigNumber | string | number): Buffer {
const value = new BigNumber(value_, 10);
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 decodeValue(calldata: RawCalldata): BigNumber {
const paddedValueBuf = calldata.popWord();
const paddedValueHex = ethUtil.bufferToHex(paddedValueBuf);
let value = new BigNumber(paddedValueHex, 16);
if (this instanceof Int) {
// Check if we're negative
const binBase = 2;
const valueBin = value.toString(2);
if (valueBin.length === 256 && valueBin[0].startsWith('1')) {
// Negative
// Step 1/3: Invert binary value
let invertedValueBin = '';
_.each(valueBin, (bit: string) => {
invertedValueBin += bit === '1' ? '0' : '1';
});
const invertedValue = new BigNumber(invertedValueBin, binBase);
// Step 2/3: Add 1 to inverted value
// The result is the two's-complement represent of the input value.
const positiveValue = invertedValue.plus(1);
// Step 3/3: Invert positive value
const negativeValue = positiveValue.times(-1);
value = negativeValue;
}
}
return value;
}
public abstract getMaxValue(): BigNumber;
public abstract getMinValue(): BigNumber;
}
export class Int extends Number {
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, EvmDataTypeFactory.getInstance(), 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 encodeValue(value: string | Buffer): Buffer {
// Sanity check if string
if (typeof value === 'string' && value.startsWith('0x') === false) {
throw new Error(`Tried to encode non-hex value. Value must inlcude '0x' prefix.`);
}
// Convert value into a buffer and do bounds checking
const valueBuf = ethUtil.toBuffer(value);
if (valueBuf.byteLength > this.width) {
throw new Error(
`Tried to assign ${value} (${
valueBuf.byteLength
} bytes), which exceeds max bytes that can be stored in a ${this.getSignature()}`,
);
} else if (value.length % 2 !== 0) {
throw new Error(`Tried to assign ${value}, which is contains a half-byte. Use full bytes only.`);
}
// Store value as hex
const evmWordWidth = 32;
const paddedValue = ethUtil.setLengthRight(valueBuf, evmWordWidth);
return paddedValue;
}
public decodeValue(calldata: RawCalldata): string {
const paddedValueBuf = calldata.popWord();
const valueBuf = paddedValueBuf.slice(0, this.width);
const value = ethUtil.bufferToHex(valueBuf);
return value;
}
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, EvmDataTypeFactory.getInstance(), 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(`Tried to encode non-hex value. Value must inlcude '0x' prefix. Got '${value}'`);
}
const valueBuf = ethUtil.toBuffer(value);
if (value.length % 2 !== 0) {
throw new Error(`Tried to assign ${value}, which is contains a half-byte. Use full bytes only.`);
}
const wordsForValue = Math.ceil(valueBuf.byteLength / 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 decodeValue(calldata: RawCalldata): string {
const lengthBuf = calldata.popWord();
const lengthHex = ethUtil.bufferToHex(lengthBuf);
const length = parseInt(lengthHex, 16);
const wordsForValue = Math.ceil(length / 32);
const paddedValueBuf = calldata.popWords(wordsForValue);
const valueBuf = paddedValueBuf.slice(0, length);
const decodedValue = ethUtil.bufferToHex(valueBuf);
return decodedValue;
}
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, EvmDataTypeFactory.getInstance(), SolString.SIZE_KNOWN_AT_COMPILE_TIME);
if (!SolString.matchGrammar(dataItem.type)) {
throw new Error(`Tried to instantiate String with bad input: ${dataItem}`);
}
}
public encodeValue(value: string): Buffer {
const wordsForValue = Math.ceil(value.length / 32);
const paddedBytesForValue = wordsForValue * 32;
const valueBuf = ethUtil.setLengthRight(new Buffer(value), paddedBytesForValue);
const lengthBuf = ethUtil.setLengthLeft(ethUtil.toBuffer(value.length), 32);
const encodedValueBuf = Buffer.concat([lengthBuf, valueBuf]);
return encodedValueBuf;
}
public decodeValue(calldata: RawCalldata): string {
const lengthBuf = calldata.popWord();
const lengthHex = ethUtil.bufferToHex(lengthBuf);
const length = parseInt(lengthHex, 16);
const wordsForValue = Math.ceil(length / 32);
const paddedValueBuf = calldata.popWords(wordsForValue);
const valueBuf = paddedValueBuf.slice(0, length);
const value = valueBuf.toString('ascii');
return value;
}
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, EvmDataTypeFactory.getInstance(), destDataType, parentDataType);
}
public getSignature(): string {
return this.dependency.getSignature();
}
}
export class Tuple extends MemberDataType {
private tupleSignature: string;
constructor(dataItem: DataItem) {
super(dataItem, EvmDataTypeFactory.getInstance());
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 arrayElementType = matches[1];
const arrayLength = matches[2] === '' ? undefined : parseInt(matches[2], 10);
super(dataItem, EvmDataTypeFactory.getInstance(), isArray, arrayLength, arrayElementType);
this.elementType = arrayElementType;
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 = this.getFactory().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;
private returnDataTypes: DataType[];
// TMP
public selector: string;
constructor(abi: MethodAbi) {
super({ type: 'method', name: abi.name, components: abi.inputs }, EvmDataTypeFactory.getInstance());
this.methodSignature = this.computeSignature();
this.selector = this.methodSelector = this.computeSelector();
this.returnDataTypes = [];
const dummy = new Byte({ type: 'byte', name: 'DUMMY' }); // @TODO TMP
_.each(abi.outputs, (dataItem: DataItem) => {
this.returnDataTypes.push(this.getFactory().create(dataItem, dummy));
});
}
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 encode(value: any, rules?: EncodingRules): string {
const calldata = super.encode(value, rules, this.selector);
return calldata;
}
public decode(calldata: string, rules?: DecodingRules): any[] | object {
if (!calldata.startsWith(this.selector)) {
throw new Error(
`Tried to decode calldata, but it was missing the function selector. Expected '${this.selector}'.`,
);
}
const hasSelector = true;
const value = super.decode(calldata, rules, hasSelector);
return value;
}
public decodeReturnValues(returndata: string, rules?: DecodingRules): any {
//console.log('O'.repeat(100), '\n', returndata, '\n', this.returnDataTypes, 'P'.repeat(100));
const returnValues: any[] = [];
const rules_ = rules ? rules : ({ structsAsObjects: false } as DecodingRules);
const rawReturnData = new RawCalldata(returndata, false);
_.each(this.returnDataTypes, (dataType: DataType) => {
returnValues.push(dataType.generateValue(rawReturnData, rules_));
});
//console.log('*'.repeat(40), '\n', JSON.stringify(returnValues), '\n', '*'.repeat(100));
/*if (returnValues.length === 1) {
return returnValues[0];
}*/
return returnValues;
}
public getSignature(): string {
return this.methodSignature;
}
public getSelector(): string {
return this.methodSelector;
}
}
export class EvmDataTypeFactory implements DataTypeFactory {
private static instance: DataTypeFactory;
private constructor() {}
public static getInstance(): DataTypeFactory {
if (!EvmDataTypeFactory.instance) {
EvmDataTypeFactory.instance = new EvmDataTypeFactory();
}
return EvmDataTypeFactory.instance;
}
public mapDataItemToDataType(dataItem: DataItem): DataType {
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;
}
if (parentDataType === undefined) {
// @Todo -- will this work for return values?
throw new Error(`Trying to create a pointer`);
}
const pointer = new Pointer(dataType, parentDataType);
return pointer;
}
}