aboutsummaryrefslogblamecommitdiffstats
path: root/packages/utils/src/abi_encoder/evm_data_types.ts
blob: 2895ee00f787fe7c5b550d42c955e2ce05d31d13 (plain) (tree)
1
2
3
4
5
6
7
8
9
                                                                                                            

                                                          




                                                     
                                                   
 
                                                    
 

                          


                                            
                                                   



                                                              

                                                                                            

                                     
                                                                                              












                                                                                        







                                                                              
                                
                                                                                   

                               






                                                       





                                                              
                                                                                           












                                                                                     
                                                




                                                                                                    










                                                                                              








                                                              
                                                                                             










                                                                                       

                                                                     
                                                    
                                                                                                        
                                                        
                                                                                                        


































                                                                                                                   
                                                          


                                                                   


                                      

                                                                         

                                                

                                                   





                                                                                   
                                                            

                                                  

                                                              


             
                     

     

































































                                                                                                                                       
                                                                                           















                                                                                     
                                                        



                                                                                              

















                                                                                                             






                                                             










                                                               
                                                                                            






                                                                                      
                                                                                                             













                                                                                                             










                                                                











                                                               
                                                                                                




                                                                                       
                                               

                                                           
                                                                                        




                                                                                    






                                                                



                                                 













                                                                                                              
                                                                                        










                                              
                                                          































                                                                                      
                                            
                                                                                       
                                                                                                  
                                            











                                                         
                                                                                  



















                                                       
                                        
 


                            
                                 
                                                                                                            
                                                       
                                                                     




                                                                                 













                                                                                                    


                                                                   

     


                                                                                                                                
         

                                                                 


                     
















                                                                                                      








                                    










                                                                   

                                                                














                                                                                  
                                                                            




                                                              


                                                                                         



                                                              
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;
    }
}