@truffle/codec
Version:
Library for encoding and decoding smart contract data
847 lines • 33.5 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkPaddingLeft = exports.decodeExternalFunction = exports.decodeContract = exports.decodeBasic = void 0;
const debug_1 = __importDefault(require("debug"));
const debug = (0, debug_1.default)("codec:basic:decode");
const read_1 = __importDefault(require("../../read"));
const Conversion = __importStar(require("../../conversion"));
const Format = __importStar(require("../../format"));
const Contexts = __importStar(require("../../contexts"));
const Evm = __importStar(require("../../evm"));
const errors_1 = require("../../errors");
const allocate_1 = require("../allocate");
const decode_1 = require("../../bytes/decode");
function* decodeBasic(dataType, pointer, info, options = {}) {
const { state } = info;
const { strictAbiMode: strict } = options; //if this is undefined it'll still be falsy so it's OK
const paddingMode = options.paddingMode || "default";
let bytes;
let rawBytes;
try {
bytes = yield* (0, read_1.default)(pointer, state);
}
catch (error) {
debug("segfault, pointer %o, state: %O", pointer, state);
return (0, errors_1.handleDecodingError)(dataType, error, strict);
}
rawBytes = bytes;
debug("type %O", dataType);
debug("pointer %o", pointer);
switch (dataType.typeClass) {
case "userDefinedValueType": {
const fullType = (Format.Types.fullType(dataType, info.userDefinedTypes));
if (!fullType.underlyingType) {
const error = {
kind: "UserDefinedTypeNotFoundError",
type: fullType
};
if (strict || options.allowRetry) {
throw new errors_1.StopDecodingError(error, true);
//note that we allow a retry if we couldn't locate the underlying type!
}
return {
type: fullType,
kind: "error",
error
};
}
const underlyingResult = yield* decodeBasic(fullType.underlyingType, pointer, info, options);
switch (underlyingResult.kind //yes this switch is a little unnecessary :P
) {
case "value":
//wrap the value and return
return {
//no idea why need coercion here
type: fullType,
kind: "value",
value: underlyingResult,
interpretations: {}
};
case "error":
//wrap the error and return an error result!
//this is inconsistent with how we handle other container types
//(structs, arrays, mappings), where having an error in one element
//does not cause an error in the whole thing, but to do that here
//would cause problems for the type system :-/
//so we'll just be inconsistent
return {
//TS is being bad again :-/
type: fullType,
kind: "error",
error: {
kind: "WrappedError",
error: underlyingResult
}
};
}
break; //to satisfy TS :P
}
case "bool": {
if (!checkPadding(bytes, dataType, paddingMode)) {
let error = {
kind: "BoolPaddingError",
paddingType: getPaddingType(dataType, paddingMode),
raw: Conversion.toHexString(bytes)
};
if (strict) {
throw new errors_1.StopDecodingError(error);
}
return {
type: dataType,
kind: "error",
error
};
}
bytes = removePadding(bytes, dataType, paddingMode);
//note: the use of the BN is a little silly here,
//but, kind of stuck with it for now
const numeric = Conversion.toBN(bytes);
if (numeric.eqn(0)) {
return {
type: dataType,
kind: "value",
value: { asBoolean: false },
interpretations: {}
};
}
else if (numeric.eqn(1)) {
return {
type: dataType,
kind: "value",
value: { asBoolean: true },
interpretations: {}
};
}
else {
let error = {
kind: "BoolOutOfRangeError",
rawAsBN: numeric
};
if (strict) {
throw new errors_1.StopDecodingError(error);
}
return {
type: dataType,
kind: "error",
error
};
}
}
case "uint":
//first, check padding (if needed)
if (!checkPadding(bytes, dataType, paddingMode)) {
let error = {
kind: "UintPaddingError",
paddingType: getPaddingType(dataType, paddingMode),
raw: Conversion.toHexString(bytes)
};
if (strict) {
throw new errors_1.StopDecodingError(error);
}
return {
type: dataType,
kind: "error",
error
};
}
//now, truncate to appropriate length
bytes = removePadding(bytes, dataType, paddingMode);
return {
type: dataType,
kind: "value",
value: {
asBN: Conversion.toBN(bytes),
rawAsBN: Conversion.toBN(rawBytes)
},
interpretations: {}
};
case "int":
//first, check padding (if needed)
if (!checkPadding(bytes, dataType, paddingMode)) {
let error = {
kind: "IntPaddingError",
paddingType: getPaddingType(dataType, paddingMode),
raw: Conversion.toHexString(bytes)
};
if (strict) {
throw new errors_1.StopDecodingError(error);
}
return {
type: dataType,
kind: "error",
error
};
}
//now, truncate to appropriate length (keeping the bytes on the right)
bytes = removePadding(bytes, dataType, paddingMode);
return {
type: dataType,
kind: "value",
value: {
asBN: Conversion.toSignedBN(bytes),
rawAsBN: Conversion.toSignedBN(rawBytes)
},
interpretations: {}
};
case "address": {
if (!checkPadding(bytes, dataType, paddingMode)) {
let error = {
kind: "AddressPaddingError",
paddingType: getPaddingType(dataType, paddingMode),
raw: Conversion.toHexString(bytes)
};
if (strict) {
throw new errors_1.StopDecodingError(error);
}
return {
type: dataType,
kind: "error",
error
};
}
bytes = removePadding(bytes, dataType, paddingMode);
const address = Evm.Utils.toAddress(bytes);
let decoded = {
type: dataType,
kind: "value",
value: {
asAddress: address,
rawAsHex: Conversion.toHexString(rawBytes)
},
interpretations: {}
};
//now: attach interpretations
const ensName = yield* reverseEnsResolve(address);
if (ensName !== null) {
decoded.interpretations.ensName = ensName;
}
//yes, this makes the contract/address distinction a little silly
const contractValueInfo = yield* decodeContract(bytes, info);
if (contractValueInfo.kind === "known") {
decoded.interpretations.contractClass = contractValueInfo.class;
}
return decoded;
}
case "contract": {
if (!checkPadding(bytes, dataType, paddingMode)) {
let error = {
kind: "ContractPaddingError",
paddingType: getPaddingType(dataType, paddingMode),
raw: Conversion.toHexString(bytes)
};
if (strict) {
throw new errors_1.StopDecodingError(error);
}
return {
type: dataType,
kind: "error",
error
};
}
bytes = removePadding(bytes, dataType, paddingMode);
const fullType = (Format.Types.fullType(dataType, info.userDefinedTypes));
const contractValueInfo = yield* decodeContract(bytes, info);
let decoded = {
type: fullType,
kind: "value",
value: contractValueInfo,
interpretations: {}
};
//now: attach interpretations
const ensName = yield* reverseEnsResolve(contractValueInfo.address);
if (ensName !== null) {
decoded.interpretations = { ensName };
}
return decoded;
}
case "bytes":
//NOTE: we assume this is a *static* bytestring,
//because this is decodeBasic! dynamic ones should
//go to decodeBytes!
let coercedDataType = dataType;
//first, check padding (if needed)
if (!checkPadding(bytes, dataType, paddingMode)) {
let error = {
kind: "BytesPaddingError",
paddingType: getPaddingType(dataType, paddingMode),
raw: Conversion.toHexString(bytes)
};
if (strict) {
throw new errors_1.StopDecodingError(error);
}
return {
type: coercedDataType,
kind: "error",
error
};
}
//now, truncate to appropriate length
bytes = removePadding(bytes, dataType, paddingMode);
return {
type: coercedDataType,
kind: "value",
value: {
asHex: Conversion.toHexString(bytes),
rawAsHex: Conversion.toHexString(rawBytes)
},
interpretations: {}
};
case "function":
switch (dataType.visibility) {
case "external":
if (!checkPadding(bytes, dataType, paddingMode)) {
const error = {
kind: "FunctionExternalNonStackPaddingError",
paddingType: getPaddingType(dataType, paddingMode),
raw: Conversion.toHexString(bytes)
};
if (strict) {
throw new errors_1.StopDecodingError(error);
}
return {
type: dataType,
kind: "error",
error
};
}
bytes = removePadding(bytes, dataType, paddingMode);
const address = bytes.slice(0, Evm.Utils.ADDRESS_SIZE);
const selector = bytes.slice(Evm.Utils.ADDRESS_SIZE, Evm.Utils.ADDRESS_SIZE + Evm.Utils.SELECTOR_SIZE);
const valueInfo = yield* decodeExternalFunction(address, selector, info);
let decoded = {
type: dataType,
kind: "value",
value: valueInfo,
interpretations: {}
};
//now: attach interpretations
const contractEnsName = yield* reverseEnsResolve(valueInfo.contract.address);
if (contractEnsName !== null) {
decoded.interpretations = { contractEnsName };
}
return decoded;
case "internal":
//note: we used to error if we hit this point with strict === true,
//since internal function pointers don't go in the ABI, and strict
//mode is intended for ABI decoding. however, there are times when
//we want to use strict mode to decode immutables, and immutables can
//include internal function pointers. so now we allow this. yes,
//this is a bit of an abuse of strict mode, which was after all meant
//for ABI decoding, but oh well.
if (!checkPadding(bytes, dataType, paddingMode)) {
const error = {
kind: "FunctionInternalPaddingError",
paddingType: getPaddingType(dataType, paddingMode),
raw: Conversion.toHexString(bytes)
};
if (strict) {
throw new errors_1.StopDecodingError(error);
}
return {
type: dataType,
kind: "error",
error
};
}
bytes = removePadding(bytes, dataType, paddingMode);
return decodeInternalFunction(dataType, bytes, info, strict);
}
break; //to satisfy TypeScript
case "enum": {
let numeric = Conversion.toBN(bytes);
const fullType = (Format.Types.fullType(dataType, info.userDefinedTypes));
if (!fullType.options) {
let error = {
kind: "EnumNotFoundDecodingError",
type: fullType,
rawAsBN: numeric
};
if (strict || options.allowRetry) {
throw new errors_1.StopDecodingError(error, true);
//note that we allow a retry if we couldn't locate the enum type!
}
return {
type: fullType,
kind: "error",
error
};
}
//note: I'm doing the padding checks a little more manually on this one
//so that we can have the right type of error
const numOptions = fullType.options.length;
const numBytes = Math.ceil(Math.log2(numOptions) / 8);
const paddingType = getPaddingType(dataType, paddingMode);
if (!checkPaddingDirect(bytes, numBytes, paddingType)) {
let error = {
kind: "EnumPaddingError",
type: fullType,
paddingType,
raw: Conversion.toHexString(bytes)
};
if (strict) {
throw new errors_1.StopDecodingError(error);
}
return {
type: dataType,
kind: "error",
error
};
}
bytes = removePaddingDirect(bytes, numBytes, paddingType);
numeric = Conversion.toBN(bytes); //alter numeric!
if (numeric.ltn(numOptions)) {
const name = fullType.options[numeric.toNumber()];
//NOTE: despite the use of toNumber(), I'm NOT catching exceptions here and returning an
//error value like elsewhere; I'm just letting this one fail. Why? Because if we have
//an enum with that many options in the first place, we have bigger problems!
return {
type: fullType,
kind: "value",
value: {
name,
numericAsBN: numeric
},
interpretations: {}
};
}
else {
let error = {
kind: "EnumOutOfRangeError",
type: fullType,
rawAsBN: numeric
};
if (strict) {
//note:
//if the enum is merely out of range rather than out of the ABI range,
//we do NOT throw an error here! instead we simply return an error value,
//which we normally avoid doing in strict mode. (the error will be caught
//later at the re-encoding step instead.) why? because we might be running
//in ABI mode, so we may need to abify this "value" rather than just throwing
//it out.
throw new errors_1.StopDecodingError(error);
//note that we do NOT allow a retry here!
//if we *can* find the enum type but the value is out of range,
//we *know* that it is invalid!
}
return {
type: fullType,
kind: "error",
error
};
}
}
case "fixed": {
//first, check padding (if needed)
if (!checkPadding(bytes, dataType, paddingMode)) {
let error = {
kind: "FixedPaddingError",
paddingType: getPaddingType(dataType, paddingMode),
raw: Conversion.toHexString(bytes)
};
if (strict) {
throw new errors_1.StopDecodingError(error);
}
return {
type: dataType,
kind: "error",
error
};
}
//now, truncate to appropriate length (keeping the bytes on the right)
bytes = removePadding(bytes, dataType, paddingMode);
let asBN = Conversion.toSignedBN(bytes);
let rawAsBN = Conversion.toSignedBN(rawBytes);
let asBig = Conversion.shiftBigDown(Conversion.toBig(asBN), dataType.places);
let rawAsBig = Conversion.shiftBigDown(Conversion.toBig(rawAsBN), dataType.places);
return {
type: dataType,
kind: "value",
value: {
asBig,
rawAsBig
},
interpretations: {}
};
}
case "ufixed": {
//first, check padding (if needed)
if (!checkPadding(bytes, dataType, paddingMode)) {
let error = {
kind: "UfixedPaddingError",
paddingType: getPaddingType(dataType, paddingMode),
raw: Conversion.toHexString(bytes)
};
if (strict) {
throw new errors_1.StopDecodingError(error);
}
return {
type: dataType,
kind: "error",
error
};
}
//now, truncate to appropriate length (keeping the bytes on the right)
bytes = removePadding(bytes, dataType, paddingMode);
let asBN = Conversion.toBN(bytes);
let rawAsBN = Conversion.toBN(rawBytes);
let asBig = Conversion.shiftBigDown(Conversion.toBig(asBN), dataType.places);
let rawAsBig = Conversion.shiftBigDown(Conversion.toBig(rawAsBN), dataType.places);
return {
type: dataType,
kind: "value",
value: {
asBig,
rawAsBig
},
interpretations: {}
};
}
}
}
exports.decodeBasic = decodeBasic;
//NOTE that this function returns a ContractValueInfo, not a ContractResult
function* decodeContract(addressBytes, info) {
return (yield* decodeContractAndContext(addressBytes, info)).contractInfo;
}
exports.decodeContract = decodeContract;
function* decodeContractAndContext(addressBytes, info) {
let address = Evm.Utils.toAddress(addressBytes);
let rawAddress = Conversion.toHexString(addressBytes);
let codeBytes = yield {
type: "code",
address
};
let code = Conversion.toHexString(codeBytes);
let context = Contexts.Utils.findContext(info.contexts, code);
if (context !== null) {
return {
context,
contractInfo: {
kind: "known",
address,
rawAddress,
class: Contexts.Import.contextToType(context)
}
};
}
else {
return {
context,
contractInfo: {
kind: "unknown",
address,
rawAddress
}
};
}
}
//note: address can have extra zeroes on the left like elsewhere, but selector should be exactly 4 bytes
//NOTE this again returns a FunctionExternalValueInfo, not a FunctionExternalResult
function* decodeExternalFunction(addressBytes, selectorBytes, info) {
let { contractInfo: contract, context } = yield* decodeContractAndContext(addressBytes, info);
let selector = Conversion.toHexString(selectorBytes);
if (contract.kind === "unknown") {
return {
kind: "unknown",
contract,
selector
};
}
let abiEntry = context.abi !== undefined ? context.abi[selector] : undefined;
if (abiEntry === undefined) {
return {
kind: "invalid",
contract,
selector
};
}
return {
kind: "known",
contract,
selector,
abi: abiEntry
};
}
exports.decodeExternalFunction = decodeExternalFunction;
//this one works a bit differently -- in order to handle errors, it *does* return a FunctionInternalResult
function decodeInternalFunction(dataType, bytes, info, strict) {
const rawInfoFormat = info.internalFunctionsTableKind || "index";
//we'll default to "index" if it's not specified, not because that's
//a reasonable default (we want to avoid ever hitting a default here,
//debugger/decoder will set their own defaults as appropriate), but because
//index doesn't attempt to, like, parse things, so it's better for reporting
//errors if we have no clue what's going on
let raw;
switch (rawInfoFormat) {
case "pcpair":
const deployedPcBytes = bytes.slice(-Evm.Utils.PC_SIZE);
const constructorPcBytes = bytes.slice(-Evm.Utils.PC_SIZE * 2, -Evm.Utils.PC_SIZE);
const deployedPc = Conversion.toBN(deployedPcBytes).toNumber();
const constructorPc = Conversion.toBN(constructorPcBytes).toNumber();
raw = {
kind: "pcpair",
deployedProgramCounter: deployedPc,
constructorProgramCounter: constructorPc
};
break;
case "index":
const index = Conversion.toBN(bytes).toNumber();
raw = {
kind: "index",
functionIndex: index
};
break;
}
const context = Contexts.Import.contextToType(info.currentContext);
//before anything else: do we even have an internal functions table?
//if not, we'll just return the info we have without really attemting to decode
if (!info.internalFunctionsTable || !info.internalFunctionsTableKind) {
//note we end up here if the table kind wasn't set; the "index" default above
//is *only* for error handling
return {
type: dataType,
kind: "value",
value: {
kind: "unknown",
context,
rawInformation: raw
},
interpretations: {}
};
}
//this switch block handles exceptional cases that are only relevant in the pcpair case
if (raw.kind === "pcpair") {
//defining these for convenience
const { deployedProgramCounter: deployedPc, constructorProgramCounter: constructorPc } = raw;
//also before we continue: is the PC zero? if so let's just return that
if (deployedPc === 0 && constructorPc === 0) {
return {
type: dataType,
kind: "value",
value: {
kind: "exception",
context,
rawInformation: raw
},
interpretations: {}
};
}
//another check: is only the deployed PC zero?
if (deployedPc === 0 && constructorPc !== 0) {
const error = {
kind: "MalformedInternalFunctionError",
context,
rawInformation: raw
};
if (strict) {
throw new errors_1.StopDecodingError(error);
}
return {
type: dataType,
kind: "error",
error
};
}
//one last pre-check: is this a deployed-format pointer in a constructor?
if (info.currentContext.isConstructor && constructorPc === 0) {
const error = {
kind: "DeployedFunctionInConstructorError",
context,
rawInformation: raw
};
if (strict) {
throw new errors_1.StopDecodingError(error);
}
return {
type: dataType,
kind: "error",
error
};
}
}
//if we didn't hit any of those exceptional cases, we now attempt to look
//up the function in the table
let functionEntry;
switch (raw.kind) {
case "pcpair":
const pc = info.currentContext.isConstructor
? raw.constructorProgramCounter
: raw.deployedProgramCounter;
functionEntry = info.internalFunctionsTable[pc];
break;
case "index":
functionEntry = info.internalFunctionsTable[raw.functionIndex];
break;
}
if (!functionEntry) {
//If we didn't find an entry, this is an error
const error = {
kind: "NoSuchInternalFunctionError",
context,
rawInformation: raw
};
if (strict) {
throw new errors_1.StopDecodingError(error);
}
return {
type: dataType,
kind: "error",
error
};
}
//finally, the rest of this handles the case where we did find an entry,
//and doesn't need to switch on the raw info type anymore :)
if (functionEntry.isDesignatedInvalid) {
return {
type: dataType,
kind: "value",
value: {
kind: "exception",
context,
rawInformation: raw
},
interpretations: {}
};
}
const name = functionEntry.name;
const mutability = functionEntry.mutability;
const definedIn = Evm.Import.functionTableEntryToType(functionEntry); //may be null
const id = Evm.Import.makeInternalFunctionId(functionEntry);
return {
type: dataType,
kind: "value",
value: {
kind: "function",
context,
rawInformation: raw,
name,
id,
definedIn,
mutability
},
interpretations: {}
};
}
function checkPadding(bytes, dataType, paddingMode, userDefinedTypes) {
const length = (0, allocate_1.byteLength)(dataType, userDefinedTypes);
const paddingType = getPaddingType(dataType, paddingMode);
if (paddingMode === "permissive") {
switch (dataType.typeClass) {
case "bool":
case "enum":
case "function":
//these three types are checked even in permissive mode
return checkPaddingDirect(bytes, length, paddingType);
default:
return true;
}
}
else {
return checkPaddingDirect(bytes, length, paddingType);
}
}
function removePadding(bytes, dataType, paddingMode, userDefinedTypes) {
const length = (0, allocate_1.byteLength)(dataType, userDefinedTypes);
const paddingType = getPaddingType(dataType, paddingMode);
return removePaddingDirect(bytes, length, paddingType);
}
function removePaddingDirect(bytes, length, paddingType) {
switch (paddingType) {
case "right":
return bytes.slice(0, length);
default:
return bytes.slice(-length);
}
}
function checkPaddingDirect(bytes, length, paddingType) {
switch (paddingType) {
case "left":
return checkPaddingLeft(bytes, length);
case "right":
return checkPaddingRight(bytes, length);
case "signed":
return checkPaddingSigned(bytes, length);
case "signedOrLeft":
return (checkPaddingSigned(bytes, length) || checkPaddingLeft(bytes, length));
}
}
function getPaddingType(dataType, paddingMode) {
switch (paddingMode) {
case "right":
return "right";
case "default":
case "permissive":
return defaultPaddingType(dataType);
case "zero": {
const defaultType = defaultPaddingType(dataType);
return defaultType === "signed" ? "left" : defaultType;
}
case "defaultOrZero": {
const defaultType = defaultPaddingType(dataType);
return defaultType === "signed" ? "signedOrLeft" : defaultType;
}
}
}
function defaultPaddingType(dataType) {
switch (dataType.typeClass) {
case "bytes":
return "right";
case "int":
case "fixed":
return "signed";
case "function":
if (dataType.visibility === "external") {
return "right";
}
//otherwise, fall through to default
default:
return "left";
}
}
function checkPaddingRight(bytes, length) {
let padding = bytes.slice(length); //cut off the first length bytes
return padding.every(paddingByte => paddingByte === 0);
}
//exporting this one for use in stack.ts
function checkPaddingLeft(bytes, length) {
let padding = bytes.slice(0, -length); //cut off the last length bytes
return padding.every(paddingByte => paddingByte === 0);
}
exports.checkPaddingLeft = checkPaddingLeft;
function checkPaddingSigned(bytes, length) {
let padding = bytes.slice(0, -length); //padding is all but the last length bytes
let value = bytes.slice(-length); //meanwhile the actual value is those last length bytes
let signByte = value[0] & 0x80 ? 0xff : 0x00;
return padding.every(paddingByte => paddingByte === signByte);
}
function* reverseEnsResolve(address) {
const nameAsBytes = yield { type: "ens-primary-name", address };
return nameAsBytes !== null ? (0, decode_1.decodeString)(nameAsBytes) : null;
}
//# sourceMappingURL=index.js.map