trufflesuite/truffle

View on GitHub
packages/codec/lib/abify.ts

Summary

Maintainability
A
0 mins
Test Coverage
import debugModule from "debug";
const debug = debugModule("codec:abify");

import * as Format from "@truffle/codec/format";
import * as Common from "@truffle/codec/common";
import type {
  CalldataDecoding,
  LogDecoding,
  ReturndataDecoding
} from "@truffle/codec/types";
import * as Conversion from "@truffle/codec/conversion";

/** @category ABIfication */
export function abifyType(
  dataType: Format.Types.Type,
  userDefinedTypes?: Format.Types.TypesById
): Format.Types.AbiType | undefined {
  switch (dataType.typeClass) {
    //we only need to specially handle types that don't go in
    //the ABI, or that have some information loss when going
    //in the ABI
    //note that we do need to handle arrays, due to recursion!
    //First: types that do not go in the ABI
    case "mapping":
    case "magic":
    case "type":
    case "options":
      return undefined;
    //Next: address & contract, these can get handled together
    case "address":
    case "contract":
      return {
        typeClass: "address",
        kind: "general",
        typeHint: Format.Types.typeString(dataType)
      };
    case "function":
      switch (dataType.visibility) {
        case "external":
          return {
            typeClass: "function",
            visibility: "external",
            kind: "general",
            typeHint: Format.Types.typeString(dataType)
          };
        case "internal": //these don't go in the ABI
          return undefined;
      }
      break; //to satisfy TypeScript
    //the complex cases: struct & enum
    case "struct": {
      const fullType = <Format.Types.StructType>(
        Format.Types.fullType(dataType, userDefinedTypes)
      );
      if (!fullType.memberTypes) {
        let typeToDisplay = Format.Types.typeString(dataType);
        throw new Common.UnknownUserDefinedTypeError(
          dataType.id,
          typeToDisplay
        );
      }
      const memberTypes = fullType.memberTypes.map(
        ({ name, type: memberType }) => ({
          name,
          type: abifyType(memberType, userDefinedTypes)
        })
      );
      return {
        typeClass: "tuple",
        typeHint: Format.Types.typeString(fullType),
        memberTypes
      };
    }
    case "enum": {
      const fullType = <Format.Types.EnumType>(
        Format.Types.fullType(dataType, userDefinedTypes)
      );
      if (!fullType.options) {
        let typeToDisplay = Format.Types.typeString(dataType);
        throw new Common.UnknownUserDefinedTypeError(
          dataType.id,
          typeToDisplay
        );
      }
      let numOptions = fullType.options.length;
      let bits = 8 * Math.ceil(Math.log2(numOptions) / 8);
      return {
        typeClass: "uint",
        bits,
        typeHint: Format.Types.typeString(fullType)
      };
    }
    case "userDefinedValueType": {
      const fullType = <Format.Types.UserDefinedValueTypeType>(
        Format.Types.fullType(dataType, userDefinedTypes)
      );
      if (!fullType.underlyingType) {
        let typeToDisplay = Format.Types.typeString(dataType);
        throw new Common.UnknownUserDefinedTypeError(
          dataType.id,
          typeToDisplay
        );
      }
      const abifiedUnderlying = abifyType(
        fullType.underlyingType,
        userDefinedTypes
      );
      return {
        ...abifiedUnderlying,
        typeHint: Format.Types.typeStringWithoutLocation(dataType)
      };
    }
    //finally: arrays
    case "array":
      return {
        ...dataType,
        typeHint: Format.Types.typeString(dataType),
        baseType: abifyType(dataType.baseType, userDefinedTypes)
      };
    //default case: just leave as-is
    default:
      return dataType;
  }
}

