packages/codec/lib/wrap/integer.ts
import debugModule from "debug";
const debug = debugModule("codec:wrap:integer");
import * as Format from "@truffle/codec/format";
import { wrapWithCases } from "./dispatch";
import { TypeMismatchError, BadResponseTypeError } from "./errors";
import type { WrapResponse, IntegerWrapRequest } from "../types";
import type {
Case,
IntegerOrEnumType,
IntegerOrEnumValue,
DecimalValue,
IntegerValue,
WrapOptions
} from "./types";
import * as Conversion from "@truffle/codec/conversion";
import * as Utils from "./utils";
import * as Messages from "./messages";
import BN from "bn.js";
import Big from "big.js";
import { validateUint8ArrayLike } from "./bytes";
//NOTE: all cases called "integerFrom..." also work for enums.
//The cases labeled "enumFrom..." work only for enums.
//there are no cases that work only for integers and not enums,
//because we always want input for integers to also be valid for enums.
const integerFromStringCases: Case<
IntegerOrEnumType,
IntegerOrEnumValue,
never
>[] = [
integerFromIntegerString,
enumFromNameString,
integerFromScientificOrUnits, //NOTE: please put this after the integer string case
integerFromNegatedBaseString, //NOTE: please put this after the other numeric string cases
integerFromStringFailureCase
];
//note: doesn't include UDVT case,
//or error case
const integerFromWrappedValueCases: Case<
IntegerOrEnumType,
IntegerOrEnumValue,
never
>[] = [
integerFromCodecIntegerValue,
integerFromCodecEnumValue,
integerFromCodecDecimalValue
];
const integerCasesBasic: Case<
IntegerOrEnumType,
IntegerOrEnumValue,
IntegerWrapRequest
>[] = [
...integerFromStringCases,
integerFromNumber,
integerFromBoxedNumber,
integerFromBoxedString,
integerFromBigint,
integerFromBN,
integerFromBig,
integerFromUint8ArrayLike,
...integerFromWrappedValueCases,
integerFromCodecEnumError,
integerFromCodecUdvtValue,
integerFromOther //must go last!
];
export const integerCases: Case<
IntegerOrEnumType,
IntegerOrEnumValue,
IntegerWrapRequest
>[] = [
integerFromIntegerTypeValueInput,
enumFromEnumTypeValueInput,
...integerCasesBasic
];
function* integerFromIntegerString(
dataType: IntegerOrEnumType,
input: unknown,
wrapOptions: WrapOptions
): Generator<never, IntegerOrEnumValue, WrapResponse> {
if (typeof input !== "string") {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
"Input was not a string"
);
}
if (input.trim() === "") {
//bigint accepts this but we shouldn't
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
5,
dataType.typeClass === "enum"
? Messages.badEnumMessage
: Messages.nonNumericMessage
);
}
const stripped = Utils.removeUnderscoresNumeric(input);
let asBN: BN;
try {
//we'll use BigInt to parse integer strings, as it's pretty good at it.
//Note that it accepts hex/octal/binary with prefixes 0x, 0o, 0b.
const asBigInt = BigInt(stripped);
asBN = Conversion.toBN(asBigInt);
} catch {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
"Input string was not an integer string"
);
}
return validateAndWrap(dataType, asBN, wrapOptions, input);
}
//this case handles both scientific notation, and numbers with units
function* integerFromScientificOrUnits(
dataType: IntegerOrEnumType,
input: unknown,
wrapOptions: WrapOptions
): Generator<never, IntegerOrEnumValue, WrapResponse> {
if (typeof input !== "string") {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
"Input was not a string"
);
}
if (input.trim() === "") {
//the code below accepts this but we shouldn't
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1, //only specificity 1 since it's already specificity 5 above
dataType.typeClass === "enum"
? Messages.badEnumMessage
: Messages.nonNumericMessage
);
}
const stripped = Utils.removeUnderscoresNoHex(input);
let [_, quantityString, unit] = stripped.match(
/^(.*?)(|wei|gwei|shannon|finney|szabo|ether)\s*$/i
); //units will be case insensitive; note this always matches
quantityString = quantityString.trim(); //Big rejects whitespace, let's allow it
const unitPlacesTable: { [unit: string]: number } = {
//we could accept all of web3's units here, but, that's a little much;
//we'll just accept the most common ones
"": 0,
"wei": 0,
"gwei": 9,
"shannon": 9,
"szabo": 12,
"finney": 15,
"ether": 18
};
let quantity: Big | null;
try {
quantity = quantityString.match(/^\s*$/)
? new Big(1) //allow just "ether" e.g.
: new Big(quantityString);
} catch {
quantity = null;
}
if (quantity === null) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
"Input was not a string using scientific notation or units"
);
}
const places: number = unitPlacesTable[unit.toLowerCase()];
const asBig = Conversion.shiftBigUp(quantity, places);
if (Conversion.countDecimalPlaces(asBig) !== 0) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
5,
unit !== ""
? "Input numeric value was not an integral number of wei"
: Messages.nonIntegerMessage
);
}
const asBN = Conversion.toBN(asBig);
return validateAndWrap(dataType, asBN, wrapOptions, input);
}
function* integerFromNegatedBaseString(
dataType: IntegerOrEnumType,
input: unknown,
wrapOptions: WrapOptions
): Generator<never, IntegerOrEnumValue, WrapResponse> {
if (typeof input !== "string") {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
"Input was not a string"
);
}
if (!input.match(/^\s*-/)) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
"Input was not a negated numeric string"
);
}
const stripped = Utils.removeUnderscoresNumeric(input);
let positiveAsBN: BN | null;
const [_, positiveString] = stripped.match(/^\s*-(.*)$/);
try {
const positive = BigInt(positiveString);
positiveAsBN = Conversion.toBN(positive);
} catch {
positiveAsBN = null;
}
if (
positiveAsBN === null ||
positiveString === "" ||
positiveString.match(/^(-|\s)/)
) {
//no double negation, no bare "-", and no space after the minus!
//(we do this as a string check, rather than checking if
//positiveAsBN is >=0, in order to prevent entering e.g. "--" or "- 2")
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
Messages.nonNumericMessage
);
}
const asBN = positiveAsBN.neg();
return validateAndWrap(dataType, asBN, wrapOptions, input);
}
function* enumFromNameString(
dataType: IntegerOrEnumType,
input: unknown,
wrapOptions: WrapOptions
): Generator<never, Format.Values.EnumValue, WrapResponse> {
if (typeof input !== "string") {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
"Input was not a string"
);
}
if (dataType.typeClass !== "enum") {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
Messages.nonNumericMessage
);
}
const fullType = <Format.Types.EnumType>(
Format.Types.fullType(dataType, wrapOptions.userDefinedTypes)
);
const options = fullType.options;
const components = input.split(".");
const finalComponent = components[components.length - 1];
debug("components: %O", components);
debug("dataType: %O", dataType);
debug("options: %O", options);
//the enum can be qualified. if it's qualified, does the type match?
let matchingType: boolean;
switch (components.length) {
case 1:
//not qualified, automatically matches
matchingType = true;
break;
case 2:
//qualified by type name, does it match?
matchingType = components[0] === dataType.typeName;
break;
case 3:
//qualified by type name and contract name, does it match?
matchingType =
dataType.kind === "local" &&
components[0] === dataType.definingContractName &&
components[1] === dataType.typeName;
break;
default:
//no valid reason to have 3 or more periods
//(and split cannot return an empty array)
matchingType = false;
}
debug("matchingType: %O", matchingType);
const numeric = matchingType ? options.indexOf(finalComponent) : -1; //if type doesn't match, just indicate error
debug("numeric: %d", numeric);
if (numeric === -1) {
//-1 comes from either our setting it manually above to indicate error,
//or from a failed indexOf call
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
Messages.badEnumMessage
);
}
const asBN = new BN(numeric); //whew!
//now: unlike in every other case, we can skip validation!
//so let's just wrap and return!
return {
type: dataType,
kind: "value" as const,
value: {
numericAsBN: asBN,
name: finalComponent //we know it matches!
},
interpretations: {}
};
}
function* integerFromStringFailureCase(
dataType: IntegerOrEnumType,
input: unknown,
wrapOptions: WrapOptions
): Generator<never, never, WrapResponse> {
if (typeof input !== "string") {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
"Input was not a string"
);
}
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
4,
dataType.typeClass === "enum"
? Messages.badEnumMessage
: Messages.nonNumericMessage
);
}
function* integerFromBN(
dataType: IntegerOrEnumType,
input: unknown,
wrapOptions: WrapOptions
): Generator<never, IntegerOrEnumValue, WrapResponse> {
if (!BN.isBN(input)) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
"Input was not a BN"
);
}
const asBN = input.clone();
return validateAndWrap(dataType, asBN, wrapOptions, input);
}
function* integerFromBigint(
dataType: IntegerOrEnumType,
input: unknown,
wrapOptions: WrapOptions
): Generator<never, IntegerOrEnumValue, WrapResponse> {
if (typeof input !== "bigint") {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
"Input was not a bigint"
);
}
const asBN = Conversion.toBN(input);
return validateAndWrap(dataType, asBN, wrapOptions, input);
}
function* integerFromNumber(
dataType: IntegerOrEnumType,
input: unknown,
wrapOptions: WrapOptions
): Generator<never, IntegerOrEnumValue, WrapResponse> {
if (typeof input !== "number") {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
"Input was not a number"
);
}
if (!Number.isInteger(input)) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
5,
Messages.nonIntegerMessage
);
}
if (!Number.isSafeInteger(input)) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
5,
Messages.nonSafeMessage
);
}
const asBN = new BN(input);
return validateAndWrap(dataType, asBN, wrapOptions, input);
}
function* integerFromBig(
dataType: IntegerOrEnumType,
input: unknown,
wrapOptions: WrapOptions
): Generator<never, IntegerOrEnumValue, WrapResponse> {
if (!Conversion.isBig(input)) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
"Input was not a Big"
);
}
if (Conversion.countDecimalPlaces(input) !== 0) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
5,
Messages.nonIntegerMessage
);
}
const asBN = Conversion.toBN(input);
return validateAndWrap(dataType, asBN, wrapOptions, input);
}
function* integerFromUint8ArrayLike(
dataType: IntegerOrEnumType,
input: unknown,
wrapOptions: WrapOptions
): Generator<never, IntegerOrEnumValue, WrapResponse> {
if (!Utils.isUint8ArrayLike(input)) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
"Input was not a Uint8Array-like"
);
}
//the next series of checks is delegated to a helper fn
validateUint8ArrayLike(input, dataType, wrapOptions.name); //(this fn just throws an appropriate error if something's bad)
const asBN = Conversion.toBN(new Uint8Array(input)); //I am surprised TS accepts this!
return validateAndWrap(dataType, asBN, wrapOptions, input);
}
function* integerFromBoxedNumber(
dataType: IntegerOrEnumType,
input: unknown,
wrapOptions: WrapOptions
): Generator<never, IntegerOrEnumValue, WrapResponse> {
if (!Utils.isBoxedNumber(input)) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
"Input was not a boxed number"
);
}
//unbox and try again
return yield* integerFromNumber(dataType, input.valueOf(), wrapOptions);
}
function* integerFromBoxedString(
dataType: IntegerOrEnumType,
input: unknown,
wrapOptions: WrapOptions
): Generator<never, IntegerOrEnumValue, WrapResponse> {
if (!Utils.isBoxedString(input)) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
"Input was not a boxed string"
);
}
//unbox and try again
return yield* wrapWithCases(
dataType,
input.valueOf(),
wrapOptions,
integerFromStringCases
);
}
function* integerFromCodecIntegerValue(
dataType: IntegerOrEnumType,
input: unknown,
wrapOptions: WrapOptions
): Generator<never, IntegerOrEnumValue, WrapResponse> {
if (!Utils.isWrappedResult(input)) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
"Input was not a wrapped result"
);
}
if (input.type.typeClass !== "int" && input.type.typeClass !== "uint") {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
5,
Messages.wrappedTypeMessage(input.type)
);
}
if (input.kind !== "value") {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
5,
Messages.errorResultMessage
);
}
if (
!wrapOptions.loose &&
(input.type.typeClass !== dataType.typeClass ||
input.type.bits !== dataType.bits)
) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
5,
Messages.wrappedTypeMessage(input.type)
);
}
const asBN = (<IntegerValue>input).value.asBN.clone();
return validateAndWrap(dataType, asBN, wrapOptions, input);
}
function* integerFromCodecDecimalValue(
dataType: IntegerOrEnumType,
input: unknown,
wrapOptions: WrapOptions
): Generator<never, IntegerOrEnumValue, WrapResponse> {
if (!Utils.isWrappedResult(input)) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
"Input was not a wrapped result"
);
}
if (input.type.typeClass !== "fixed" && input.type.typeClass !== "ufixed") {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
5,
Messages.wrappedTypeMessage(input.type)
);
}
if (input.kind !== "value") {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
5,
Messages.errorResultMessage
);
}
if (!wrapOptions.loose) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
5,
Messages.wrappedTypeMessage(input.type)
);
}
const asBN = Conversion.toBN((<DecimalValue>input).value.asBig);
return validateAndWrap(dataType, asBN, wrapOptions, input);
}
function* integerFromCodecEnumValue(
dataType: IntegerOrEnumType,
input: unknown,
wrapOptions: WrapOptions
): Generator<never, IntegerOrEnumValue, WrapResponse> {
if (!Utils.isWrappedResult(input)) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
"Input was not a wrapped result"
);
}
if (input.type.typeClass !== "enum") {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
5,
Messages.wrappedTypeMessage(input.type)
);
}
if (input.kind !== "value") {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1, //only specificity 1 due to EnumError case
Messages.errorResultMessage
);
}
if (
!wrapOptions.loose &&
(dataType.typeClass !== "enum" || input.type.id !== dataType.id)
) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
5,
Messages.wrappedTypeMessage(input.type)
);
}
const asBN = (<Format.Values.EnumValue>input).value.numericAsBN.clone();
return validateAndWrap(dataType, asBN, wrapOptions, input);
}
function* integerFromCodecEnumError(
dataType: IntegerOrEnumType,
input: unknown,
wrapOptions: WrapOptions
): Generator<never, IntegerOrEnumValue, WrapResponse> {
if (!Utils.isWrappedResult(input)) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
"Input was not a wrapped result"
);
}
if (input.type.typeClass !== "enum") {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
5,
Messages.wrappedTypeMessage(input.type)
);
}
if (input.kind !== "error") {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
"Wrapped result was a value rather than an error"
);
}
if (
!wrapOptions.loose &&
(dataType.typeClass !== "enum" || input.type.id !== dataType.id)
) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
5,
Messages.wrappedTypeMessage(input.type)
);
}
const coercedInput = <Format.Errors.EnumErrorResult>input;
//only one specific kind of error will be allowed
if (coercedInput.error.kind !== "EnumOutOfRangeError") {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
5,
Messages.errorResultMessage
);
}
const asBN = coercedInput.error.rawAsBN.clone();
return validateAndWrap(dataType, asBN, wrapOptions, input);
}
function* integerFromCodecUdvtValue(
dataType: IntegerOrEnumType,
input: unknown,
wrapOptions: WrapOptions
): Generator<never, IntegerOrEnumValue, WrapResponse> {
if (!Utils.isWrappedResult(input)) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
"Input was not a wrapped result"
);
}
if (input.type.typeClass !== "userDefinedValueType") {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
Messages.wrappedTypeMessage(input.type)
);
}
if (input.kind !== "value") {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
5,
Messages.errorResultMessage
);
}
return yield* wrapWithCases(
dataType,
input.value,
wrapOptions,
integerFromWrappedValueCases
);
}
function* integerFromIntegerTypeValueInput(
dataType: IntegerOrEnumType,
input: unknown,
wrapOptions: WrapOptions
): Generator<IntegerWrapRequest, IntegerOrEnumValue, WrapResponse> {
if (!Utils.isTypeValueInput(input)) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
"Input was not a type/value pair"
);
}
if (!input.type.match(/^u?int\d*$/)) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
dataType.typeClass === "enum" ? 1 : 5,
//use specificity 5 when getting an integer (which have no alternative),
//but specificity 1 when getting an enum (which have enum type/value input also)
Messages.specifiedTypeMessage(input.type)
);
}
const [_, typeClass, bitsAsString] = input.type.match(/^(u?int)(\d*)$/);
const bits = bitsAsString ? Number(bitsAsString) : 256; //defaults to 256
//(not using the WORD_SIZE constant due to fixed types bringing its applicability
//here into question)
const requiredTypeClass =
dataType.typeClass !== "enum" ? dataType.typeClass : "uint"; //allow underlying uint type to work for enums
//(we handle "enum" given as type in a separate case below)
const requiredBits =
dataType.typeClass !== "enum"
? dataType.bits
: 8 *
Math.ceil(
Math.log2(
(<Format.Types.EnumType>(
Format.Types.fullType(dataType, wrapOptions.userDefinedTypes)
)).options.length
) / 8
); //compute required bits for enum type (sorry)
if (requiredTypeClass !== typeClass || requiredBits !== bits) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
5,
Messages.specifiedTypeMessage(input.type)
);
}
//extract value & try again, with loose option turned on
return yield* wrapWithCases(
dataType,
input.value,
{ ...wrapOptions, loose: true },
integerCasesBasic
);
}
function* enumFromEnumTypeValueInput(
dataType: IntegerOrEnumType,
input: unknown,
wrapOptions: WrapOptions
): Generator<IntegerWrapRequest, Format.Values.EnumValue, WrapResponse> {
if (!Utils.isTypeValueInput(input)) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
1,
"Input was not a type/value pair"
);
}
if (input.type !== "enum") {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
dataType.typeClass === "enum" ? 5 : 1,
//use specificity 5 when getting an enum (which will have also failed integer type/value input),
//but specificity 1 when getting an integer (to which this doesn't really apply)
Messages.specifiedTypeMessage(input.type)
);
}
if (dataType.typeClass !== "enum") {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
5,
Messages.specifiedTypeMessage(input.type)
);
}
//extract value & try again, with loose option turned on
//(we'll also coerce the type on this one since we know it's
//going to be an enum value :P )
return <Format.Values.EnumValue>(
yield* wrapWithCases(
dataType,
input.value,
{ ...wrapOptions, loose: true },
integerCasesBasic
)
);
}
function* integerFromOther(
dataType: IntegerOrEnumType,
input: unknown,
wrapOptions: WrapOptions
): Generator<IntegerWrapRequest, IntegerOrEnumValue, WrapResponse> {
const request = { kind: "integer" as const, input };
const response = yield request;
if (response.kind !== "integer") {
throw new BadResponseTypeError(request, response);
}
if (response.value === null) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
response.partiallyRecognized ? 5 : 3,
response.reason || Messages.unrecognizedNumberMessage(dataType)
);
}
const asBN = Conversion.toBN(response.value);
return validateAndWrap(dataType, asBN, wrapOptions, input);
}
function validateAndWrap(
dataType: IntegerOrEnumType,
asBN: BN,
wrapOptions: WrapOptions,
input: unknown //just for erroring
): IntegerOrEnumValue {
switch (dataType.typeClass) {
case "uint":
if (asBN.isNeg() || asBN.bitLength() > dataType.bits) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
5,
Messages.outOfRangeMessage
);
}
return {
type: dataType,
kind: "value" as const,
value: {
asBN
},
interpretations: {}
};
case "int":
if (
(!asBN.isNeg() && asBN.bitLength() >= dataType.bits) || //>= since signed
(asBN.isNeg() && asBN.neg().subn(1).bitLength() >= dataType.bits)
//bitLength doesn't work great for negatives so we do this instead
) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
5,
Messages.outOfRangeMessage
);
}
return {
type: dataType,
kind: "value" as const,
value: {
asBN
},
interpretations: {}
};
case "enum":
const fullType = <Format.Types.EnumType>(
Format.Types.fullType(dataType, wrapOptions.userDefinedTypes)
);
if (asBN.isNeg() || asBN.gten(fullType.options.length)) {
throw new TypeMismatchError(
dataType,
input,
wrapOptions.name,
5,
Messages.outOfRangeEnumMessage
);
}
return {
type: dataType,
kind: "value" as const,
value: {
numericAsBN: asBN,
name: fullType.options[asBN.toNumber()]
},
interpretations: {}
};
}
}