/** @category ABIfication */
export function abifyResult(
  result: Format.Values.Result,
  userDefinedTypes?: Format.Types.TypesById
): Format.Values.AbiResult | undefined {
  switch (result.type.typeClass) {
    case "mapping": //doesn't go in ABI
    case "magic": //doesn't go in ABI
    case "type": //doesn't go in ABI
      return undefined;
    case "address":
      //abify the type but leave the value alone
      return {
        ...(<Format.Values.AddressResult>result),
        type: <Format.Types.AddressType>abifyType(result.type, userDefinedTypes)
      };
    case "contract": {
      let coercedResult = <Format.Values.ContractResult>result;
      switch (coercedResult.kind) {
        case "value":
          return {
            type: <Format.Types.AddressType>(
              abifyType(result.type, userDefinedTypes)
            ),
            kind: "value",
            value: {
              asAddress: coercedResult.value.address,
              rawAsHex: coercedResult.value.rawAddress
            },
            interpretations: coercedResult.interpretations
          };
        case "error":
          switch (coercedResult.error.kind) {
            case "ContractPaddingError":
              return {
                type: <Format.Types.AddressType>(
                  abifyType(result.type, userDefinedTypes)
                ),
                kind: "error",
                error: {
                  kind: "AddressPaddingError",
                  paddingType: coercedResult.error.paddingType,
                  raw: coercedResult.error.raw
                }
              };
            default:
              //other contract errors are generic errors!
              //but TS doesn't know this so we coerce
              return <Format.Errors.AddressErrorResult>{
                ...coercedResult,
                type: <Format.Types.AddressType>(
                  abifyType(result.type, userDefinedTypes)
                )
              };
          }
      }
      break; //to satisfy typescript
    }
    case "function":
      switch (result.type.visibility) {
        case "external": {
          let coercedResult = <Format.Values.FunctionExternalResult>result;
          return {
            ...coercedResult,
            type: <Format.Types.FunctionExternalType>(
              abifyType(result.type, userDefinedTypes)
            )
          };
        }
        case "internal": //these don't go in the ABI
          return undefined;
      }
      break; //to satisfy TypeScript
    case "struct": {
      let coercedResult = <Format.Values.StructResult>result;
      switch (coercedResult.kind) {
        case "value":
          if (coercedResult.reference !== undefined) {
            return undefined; //no circular values in the ABI!
          }
          let abifiedMembers = coercedResult.value.map(
            ({ name, value: member }) => ({
              name,
              value: abifyResult(member, userDefinedTypes)
            })
          );
          return {
            kind: "value",
            type: <Format.Types.TupleType>(
              abifyType(result.type, userDefinedTypes)
            ), //note: may throw exception
            value: abifiedMembers,
            interpretations: coercedResult.interpretations
          };
        case "error":
          return {
            ...coercedResult,
            type: <Format.Types.TupleType>(
              abifyType(result.type, userDefinedTypes)
            ) //note: may throw exception
          };
      }
    }
    case "userDefinedValueType": {
      const coercedResult = <Format.Values.UserDefinedValueTypeResult>result;
      switch (coercedResult.kind) {
        case "value":
          return abifyResult(coercedResult.value, userDefinedTypes);
        case "error":
          return <Format.Errors.BuiltInValueErrorResult>{
            //I have no idea what TS is thinking here
            ...coercedResult,
            type: abifyType(result.type, userDefinedTypes)
          };
      }
      break; //to satisfy TS :P
    }
    case "enum": {
      //NOTE: this is the one case where errors are converted to non-error values!!
      //(other than recursively, I mean)
      //be aware!
      let coercedResult = <Format.Values.EnumResult>result;
      let uintType = <Format.Types.UintType>(
        abifyType(result.type, userDefinedTypes)
      ); //may throw exception
      switch (coercedResult.kind) {
        case "value":
          return {
            type: uintType,
            kind: "value",
            value: {
              asBN: coercedResult.value.numericAsBN.clone()
            },
            interpretations: coercedResult.interpretations
          };
        case "error":
          switch (coercedResult.error.kind) {
            case "EnumOutOfRangeError":
              return {
                type: uintType,
                kind: "value",
                value: {
                  asBN: coercedResult.error.rawAsBN.clone()
                },
                interpretations: {}
              };
            case "EnumPaddingError":
              return {
                type: uintType,
                kind: "error",
                error: {
                  kind: "UintPaddingError",
                  paddingType: coercedResult.error.paddingType,
                  raw: coercedResult.error.raw
                }
              };
            case "EnumNotFoundDecodingError":
              let numericValue = coercedResult.error.rawAsBN.clone();
              if (numericValue.bitLength() <= uintType.bits) {
                return {
                  type: uintType,
                  kind: "value",
                  value: {
                    asBN: numericValue
                  },
                  interpretations: {}
                };
              } else {
                return {
                  type: uintType,
                  kind: "error",
                  error: {
                    kind: "UintPaddingError",
                    paddingType: "left", //we're dealing with ABI-encoded things so we can assume this
                    raw: Conversion.toHexString(numericValue)
                  }
                };
              }
            default:
              return {
                type: uintType,
                kind: "error",
                error: coercedResult.error
              };
          }
      }
    }
    case "array": {
      let coercedResult = <Format.Values.ArrayResult>result;
      switch (coercedResult.kind) {
        case "value":
          if (coercedResult.reference !== undefined) {
            return undefined; //no circular values in the ABI!
          }
          let abifiedMembers = coercedResult.value.map(member =>
            abifyResult(member, userDefinedTypes)
          );
          return {
            kind: "value",
            type: <Format.Types.ArrayType>(
              abifyType(result.type, userDefinedTypes)
            ),
            value: abifiedMembers,
            interpretations: coercedResult.interpretations
          };
        case "error":
          return {
            ...coercedResult,
            type: <Format.Types.ArrayType>(
              abifyType(result.type, userDefinedTypes)
            )
          };
      }
    }
    default:
      return <Format.Values.AbiResult>result; //just coerce :-/
  }
}

/** @category ABIfication */
export function abifyCalldataDecoding(
  decoding: CalldataDecoding,
  userDefinedTypes: Format.Types.TypesById
): CalldataDecoding {
  if (decoding.decodingMode === "abi") {
    return decoding;
  }
  switch (decoding.kind) {
    case "function":
    case "constructor":
      return {
        ...decoding,
        decodingMode: "abi",
        arguments: decoding.arguments.map(argument => ({
          ...argument,
          value: abifyResult(argument.value, userDefinedTypes)
        }))
      };
    default:
      return {
        ...decoding,
        decodingMode: "abi"
      };
  }
}

/** @category ABIfication */
export function abifyLogDecoding(
  decoding: LogDecoding,
  userDefinedTypes: Format.Types.TypesById
): LogDecoding {
  if (decoding.decodingMode === "abi") {
    return decoding;
  }
  return {
    ...decoding,
    decodingMode: "abi",
    arguments: decoding.arguments.map(argument => ({
      ...argument,
      value: abifyResult(argument.value, userDefinedTypes)
    }))
  };
}

/** @category ABIfication */
export function abifyReturndataDecoding(
  decoding: ReturndataDecoding,
  userDefinedTypes: Format.Types.TypesById
): ReturndataDecoding {
  if (decoding.decodingMode === "abi") {
    return decoding;
  }
  switch (decoding.kind) {
    case "return":
    case "revert":
      return {
        ...decoding,
        decodingMode: "abi",
        arguments: decoding.arguments.map(argument => ({
          ...argument,
          value: abifyResult(argument.value, userDefinedTypes)
        }))
      };
    case "bytecode":
      return {
        ...decoding,
        decodingMode: "abi",
        immutables: undefined
      };
    default:
      return {
        ...decoding,
        decodingMode: "abi"
      };
  }
}