packages/decoder/lib/decoders.ts
import debugModule from "debug";
const debug = debugModule("decoder:decoders");
import * as Abi from "@truffle/abi-utils";
import * as Codec from "@truffle/codec";
import {
AbiData,
Ast,
Evm,
Format,
Conversion,
Storage,
Contexts,
Compilations,
Compiler,
CalldataDecoding,
FunctionDecoding,
LogDecoding,
ReturndataDecoding,
BlockSpecifier,
RegularizedBlockSpecifier,
CallInterpretationInfo,
TryAggregateInfo,
DeadlinedMulticallInfo,
BlockhashedMulticallInfo,
decodeCalldata,
decodeEvent,
decodeReturndata
} from "@truffle/codec";
import * as Encoder from "@truffle/encoder";
import { ProviderAdapter, Provider } from "@truffle/encoder";
import type * as DecoderTypes from "./types";
import Web3Utils from "web3-utils";
import type { ContractObject as Artifact } from "@truffle/contract-schema/spec";
import BN from "bn.js";
import {
ContractBeingDecodedHasNoNodeError,
ContractAllocationFailedError,
ContractNotFoundError,
InvalidAddressError,
VariableNotFoundError,
MemberNotFoundError,
ArrayIndexOutOfBoundsError,
NoProviderError
} from "./errors";
import { fetchSignatures } from "./fetch-signatures";
import { Shims } from "@truffle/compile-common";
//sorry for untyped imports!
const SourceMapUtils = require("@truffle/source-map-utils");
const { default: ENS, getEnsAddress } = require("@ensdomains/ensjs");
const defaultSelectorDirectory = "https://www.4byte.directory/api";
/**
* The ProjectDecoder class. Decodes transactions and logs. See below for a method listing.
* @category Decoder
*/
export class ProjectDecoder {
private providerAdapter: ProviderAdapter;
private compilations: Compilations.Compilation[];
private contexts: Contexts.Contexts = {}; //all contexts
private deployedContexts: Contexts.Contexts = {};
private contractsAndContexts: AbiData.Allocate.ContractAndContexts[] = [];
private referenceDeclarations: { [compilationId: string]: Ast.AstNodes };
private userDefinedTypesByCompilation: Format.Types.TypesByCompilationAndId;
private userDefinedTypes: Format.Types.TypesById;
private allocations: Evm.AllocationInfo;
private codeCache: DecoderTypes.CodeCache = {};
private ensCache: { [address: string]: Uint8Array | null } = {};
private ens: any | null; //any should be ENS, sorry >_>
private ensSettings: DecoderTypes.EnsSettings;
private selectorDirectory: string | null;
private addProjectInfoNonce: number = 0;
/**
* @protected
*/
constructor(
compilations: Compilations.Compilation[],
provider: Provider,
ensSettings?: DecoderTypes.EnsSettings,
selectorDirectory?: DecoderTypes.SelectorDirectorySettings
) {
if (!provider) {
throw new NoProviderError();
}
//check for repeat compilation IDs
const repeatIds =
Codec.Compilations.Utils.findRepeatCompilationIds(compilations);
if (repeatIds.size !== 0) {
throw new Codec.RepeatCompilationIdError([...repeatIds]);
}
this.providerAdapter = new ProviderAdapter(provider);
this.compilations = compilations;
this.ensSettings = {
provider:
ensSettings?.provider !== undefined ? ensSettings?.provider : provider, //importantly, null does not trigger this case
registryAddress: ensSettings?.registryAddress
}; //we don't use an object spread because we want undefined to be ignored
this.selectorDirectory = selectorDirectory?.enabled
? selectorDirectory.url ?? defaultSelectorDirectory
: null;
let allocationInfo: AbiData.Allocate.ContractAllocationInfo[];
({
definitions: this.referenceDeclarations,
typesByCompilation: this.userDefinedTypesByCompilation,
types: this.userDefinedTypes
} = Compilations.Utils.collectUserDefinedTypesAndTaggedOutputs(
this.compilations
));
({
contexts: this.contexts,
deployedContexts: this.deployedContexts,
contractsAndContexts: this.contractsAndContexts,
allocationInfo
} = AbiData.Allocate.Utils.collectAllocationInfo(this.compilations));
this.allocations = {};
this.allocations.abi = AbiData.Allocate.getAbiAllocations(
this.userDefinedTypes
);
this.allocations.storage = Storage.Allocate.getStorageAllocations(
this.userDefinedTypesByCompilation
); //not used by project decoder itself, but used by contract decoder
this.allocations.calldata = AbiData.Allocate.getCalldataAllocations(
allocationInfo,
this.referenceDeclarations,
this.userDefinedTypes,
this.allocations.abi
);
this.allocations.returndata = AbiData.Allocate.getReturndataAllocations(
allocationInfo,
this.referenceDeclarations,
this.userDefinedTypes,
this.allocations.abi
);
this.allocations.event = AbiData.Allocate.getEventAllocations(
allocationInfo,
this.referenceDeclarations,
this.userDefinedTypes,
this.allocations.abi
);
this.allocations.state = Storage.Allocate.getStateAllocations(
allocationInfo,
this.referenceDeclarations,
this.userDefinedTypes,
this.allocations.storage
);
debug("done with allocation");
}
/**
* @protected
* WARNING: this code is copypasted (w/slight modifications) from encoder!!
*/
public async init(): Promise<void> {
debug("initting!");
const { provider, registryAddress } = this.ensSettings;
if (provider) {
debug("provider given!");
if (registryAddress !== undefined) {
this.ens = new ENS({
provider,
ensAddress: registryAddress
});
} else {
//if we weren't given a registry address, we use the default one,
//but what is that? We have to look it up.
//NOTE: ENS is supposed to do this for us in the constructor,
//but due to a bug it doesn't.
debug("using default registry address");
const networkId = await new ProviderAdapter(provider).getNetworkId();
const registryAddress: string | undefined = getEnsAddress(networkId);
if (registryAddress) {
this.ens = new ENS({
provider: provider,
ensAddress: registryAddress
});
} else {
//there is no default registry on this chain
this.ens = null;
}
}
} else {
debug("no provider given, ens off");
this.ens = null;
}
}
/**
* **This function is asynchronous.**
*
* Adds compilations to the decoder after it has started. Note it is
* only presently possible to do this with a `ProjectDecoder` and not
* with the other decoder classes.
*
* @param compilations The compilations to be added. Take care that these
* have IDs distinct from those the decoder already has.
*/
public async addCompilations(
compilations: Compilations.Compilation[]
): Promise<void> {
//first: make sure we're not adding a compilation with an existing ID
const existingIds = new Set(
this.compilations.map(compilation => compilation.id)
);
const newIds = new Set(compilations.map(compilation => compilation.id));
//we use a find() rather than a some() so that we can put the ID in the error
const overlappingIds = [...newIds].filter(id => existingIds.has(id));
if (overlappingIds.length !== 0) {
throw new Codec.RepeatCompilationIdError(overlappingIds);
}
//also: check for repeats among the ones we're adding
const repeatIds =
Codec.Compilations.Utils.findRepeatCompilationIds(compilations);
if (repeatIds.size !== 0) {
throw new Codec.RepeatCompilationIdError([...repeatIds]);
}
//now: checks are over, start adding stuff
this.compilations = [...this.compilations, ...compilations];
const {
definitions: referenceDeclarations,
typesByCompilation: userDefinedTypesByCompilation,
types: userDefinedTypes
} = Compilations.Utils.collectUserDefinedTypesAndTaggedOutputs(
compilations
);
Object.assign(this.referenceDeclarations, referenceDeclarations);
Object.assign(
this.userDefinedTypesByCompilation,
userDefinedTypesByCompilation
);
Object.assign(this.userDefinedTypes, userDefinedTypes);
const { contexts, deployedContexts, contractsAndContexts, allocationInfo } =
AbiData.Allocate.Utils.collectAllocationInfo(compilations);
this.contexts = Object.assign(contexts, this.contexts); //HACK: we want to
//prioritize new contexts over old contexts, so we do this. a proper timestamp
//system would be better, but this should hopefully do for now.
Object.assign(this.deployedContexts, deployedContexts);
this.contractsAndContexts = [
...this.contractsAndContexts,
...contractsAndContexts
];
//everything up till now has been pretty straightforward merges.
//but allocations are a bit more complicated.
//some of them can be merged straightforwardly, some can't.
//we'll start with the straightforward ones.
const abiAllocations = AbiData.Allocate.getAbiAllocations(userDefinedTypes);
Object.assign(this.allocations.abi, abiAllocations);
const storageAllocations = Storage.Allocate.getStorageAllocations(
userDefinedTypesByCompilation
);
Object.assign(this.allocations.storage, storageAllocations);
const stateAllocations = Storage.Allocate.getStateAllocations(
allocationInfo,
referenceDeclarations,
userDefinedTypes,
storageAllocations //only need the ones from the new compilations
);
Object.assign(this.allocations.state, stateAllocations);
//now we have calldata allocations. merging these is still mostly straightforward,
//but slightly less so.
const calldataAllocations = AbiData.Allocate.getCalldataAllocations(
allocationInfo,
referenceDeclarations,
userDefinedTypes,
abiAllocations //only need the ones from the new compilations
);
//merge constructor and function allocations separately
Object.assign(
this.allocations.calldata.constructorAllocations,
calldataAllocations.constructorAllocations
);
Object.assign(
this.allocations.calldata.functionAllocations,
calldataAllocations.functionAllocations
);
//finally, redo the allocations for returndata and eventdata.
//attempting to perform a merge on these is too complicated, so we
//won't try; we'll just recompute.
this.allocations.returndata = AbiData.Allocate.getReturndataAllocations(
allocationInfo,
referenceDeclarations,
userDefinedTypes,
this.allocations.abi //we're doing this for merged result, so use merged input!
);
this.allocations.event = AbiData.Allocate.getEventAllocations(
allocationInfo,
referenceDeclarations,
userDefinedTypes,
this.allocations.abi //we're doing this for merged result, so use merged input!
);
}
/**
* **This function is asynchronous.**
*
* Adds additional compilations to the decoder like [[addCompilations]],
* but allows it to be specified in more general forms.
*
* @param projectInfo Information about the additional compilations or
* contracts to be decoded. This may come in several forms; see the type
* documentation for more information. If passing in `{ compilations: ... }`,
* take care that the compilations have different IDs from others passed in
* so far, otherwise this will error. If passed in in another form, an ID
* will be assigned automatically, which should generally avoid any
* collisions.
*/
public async addAdditionalProjectInfo(
projectInfo: Compilations.ProjectInfo
): Promise<void> {
const compilations = Compilations.Utils.infoToCompilations(
projectInfo,
`decoderAdditionalShimmedCompilationGroup(${this.addProjectInfoNonce})`
);
this.addProjectInfoNonce++;
await this.addCompilations(compilations);
}
/**
* @protected
*/
public async getCode(
address: string,
block: RegularizedBlockSpecifier
): Promise<Uint8Array> {
//if pending, ignore the cache
if (block === "pending") {
return Conversion.toBytes(
await this.providerAdapter.getCode(address, block)
);
}
//otherwise, start by setting up any preliminary layers as needed
if (this.codeCache[block] === undefined) {
this.codeCache[block] = {};
}
//now, if we have it cached, just return it
if (this.codeCache[block][address] !== undefined) {
return this.codeCache[block][address];
}
//otherwise, get it, cache it, and return it
let code = Conversion.toBytes(
await this.providerAdapter.getCode(address, block)
);
this.codeCache[block][address] = code;
return code;
}
/**
* @protected
*/
public async regularizeBlock(
block: BlockSpecifier | null
): Promise<RegularizedBlockSpecifier> {
if (typeof block === "number" || block === "pending") {
return block;
}
if (block === null) {
return "pending";
}
return (await this.providerAdapter.getBlockByNumber(block)).number;
}
/**
* **This method is asynchronous.**
*
* Takes a [[Transaction]] object and decodes it. The result is a
* [[CalldataDecoding]]; see the documentation on that interface for more.
*
* Note that decoding of transactions sent to libraries is presently not
* supported and may have unreliable results. Limited support for this is
* planned for future versions.
* @param transaction The transaction to be decoded.
*/
public async decodeTransaction(
transaction: DecoderTypes.Transaction
): Promise<CalldataDecoding> {
return await this.decodeTransactionWithAdditionalContexts(transaction);
}
/**
* @protected
*/
public async reverseEnsResolve(address: string): Promise<Uint8Array | null> {
debug("reverse resolving %s", address);
if (this.ens === null) {
debug("no ens set up!");
return null;
}
if (address in this.ensCache) {
debug("got cached: %o", this.ensCache[address]);
return this.ensCache[address];
}
let name: string | null;
try {
//try-catch because ensjs throws on bad UTF-8 :-/
//this should be fixed later
name = (await this.ens.getName(address)).name;
debug("got name: %o", name);
} catch {
//Normally I'd rethrow unexpected errors, but given the context here
//that seems like it might be a problem
name = null;
}
if (name !== null) {
//do a forward resolution check to make sure it matches
let checkAddress: string;
try {
checkAddress = await this.ens.name(name).getAddress();
} catch {
//why the try/catch? because forward resolution will throw if the
//name contains certain characters that are illegal in a domain name,
//but this isn't in any way enforced on reverse resolution above. yay.
checkAddress = null;
}
if (checkAddress !== address) {
//if it doesn't, the name is no good!
name = null;
}
}
const nameAsBytes = name !== null ? Conversion.stringToBytes(name) : null;
this.ensCache[address] = nameAsBytes;
return nameAsBytes;
}
/**
* @protected
*/
public async decodeTransactionWithAdditionalContexts(
transaction: DecoderTypes.Transaction,
additionalContexts: Contexts.Contexts = {},
additionalAllocations?: {
[
selector: string
]: AbiData.Allocate.FunctionCalldataAndReturndataAllocation;
},
overrideContext?: Contexts.Context,
isForSelectorBasedDecoding?: boolean
): Promise<CalldataDecoding> {
const block =
transaction.blockNumber !== null ? Number(transaction.blockNumber) : null;
const blockNumber = await this.regularizeBlock(block);
const isConstructor = transaction.to === null;
const context =
overrideContext ||
(await this.getContextByAddress(
transaction.to,
blockNumber,
transaction.input,
additionalContexts
));
let allocations = this.allocations;
if (overrideContext) {
//if we've got an override context, let's override some things
//(this branch is used when doing selector-based decoding)
allocations = {
...this.allocations,
calldata: {
...this.allocations.calldata,
functionAllocations: {
...this.allocations.calldata.functionAllocations,
[context.context]: additionalAllocations
}
}
};
} else if (context && !(context.context in this.contexts)) {
//otherwise, if the context comes from additionalContexts,
//we'll add the additional allocations to the allocations;
//however, we'll allow other allocations to override it...
//when we're not overriding, it's only supposed to be used if necessary!
allocations = {
...this.allocations,
calldata: {
...this.allocations.calldata,
functionAllocations: {
[context.context]: additionalAllocations,
...this.allocations.calldata.functionAllocations
}
}
};
}
const data = Conversion.toBytes(transaction.input);
const contexts = { ...this.deployedContexts, ...additionalContexts };
const info: Evm.EvmInfo = {
state: {
storage: {},
calldata: data
},
userDefinedTypes: this.userDefinedTypes,
allocations,
contexts,
currentContext: context
};
const decoder = decodeCalldata(
info,
isConstructor,
isForSelectorBasedDecoding
); //turn on strict mode for selector-based decoding
let result = decoder.next();
while (result.done === false) {
let request = result.value;
let response: Uint8Array;
switch (request.type) {
case "code":
response = await this.getCode(request.address, blockNumber);
break;
case "ens-primary-name":
response = await this.reverseEnsResolve(request.address);
break;
//not writing a storage case as it shouldn't occur here!
}
result = decoder.next(response);
}
//at this point, result.value holds the final value
let decoding = result.value;
//...except wait! we're not done yet! we need to do multicall processing!
if (decoding.kind === "function") {
decoding = await this.withMulticallInterpretations(
decoding,
transaction,
additionalContexts,
additionalAllocations,
overrideContext
);
}
//...and 4byte.directory processing
if (
(decoding.kind === "message" || decoding.kind === "unknown") &&
!isForSelectorBasedDecoding //prevent infinite loops!
) {
const selectorBasedDecodings = await this.decodeTransactionBySelector(
transaction,
data, //this is redundant but included for convenience
additionalContexts,
context
);
if (selectorBasedDecodings.length > 0) {
decoding.interpretations.selectorBasedDecodings =
selectorBasedDecodings;
}
}
return decoding;
}
/**
* **This method is asynchronous.**
*
* Takes a [[Log]] object and decodes it. Logs can be ambiguous, so
* this function returns an array of [[LogDecoding|LogDecodings]].
*
* Note that logs are decoded in strict mode, so (with one exception) none of the decodings should
* contain errors; if a decoding would contain an error, instead it is simply excluded from the
* list of possible decodings. The one exception to this is that indexed parameters of reference
* type cannot meaningfully be decoded, so those will decode to an error.
*
* If there are multiple possible decodings, they will always be listed in the following order:
*
* 1. Non-anonymous events coming from the contract itself (these will moreover be ordered
* from most derived to most base)
* 2. Non-anonymous events coming from libraries
* 3. Anonymous events coming from the contract itself (again, ordered from most derived
* to most base)
* 4. Anonymous events coming from libraries
*
* You can check the kind and class.contractKind fields to distinguish between these.
*
* If no possible decodings are found, the returned array of decodings will be empty.
*
* Note that different decodings may use different decoding modes.
*
* Using `options.extras = "on"` or `options.extras = "necessary"` will change the
* above behavior; see the documentation on [[ExtrasAllowed]] for more.
*
* If absolutely necessary, you can also set `options.disableChecks = true` to allow
* looser decoding. Only use this option if you know what you are doing.
*
* @param log The log to be decoded.
* @param options Options for controlling decoding.
*/
public async decodeLog(
log: DecoderTypes.Log,
options: DecoderTypes.DecodeLogOptions = {}
): Promise<LogDecoding[]> {
return await this.decodeLogWithAdditionalOptions(log, options);
}
/**
* @protected
*/
public async decodeLogWithAdditionalOptions(
log: DecoderTypes.Log,
options: DecoderTypes.EventOptions = {},
additionalContexts: Contexts.Contexts = {}
): Promise<LogDecoding[]> {
const block = log.blockNumber !== null ? Number(log.blockNumber) : null;
const blockNumber = await this.regularizeBlock(block);
const data = Conversion.toBytes(log.data);
const topics = log.topics.map(Conversion.toBytes);
const info: Evm.EvmInfo = {
state: {
storage: {},
eventdata: data,
eventtopics: topics
},
userDefinedTypes: this.userDefinedTypes,
allocations: this.allocations,
contexts: { ...this.deployedContexts, ...additionalContexts }
};
const decoder = decodeEvent(info, log.address, options);
let result = decoder.next();
while (result.done === false) {
let request = result.value;
let response: Uint8Array;
switch (request.type) {
case "code":
response = await this.getCode(request.address, blockNumber);
break;
case "ens-primary-name":
response = await this.reverseEnsResolve(request.address);
break;
//not writing a storage case as it shouldn't occur here!
}
result = decoder.next(response);
}
//at this point, result.value holds the final value
return result.value;
}
/**
* **This method is asynchronous.**
*
* Gets all events meeting certain conditions and decodes them.
* This function is fairly rudimentary at the moment but more functionality
* will be added in the future.
* @param options Used to determine what events to fetch and how to decode
* them; see the documentation on the [[EventOptions]] type for more.
* @return An array of [[DecodedLog|DecodedLogs]].
* These consist of a log together with its possible decodings; see that
* type for more info. And see [[decodeLog]] for more info on how log
* decoding works in general.
* @example `events({name: "TestEvent"})` -- get events named "TestEvent"
* from the most recent block
*/
public async events(
options: DecoderTypes.EventOptions = {}
): Promise<DecoderTypes.DecodedLog[]> {
return await this.eventsWithAdditionalContexts(options);
}
/**
* @protected
*/
public async eventsWithAdditionalContexts(
options: DecoderTypes.EventOptions = {},
additionalContexts: Contexts.Contexts = {}
): Promise<DecoderTypes.DecodedLog[]> {
let { address, name, fromBlock, toBlock } = options;
if (fromBlock === undefined) {
fromBlock = "latest";
}
if (toBlock === undefined) {
toBlock = "latest";
}
const fromBlockNumber = await this.regularizeBlock(fromBlock);
const toBlockNumber = await this.regularizeBlock(toBlock);
const logs = await this.providerAdapter.getPastLogs({
address,
fromBlock: fromBlockNumber,
toBlock: toBlockNumber
});
let events = await Promise.all(
logs.map(async log => ({
...log,
decodings: await this.decodeLogWithAdditionalOptions(
log,
options,
additionalContexts
)
}))
);
debug("events: %o", events);
//if a target name was specified, we'll restrict to events that decoded
//to something with that name. (note that only decodings with that name
//will have been returned from decodeLogs in the first place)
if (name !== undefined) {
events = events.filter(event => event.decodings.length > 0);
}
return events;
}
/**
* Takes a [[CalldataDecoding]], which may have been produced in full mode or ABI mode,
* and converts it to its ABI mode equivalent. See the README for more information.
*
* Please only use on decodings produced by this same decoder instance; use
* on decodings produced by other instances may not work consistently.
* @param decoding The decoding to abify
*/
public abifyCalldataDecoding(decoding: CalldataDecoding): CalldataDecoding {
return Codec.abifyCalldataDecoding(decoding, this.userDefinedTypes);
}
/**
* Takes a [[LogDecoding]], which may have been produced in full mode or ABI mode,
* and converts it to its ABI mode equivalent. See the README for more information.
*
* Please only use on decodings produced by this same decoder instance; use
* on decodings produced by other instances may not work consistently.
* @param decoding The decoding to abify
*/
public abifyLogDecoding(decoding: LogDecoding): LogDecoding {
return Codec.abifyLogDecoding(decoding, this.userDefinedTypes);
}
/**
* Takes a [[ReturndataDecoding]], which may have been produced in full mode
* or ABI mode, and converts it to its ABI mode equivalent. See the README
* for more information.
*
* Please only use on decodings produced by this same decoder instance; use
* on decodings produced by other instances may not work consistently.
* @param decoding The decoding to abify
*/
public abifyReturndataDecoding(
decoding: ReturndataDecoding
): ReturndataDecoding {
return Codec.abifyReturndataDecoding(decoding, this.userDefinedTypes);
}
//normally, this function gets the code of the given address at the given block,
//and checks this against the known contexts to determine the contract type
//however, if this fails and constructorBinary is passed in, it will then also
//attempt to determine it from that
private async getContextByAddress(
address: string,
block: RegularizedBlockSpecifier,
constructorBinary?: string,
additionalContexts: Contexts.Contexts = {}
): Promise<Contexts.Context | null> {
let code: string;
if (address !== null) {
code = Conversion.toHexString(await this.getCode(address, block));
} else if (constructorBinary) {
code = constructorBinary;
}
//if neither of these hold... we have a problem
let contexts = { ...this.contexts, ...additionalContexts };
return Contexts.Utils.findContext(contexts, code);
}
//finally: the spawners!
/**
* **This method is asynchronous.**
*
* Constructs a contract decoder for a given contract artifact.
* @param artifact The artifact for the contract.
*
* A contract constructor object may be substituted for the artifact, so if
* you're not sure which you're dealing with, it's OK.
*
* Note: The artifact must be for a contract that the decoder knows about;
* otherwise you will have problems.
*/
public async forArtifact(artifact: Artifact): Promise<ContractDecoder> {
let { compilation, contract } =
Compilations.Utils.findCompilationAndContract(
this.compilations,
artifact
);
//to be *sure* we've got the right ABI, we trust the input over what was
//found
contract = {
...contract,
abi: artifact.abi
};
let contractDecoder = new ContractDecoder(
contract,
compilation,
this,
artifact
);
await contractDecoder.init();
return contractDecoder;
}
/**
* **This method is asynchronous.**
*
* Constructs a contract instance decoder for a given instance of a contract in this
* project.
* @param artifact The artifact for the contract.
*
* A contract constructor object may be substituted for the artifact, so if
* you're not sure which you're dealing with, it's OK.
*
* Note: The artifact must be for a contract that the decoder knows about;
* otherwise you will have problems.
* @param address The address of the contract instance to decode. If left out, it will be autodetected.
* If an invalid address is provided, this method will throw an exception.
*/
public async forInstance(
artifact: Artifact,
address?: string
): Promise<ContractInstanceDecoder> {
let contractDecoder = await this.forArtifact(artifact);
return await contractDecoder.forInstance(address);
}
/**
* **This method is asynchronous.**
*
* Constructs a contract instance decoder for a given instance of a contract in this
* project. Unlike [[forInstance]], this method doesn't require an artifact; it
* will automatically detect the class of the given contract. If it's not in
* the project, or the decoder can't identify it, you'll get an exception.
* @param address The address of the contract instance to decode.
* If an invalid address is provided, this method will throw an exception.
* @param block You can include this argument to specify that this should be
* based on the addresses content's at a specific block (if say the contract
* has since self-destructed).
*/
public async forAddress(
address: string,
block: BlockSpecifier = "latest"
): Promise<ContractInstanceDecoder> {
if (!Web3Utils.isAddress(address)) {
throw new InvalidAddressError(address);
}
address = Web3Utils.toChecksumAddress(address);
const blockNumber = await this.regularizeBlock(block);
const deployedBytecode = Conversion.toHexString(
await this.getCode(address, blockNumber)
);
const contractAndContexts = this.contractsAndContexts.find(
({ deployedContext }) =>
deployedContext &&
Contexts.Utils.matchContext(deployedContext, deployedBytecode)
);
if (!contractAndContexts) {
throw new ContractNotFoundError(
undefined,
undefined,
deployedBytecode,
address
);
}
const { contract, compilationId } = contractAndContexts;
const compilation = this.compilations.find(
compilation => compilation.id === compilationId
);
let contractDecoder = new ContractDecoder(contract, compilation, this); //no artifact
//(artifact is only used for address autodetection, and here we're supplying the
//address, so this won't cause any problems)
await contractDecoder.init();
return await contractDecoder.forInstance(address);
}
//the following functions are intended for internal use only
/**
* @protected
*/
public getReferenceDeclarations(): { [compilationId: string]: Ast.AstNodes } {
return this.referenceDeclarations;
}
/**
* @protected
*/
public getUserDefinedTypes(): Format.Types.TypesById {
return this.userDefinedTypes;
}
/**
* @protected
*/
public getAllocations(): Evm.AllocationInfo {
return this.allocations;
}
/**
* @protected
*/
public getProviderAdapter(): ProviderAdapter {
return this.providerAdapter;
}
/**
* @protected
*/
public getEnsSettings(): DecoderTypes.EnsSettings {
return this.ensSettings;
}
/**
* @protected
*/
public getDeployedContexts(): Contexts.Contexts {
return this.deployedContexts;
}
//now, the interpretation stuff. ideally this would be a separate file
//(I mean, as would each decoder!) but that would cause circular imports, so... :-/
private async withMulticallInterpretations(
decoding: FunctionDecoding,
transaction: DecoderTypes.Transaction,
additionalContexts: Contexts.Contexts = {},
additionalAllocations?: {
[
selector: string
]: AbiData.Allocate.FunctionCalldataAndReturndataAllocation;
},
overrideContext?: Contexts.Context
): Promise<CalldataDecoding> {
//first, let's clone our decoding and its interpretations
decoding = {
...decoding,
interpretations: {
...decoding.interpretations
}
};
//now we can freely modify decoding.interpretations
//(note: these may return undefined)
decoding.interpretations.multicall = await this.interpretMulticall(
decoding,
transaction,
additionalContexts,
additionalAllocations,
overrideContext
);
decoding.interpretations.aggregate = await this.interpretAggregate(
decoding,
transaction,
additionalContexts,
additionalAllocations,
overrideContext
);
decoding.interpretations.tryAggregate = await this.interpretTryAggregate(
decoding,
transaction,
additionalContexts,
additionalAllocations,
overrideContext
);
decoding.interpretations.deadlinedMulticall =
await this.interpretDeadlinedMulticall(
decoding,
transaction,
additionalContexts,
additionalAllocations,
overrideContext
);
decoding.interpretations.specifiedBlockhashMulticall =
await this.interpretBlockhashedMulticall(
decoding,
transaction,
additionalContexts,
additionalAllocations,
overrideContext
);
return decoding;
}
private async interpretMulticall(
decoding: Codec.CalldataDecoding,
transaction: DecoderTypes.Transaction,
additionalContexts: Contexts.Contexts = {},
additionalAllocations?: {
[
selector: string
]: AbiData.Allocate.FunctionCalldataAndReturndataAllocation;
},
overrideContext?: Contexts.Context
): Promise<(Codec.CalldataDecoding | null)[] | undefined> {
if (
decoding.kind === "function" &&
decoding.abi.name === "multicall" &&
decoding.abi.inputs.length === 1 &&
decoding.abi.inputs[0].type === "bytes[]" &&
decoding.arguments[0].value.kind === "value"
) {
//sorry, this is going to involve some coercion...
const decodedArray = decoding.arguments[0]
.value as Format.Values.ArrayValue;
return await Promise.all(
decodedArray.value.map(
async callResult =>
await this.interpretCallInMulti(
callResult as Format.Values.BytesResult,
transaction,
additionalContexts,
additionalAllocations,
overrideContext
)
)
);
} else {
return undefined;
}
}
private async interpretCallInMulti(
callResult: Format.Values.BytesResult,
transaction: DecoderTypes.Transaction,
additionalContexts: Contexts.Contexts = {},
additionalAllocations?: {
[
selector: string
]: AbiData.Allocate.FunctionCalldataAndReturndataAllocation;
},
overrideContext?: Contexts.Context
): Promise<Codec.CalldataDecoding | null> {
switch (callResult.kind) {
case "value":
return await this.decodeTransactionWithAdditionalContexts(
{ ...transaction, input: callResult.value.asHex },
additionalContexts,
additionalAllocations,
overrideContext
);
case "error":
return null;
}
}
private async interpretAggregate(
decoding: Codec.CalldataDecoding,
transaction: DecoderTypes.Transaction,
additionalContexts: Contexts.Contexts = {},
additionalAllocations?: {
[
selector: string
]: AbiData.Allocate.FunctionCalldataAndReturndataAllocation;
},
overrideContext?: Contexts.Context
): Promise<CallInterpretationInfo[] | undefined> {
if (
decoding.kind === "function" &&
(decoding.abi.name === "aggregate" ||
decoding.abi.name === "blockAndAggregate") &&
decoding.abi.inputs.length === 1 &&
decoding.abi.inputs[0].type === "tuple[]" &&
decoding.abi.inputs[0].components.length === 2 &&
decoding.abi.inputs[0].components[0].type === "address" &&
decoding.abi.inputs[0].components[1].type === "bytes" &&
decoding.arguments[0].value.kind === "value"
) {
//sorry, this is going to involve some coercion...
const decodedArray = decoding.arguments[0]
.value as Format.Values.ArrayValue;
return await Promise.all(
decodedArray.value.map(
async callResult =>
await this.interpretCallInAggregate(
callResult as
| Format.Values.StructResult
| Format.Values.TupleResult,
transaction,
additionalContexts,
additionalAllocations,
overrideContext
)
)
);
} else if (
decoding.kind === "function" &&
decoding.abi.name === "aggregate3" &&
decoding.abi.inputs.length === 1 &&
decoding.abi.inputs[0].type === "tuple[]" &&
decoding.abi.inputs[0].components.length === 3 &&
decoding.abi.inputs[0].components[0].type === "address" &&
decoding.abi.inputs[0].components[1].type === "bool" &&
decoding.abi.inputs[0].components[2].type === "bytes" &&
decoding.arguments[0].value.kind === "value"
) {
//Identical to above, just split out for clarity
const decodedArray = decoding.arguments[0]
.value as Format.Values.ArrayValue;
return await Promise.all(
decodedArray.value.map(
async callResult =>
await this.interpretCallInAggregate(
callResult as
| Format.Values.StructResult
| Format.Values.TupleResult,
transaction,
additionalContexts,
additionalAllocations,
overrideContext
)
)
);
} else if (
decoding.kind === "function" &&
decoding.abi.name === "aggregate3Value" &&
decoding.abi.inputs.length === 1 &&
decoding.abi.inputs[0].type === "tuple[]" &&
decoding.abi.inputs[0].components.length === 4 &&
decoding.abi.inputs[0].components[0].type === "address" &&
decoding.abi.inputs[0].components[1].type === "bool" &&
decoding.abi.inputs[0].components[2].type === "uint256" &&
decoding.abi.inputs[0].components[3].type === "bytes" &&
decoding.arguments[0].value.kind === "value"
) {
//Identical to above, just split out for clarity
const decodedArray = decoding.arguments[0]
.value as Format.Values.ArrayValue;
return await Promise.all(
decodedArray.value.map(
async callResult =>
await this.interpretCallInAggregate(
callResult as
| Format.Values.StructResult
| Format.Values.TupleResult,
transaction,
additionalContexts,
additionalAllocations,
overrideContext
)
)
);
} else {
return undefined;
}
}
private async interpretCallInAggregate(
callResult: Format.Values.StructResult | Format.Values.TupleResult,
transaction: DecoderTypes.Transaction,
additionalContexts: Contexts.Contexts = {},
additionalAllocations?: {
[
selector: string
]: AbiData.Allocate.FunctionCalldataAndReturndataAllocation;
},
overrideContext?: Contexts.Context
): Promise<CallInterpretationInfo> {
if (callResult.kind === "error") {
return {
address: null,
allowFailure: null,
value: null,
decoding: null
};
}
let address: string;
let subDecoding: Codec.CalldataDecoding;
let value: BN = new BN(0);
let allowFailure: boolean = false;
for (const { value: subResult } of callResult.value) {
switch (subResult.type.typeClass) {
case "address":
if (subResult.kind === "error") {
address = null;
} else {
address = (subResult as Format.Values.AddressValue).value.asAddress;
}
break;
case "bool":
if (subResult.kind === "error") {
allowFailure = null;
} else {
allowFailure = (subResult as Format.Values.BoolValue).value
.asBoolean;
}
break;
case "uint":
if (subResult.kind === "error") {
value = null;
} else {
value = (subResult as Format.Values.UintValue).value.asBN.clone();
}
break;
case "bytes":
if (subResult.kind === "error") {
subDecoding = null;
} else {
const asHex = (subResult as Format.Values.BytesValue).value.asHex;
subDecoding = await this.decodeTransactionWithAdditionalContexts(
{
...transaction,
input: asHex,
to: address
},
additionalContexts,
additionalAllocations,
overrideContext
);
}
break;
}
}
return { address, allowFailure, value, decoding: subDecoding };
}
private async interpretTryAggregate(
decoding: Codec.CalldataDecoding,
transaction: DecoderTypes.Transaction,
additionalContexts: Contexts.Contexts = {},
additionalAllocations?: {
[
selector: string
]: AbiData.Allocate.FunctionCalldataAndReturndataAllocation;
},
overrideContext?: Contexts.Context
): Promise<TryAggregateInfo | undefined> {
if (
decoding.kind === "function" &&
(decoding.abi.name === "tryAggregate" ||
decoding.abi.name === "tryBlockAndAggregate") &&
decoding.abi.inputs.length === 2 &&
decoding.abi.inputs[0].type === "bool" &&
decoding.abi.inputs[1].type === "tuple[]" &&
decoding.abi.inputs[1].components.length === 2 &&
decoding.abi.inputs[1].components[0].type === "address" &&
decoding.abi.inputs[1].components[1].type === "bytes" &&
decoding.arguments[0].value.kind === "value" &&
decoding.arguments[1].value.kind === "value"
) {
//sorry, this is going to involve some coercion...
const decodedBool = decoding.arguments[0]
.value as Format.Values.BoolValue;
const decodedArray = decoding.arguments[1]
.value as Format.Values.ArrayValue;
const requireSuccess: boolean = decodedBool.value.asBoolean;
const calls = (
await Promise.all(
decodedArray.value.map(
async callResult =>
await this.interpretCallInAggregate(
callResult as
| Format.Values.StructResult
| Format.Values.TupleResult,
transaction,
additionalContexts,
additionalAllocations,
overrideContext
)
)
)
).map(call => ({ ...call, allowFailure: !requireSuccess }));
return { requireSuccess, calls };
} else {
return undefined;
}
}
private async interpretDeadlinedMulticall(
decoding: Codec.CalldataDecoding,
transaction: DecoderTypes.Transaction,
additionalContexts: Contexts.Contexts = {},
additionalAllocations?: {
[
selector: string
]: AbiData.Allocate.FunctionCalldataAndReturndataAllocation;
},
overrideContext?: Contexts.Context
): Promise<DeadlinedMulticallInfo | undefined> {
if (
decoding.kind === "function" &&
decoding.abi.name === "multicall" &&
decoding.abi.inputs.length === 2 &&
decoding.abi.inputs[0].type === "uint256" &&
decoding.abi.inputs[1].type === "bytes[]" &&
decoding.arguments[0].value.kind === "value" &&
decoding.arguments[1].value.kind === "value"
) {
//sorry, this is going to involve some coercion...
const decodedUint = decoding.arguments[0]
.value as Format.Values.UintValue;
const decodedArray = decoding.arguments[1]
.value as Format.Values.ArrayValue;
const deadline: BN = decodedUint.value.asBN;
const calls = await Promise.all(
decodedArray.value.map(
async callResult =>
await this.interpretCallInMulti(
callResult as Format.Values.BytesResult,
transaction,
additionalContexts,
additionalAllocations,
overrideContext
)
)
);
return { deadline, calls };
} else {
return undefined;
}
}
private async interpretBlockhashedMulticall(
decoding: Codec.CalldataDecoding,
transaction: DecoderTypes.Transaction,
additionalContexts: Contexts.Contexts = {},
additionalAllocations?: {
[
selector: string
]: AbiData.Allocate.FunctionCalldataAndReturndataAllocation;
},
overrideContext?: Contexts.Context
): Promise<BlockhashedMulticallInfo | undefined> {
if (
decoding.kind === "function" &&
decoding.abi.name === "multicall" &&
decoding.abi.inputs.length === 2 &&
decoding.abi.inputs[0].type === "bytes32" &&
decoding.abi.inputs[1].type === "bytes[]" &&
decoding.arguments[0].value.kind === "value" &&
decoding.arguments[1].value.kind === "value"
) {
//sorry, this is going to involve some coercion...
const decodedHash = decoding.arguments[0]
.value as Format.Values.BytesValue;
const decodedArray = decoding.arguments[1]
.value as Format.Values.ArrayValue;
const specifiedBlockhash: string = decodedHash.value.asHex;
const calls = await Promise.all(
decodedArray.value.map(
async callResult =>
await this.interpretCallInMulti(
callResult as Format.Values.BytesResult,
transaction,
additionalContexts,
additionalAllocations,
overrideContext
)
)
);
return { specifiedBlockhash, calls };
} else {
return undefined;
}
}
private async decodeTransactionBySelector(
transaction: DecoderTypes.Transaction,
data: Uint8Array, //this is redundant but included for convenience
additionalContexts: Contexts.Contexts,
context: Contexts.Context | null
): Promise<FunctionDecoding[]> {
//first: bail out if no directory
if (this.selectorDirectory === null) {
return [];
}
if (data.length < Evm.Utils.SELECTOR_SIZE) {
return [];
}
const selector = data.slice(0, Evm.Utils.SELECTOR_SIZE);
const signatures: string[] = await fetchSignatures(
selector,
this.selectorDirectory
);
debug("signatures: %O", signatures);
const abis = signatures.map(Abi.parseFunctionSignature);
const fakeContextHash =
"0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"; //hash of empty string
//the particular choice here doesn't really matter, but I figured let's pick something that will never
//occur as a real context hash
//(remember that context hashes are currently taken of the ASCII hex string, so the input
//will never be empty)
let decodings: FunctionDecoding[] = [];
for (const abiEntry of abis) {
//create a fake context with only this one ABI entry
//we can't do it with multiple of them because codec is not prepared
//for the idea that multiple might match
const fakeContextAbi = { [Abi.abiSelector(abiEntry)]: abiEntry };
const fakeContext = context
? { ...context, abi: fakeContextAbi }
: {
abi: fakeContextAbi,
context: fakeContextHash,
binary: "0x",
isConstructor: false
};
const additionalAllocations = Object.values(
AbiData.Allocate.getCalldataAllocations(
[
{
abi: [abiEntry],
compiler: null,
contractNode: null,
deployedContext: fakeContext
//we won't need anything else
}
],
{}, //we won't need the reference declarations
{}, //we won't need the user-defined types
{} //we won't need the struct allocations
).functionAllocations
)[0];
let decoding: CalldataDecoding;
try {
decoding = await this.decodeTransactionWithAdditionalContexts(
transaction,
additionalContexts,
additionalAllocations,
fakeContext, //force decoding to use the fake context & not attempt to detect
true //prevent infinite loops! and turn on strict mode
);
} catch (error) {
//because we're decoding in strict mode, it may error.
if (error instanceof Codec.StopDecodingError) {
//decoding didn't match, go to the next one
continue;
} else {
//unexpected error, rethrow!
throw error;
}
}
if (decoding.kind === "function") {
debug("accepted");
decodings.push(decoding);
}
debug("moving on");
}
return decodings;
}
}
/**
* The ContractDecoder class. Decodes return values, and spawns the
* [[ContractInstanceDecoder]] class. Also, decodes transactions logs. See
* below for a method listing.
* @category Decoder
*/
export class ContractDecoder {
private providerAdapter: ProviderAdapter;
private contexts: Contexts.Contexts; //note: this is deployed contexts only!
private compilation: Compilations.Compilation;
private contract: Compilations.Contract;
private artifact: Artifact;
private contractNode: Ast.AstNode;
private contractNetwork: number;
private contextHash: string;
private allocations: Codec.Evm.AllocationInfo;
private noBytecodeAllocations: {
[
selector: string
]: AbiData.Allocate.FunctionCalldataAndReturndataAllocation;
};
private userDefinedTypes: Format.Types.TypesById;
private stateVariableReferences: Storage.Allocate.StateVariableAllocation[];
private projectDecoder: ProjectDecoder;
/**
* @protected
*/
constructor(
contract: Compilations.Contract,
compilation: Compilations.Compilation,
projectDecoder: ProjectDecoder,
artifact?: Artifact
) {
this.artifact = artifact; //may be undefined; only used for address autodetection in instance decoder
this.contract = contract;
this.compilation = compilation;
this.projectDecoder = projectDecoder;
this.providerAdapter = projectDecoder.getProviderAdapter();
this.contexts = projectDecoder.getDeployedContexts();
this.userDefinedTypes = this.projectDecoder.getUserDefinedTypes();
this.contractNode = Compilations.Utils.getContractNode(
this.contract,
this.compilation
);
this.allocations = this.projectDecoder.getAllocations();
//note: ordinarily this.contract.deployedBytecode should equal artifact.deployedBytecode
//at this point, so it may seem strange that I'm using this longer version (but not
//doing anything to handle the case we're there not). This is basically because I don't
//think such error handling is really necessary right now, but this way at least it won't
//crash.
if (
this.contract.deployedBytecode &&
this.contract.deployedBytecode !== "0x"
) {
const unnormalizedContext = Contexts.Utils.makeContext(
this.contract,
this.contractNode,
this.compilation
);
this.contextHash = unnormalizedContext.context;
//we now throw away the unnormalized context, instead fetching the correct one from
//this.contexts (which is normalized) via the context getter below
} else {
//if there's no bytecode, allocate output data manually
const referenceDeclarations =
this.projectDecoder.getReferenceDeclarations();
const compiler = this.compilation.compiler || this.contract.compiler;
this.noBytecodeAllocations = Object.values(
AbiData.Allocate.getCalldataAllocations(
[
{
abi: Abi.normalize(this.contract.abi),
compilationId: this.compilation.id,
compiler,
contractNode: this.contractNode,
deployedContext: Contexts.Utils.makeContext(
{
...this.contract,
deployedBytecode: "0x" //only time this should ever appear in a context!
//note that we immediately discard it!
},
this.contractNode,
this.compilation
)
}
],
referenceDeclarations,
this.userDefinedTypes,
this.allocations.abi
).functionAllocations
)[0];
}
if (this.contractNode) {
//note: there used to be code here to do state allocations for the contract,
//but now the project decoder does this all up-front
//(I could change this back if for some reason performance is an issue,
//but this way is simpler TBH)
//NOTE: does this change make this intermediate class essentially pointless?
//Yes. But not going to get rid of it now!
if (
this.allocations.state[this.compilation.id] &&
this.allocations.state[this.compilation.id][this.contractNode.id]
) {
this.stateVariableReferences =
this.allocations.state[this.compilation.id][
this.contractNode.id
].members;
}
//if it doesn't exist, we will leave it undefined, and then throw an exception when
//we attempt to decode
}
}
/**
* @protected
*/
public async init(): Promise<void> {
this.contractNetwork = await this.providerAdapter.getNetworkId();
}
private get context(): Contexts.Context {
return this.contexts[this.contextHash];
}
/**
* **This method is asynchronous.**
*
* Decodes the return value of a call. Return values can be ambiguous, so
* this function returns an array of [[ReturndataDecoding|ReturndataDecodings]].
*
* Note that return values are decoded in strict mode, so none of the decodings should
* contain errors; if a decoding would contain an error, instead it is simply excluded from the
* list of possible decodings.
*
* If there are multiple possible decodings, they will always be listed in the following order:
* 1. The decoded return value from a successful call.
* 2. The decoded revert message from a call that reverted with a message.
* 3. A decoding indicating that the call reverted with no message.
* 4. A decoding indicating that the call self-destructed.
*
* You can check the kind and field to distinguish between these.
*
* If no possible decodings are found, the returned array of decodings will be empty.
*
* Note that different decodings may use different decoding modes.
*
* Decoding creation calls with this method is not supported. If you simply
* want to decode a revert message from an arbitrary call that you know
* failed, you may also want to see the [[decodeRevert]] function in
* `@truffle/codec`.
*
* @param abi The abi entry for the function call whose return value is being decoded.
* @param data The data to be decoded, as a hex string (beginning with "0x").
* @param options Additional options, such as the block the call occurred in.
* See [[ReturnOptions]] for more information.
*/
public async decodeReturnValue(
abi: Abi.FunctionEntry,
data: string,
options: DecoderTypes.ReturnOptions = {}
): Promise<ReturndataDecoding[]> {
return await this.decodeReturnValueWithAdditionalContexts(
abi,
data,
options
);
}
/**
* @protected
*/
public async decodeReturnValueWithAdditionalContexts(
abi: Abi.FunctionEntry,
data: string,
options: DecoderTypes.ReturnOptions = {},
additionalContexts: Contexts.Contexts = {}
): Promise<ReturndataDecoding[]> {
abi = <Abi.FunctionEntry>Abi.normalizeEntry(abi); //just to be absolutely certain!
const block = options.block !== undefined ? options.block : "latest";
const blockNumber = await this.regularizeBlock(block);
const status = options.status; //true, false, or undefined
const selector = Abi.abiSelector(abi);
let allocation: AbiData.Allocate.ReturndataAllocation;
if (this.contextHash !== undefined) {
allocation =
this.allocations.calldata.functionAllocations[this.contextHash][
selector
].output;
} else {
allocation = this.noBytecodeAllocations[selector].output;
}
debug("this.allocations: %O", this.allocations);
const bytes = Conversion.toBytes(data);
const info: Evm.EvmInfo = {
state: {
storage: {},
returndata: bytes
},
userDefinedTypes: this.userDefinedTypes,
allocations: this.allocations,
contexts: { ...this.contexts, ...additionalContexts },
currentContext: this.context
};
const decoder = decodeReturndata(info, allocation, status);
let result = decoder.next();
while (result.done === false) {
let request = result.value;
let response: Uint8Array;
switch (request.type) {
case "code":
response = await this.getCode(request.address, blockNumber);
break;
case "ens-primary-name":
response = await this.reverseEnsResolve(request.address);
break;
//not writing a storage case as it shouldn't occur here!
}
result = decoder.next(response);
}
//at this point, result.value holds the final value
return result.value;
}
/**
* **This method is asynchronous.**
*
* Constructs a contract instance decoder for a given instance of this contract.
* @param address The address of the contract instance decode. If left out, it will be autodetected.
* If an invalid address is provided, this method will throw an exception.
*/
public async forInstance(address?: string): Promise<ContractInstanceDecoder> {
let instanceDecoder = new ContractInstanceDecoder(this, address);
await instanceDecoder.init();
return instanceDecoder;
}
private async getCode(
address: string,
block: RegularizedBlockSpecifier
): Promise<Uint8Array> {
return await this.projectDecoder.getCode(address, block);
}
private async reverseEnsResolve(address: string): Promise<Uint8Array | null> {
return await this.projectDecoder.reverseEnsResolve(address);
}
private async regularizeBlock(
block: BlockSpecifier
): Promise<RegularizedBlockSpecifier> {
return await this.projectDecoder.regularizeBlock(block);
}
/**
* **This method is asynchronous.**
*
* See [[ProjectDecoder.decodeTransaction]].
* @param transaction The transaction to be decoded.
*/
public async decodeTransaction(
transaction: DecoderTypes.Transaction
): Promise<CalldataDecoding> {
return await this.projectDecoder.decodeTransaction(transaction);
}
/**
* **This method is asynchronous.**
*
* See [[ProjectDecoder.decodeLog]].
* @param log The log to be decoded.
*/
public async decodeLog(
log: DecoderTypes.Log,
options: DecoderTypes.DecodeLogOptions = {}
): Promise<LogDecoding[]> {
return await this.projectDecoder.decodeLog(log, options);
}
/**
* **This method is asynchronous.**
*
* See [[ProjectDecoder.events]].
* @param options Used to determine what events to fetch and how to decode them;
* see the documentation on the EventOptions type for more.
*/
public async events(
options: DecoderTypes.EventOptions = {}
): Promise<DecoderTypes.DecodedLog[]> {
return await this.projectDecoder.events(options);
}
/**
* See [[ProjectDecoder.abifyCalldataDecoding]].
*/
public abifyCalldataDecoding(decoding: CalldataDecoding): CalldataDecoding {
return this.projectDecoder.abifyCalldataDecoding(decoding);
}
/**
* See [[ProjectDecoder.abifyLogDecoding]].
*/
public abifyLogDecoding(decoding: LogDecoding): LogDecoding {
return this.projectDecoder.abifyLogDecoding(decoding);
}
/**
* See [[ProjectDecoder.abifyReturndataDecoding]].
*/
public abifyReturndataDecoding(
decoding: ReturndataDecoding
): ReturndataDecoding {
return this.projectDecoder.abifyReturndataDecoding(decoding);
}
//the following functions are for internal use
/**
* @protected
*/
public getAllocations() {
return this.allocations;
}
/**
* @protected
*/
public getStateVariableReferences() {
return this.stateVariableReferences;
}
/**
* @protected
*/
public getProjectDecoder() {
return this.projectDecoder;
}
/**
* @protected
*/
public getNoBytecodeAllocations() {
return this.noBytecodeAllocations;
}
/**
* @protected
*/
public getContractInfo(): DecoderTypes.ContractInfo {
return {
compilation: this.compilation,
contract: this.contract,
artifact: this.artifact,
contractNode: this.contractNode,
contractNetwork: this.contractNetwork,
contextHash: this.contextHash
};
}
}
/**
* The ContractInstanceDecoder class. Decodes storage for a specified
* instance. Also, decodes transactions, logs, and return values. See below
* for a method listing.
*
* Note that when using this class to decode transactions, logs, and return
* values, it does have one advantage over using the ProjectDecoder or
* ContractDecoder. If the artifact for the class does not have a
* deployedBytecode field, the ProjectDecoder (and therefore also the
* ContractDecoder) will not be able to tell that this instance is of that
* class, and so will fail to decode transactions sent to it or logs
* originating from it, and will fall back to ABI mode when decoding return
* values received from it. However, the ContractInstanceDecoder has that
* information and will make use of it, making it possible for it to decode
* transactions sent to this instance, or logs originating from it, or decode
* return values received from it in full mode, even if the deployedBytecode
* field is missing.
* @category Decoder
*/
export class ContractInstanceDecoder {
private providerAdapter: ProviderAdapter;
private compilation: Compilations.Compilation;
private contract: Compilations.Contract;
private contractNode: Ast.AstNode;
private contractNetwork: number;
private contractAddress: string;
private contractCode: string;
private contextHash: string;
private compiler: Compiler.CompilerVersion;
private contexts: Contexts.Contexts = {}; //deployed contexts only
private additionalContexts: Contexts.Contexts = {}; //for passing to project decoder when contract has no deployedBytecode
private referenceDeclarations: { [compilationId: string]: Ast.AstNodes };
private userDefinedTypes: Format.Types.TypesById;
private allocations: Codec.Evm.AllocationInfo;
private stateVariableReferences: Storage.Allocate.StateVariableAllocation[];
private internalFunctionsTable: Codec.Evm.InternalFunctions;
private internalFunctionsTableKind: Codec.Evm.InternalFunctionsTableKind;
private mappingKeys: Storage.Slot[] = [];
private storageCache: DecoderTypes.StorageCache = {};
private contractDecoder: ContractDecoder;
private projectDecoder: ProjectDecoder;
private encoder: Encoder.ProjectEncoder;
/**
* @protected
*/
constructor(contractDecoder: ContractDecoder, address?: string) {
this.contractDecoder = contractDecoder;
this.projectDecoder = this.contractDecoder.getProjectDecoder();
this.providerAdapter = this.projectDecoder.getProviderAdapter();
if (address !== undefined) {
if (!Web3Utils.isAddress(address)) {
throw new InvalidAddressError(address);
}
this.contractAddress = Web3Utils.toChecksumAddress(address);
}
this.referenceDeclarations = this.projectDecoder.getReferenceDeclarations();
this.userDefinedTypes = this.projectDecoder.getUserDefinedTypes();
this.contexts = this.projectDecoder.getDeployedContexts();
let artifact: Artifact;
({
compilation: this.compilation,
contract: this.contract,
artifact,
contractNode: this.contractNode,
contractNetwork: this.contractNetwork,
contextHash: this.contextHash
} = this.contractDecoder.getContractInfo());
this.allocations = this.contractDecoder.getAllocations();
this.stateVariableReferences =
this.contractDecoder.getStateVariableReferences();
//note that if we're in the null artifact case, this.contractAddress should have
//been set by now, so we shouldn't end up here
if (this.contractAddress === undefined) {
this.contractAddress = artifact.networks[this.contractNetwork].address;
}
this.compiler = this.compilation.compiler || this.contract.compiler;
}
/**
* @protected
*/
public async init(): Promise<void> {
this.contractCode = Conversion.toHexString(
await this.getCode(
this.contractAddress,
await this.providerAdapter.getBlockNumber() //not "latest" because regularized
)
);
const deployedBytecode = Shims.NewToLegacy.forBytecode(
this.contract.deployedBytecode
);
if (!deployedBytecode || deployedBytecode === "0x") {
//if this contract does *not* have the deployedBytecode field, then the decoder core
//has no way of knowing that contracts or function pointers with its address
//are of its class; this is an especial problem for function pointers, as it
//won't be able to determine what the selector points to.
//so, to get around this, we make an "additional context" for the contract,
//based on its *actual* deployed bytecode as pulled from the blockchain.
//This way the decoder core can recognize the address as the class, without us having
//to make serious modifications to contract decoding. And while sure this requires
//a little more work, I mean, it's all cached, so, no big deal.
const contractWithCode = {
...this.contract,
deployedBytecode: this.contractCode
};
const extraContext = Contexts.Utils.makeContext(
contractWithCode,
this.contractNode,
this.compilation
);
this.contextHash = extraContext.context;
this.additionalContexts = { [extraContext.context]: extraContext };
//the following line only has any effect if we're dealing with a library,
//since the code we pulled from the blockchain obviously does not have unresolved link references!
//(it's not strictly necessary even then, but, hey, why not?)
this.additionalContexts = Contexts.Utils.normalizeContexts(
this.additionalContexts
);
//again, since the code did not have unresolved link references, it is safe to just
//mash these together like I'm about to
this.contexts = { ...this.contexts, ...this.additionalContexts };
}
//set up encoder for wrapping elementary values.
//we pass it a provider, so it can handle ENS names.
const { provider: ensProvider, registryAddress } =
this.projectDecoder.getEnsSettings();
this.encoder = await Encoder.forProjectInternal({
provider: ensProvider,
registryAddress,
userDefinedTypes: this.userDefinedTypes,
allocations: this.allocations,
networkId: this.contractNetwork
});
//finally: set up internal functions table, if we can
const compiler = this.compilation.compiler || this.contract.compiler;
const viaIR =
this.compilation?.settings?.viaIR || this.contract?.settings?.viaIR;
if (
!this.compilation.unreliableSourceOrder &&
!viaIR &&
this.contract.deployedSourceMap &&
compiler.name === "solc" &&
this.compilation.sources.every(source => !source || source.ast)
) {
// old-style internal function pointers
// (only if source order is reliable; otherwise leave as undefined)
// unlike the debugger, we don't *demand* an answer, so we won't set up
// some sort of fake table if we don't have a source map, or if any ASTs
// are missing (if a whole *source* is missing, we'll consider that OK)
// note: we don't attempt to handle Vyper source maps!
// WARNING: untyped code in this block!
const asts: Ast.AstNode[] = this.compilation.sources.map(source =>
source ? source.ast : undefined
);
const instructions = SourceMapUtils.getProcessedInstructionsForBinary(
this.compilation.sources.map(source =>
source ? source.source : undefined
),
this.contractCode,
SourceMapUtils.getHumanReadableSourceMap(
this.contract.deployedSourceMap
)
);
try {
//this can fail if some of the source files are missing :(
this.internalFunctionsTable =
SourceMapUtils.getFunctionsByProgramCounter(
instructions,
asts,
asts.map(SourceMapUtils.makeOverlapFunction),
this.compilation.id
);
this.internalFunctionsTableKind = "pcpair";
} catch {
//just leave the internal functions table undefined
}
} else if (viaIR && this.contractNode?.internalFunctionIDs) {
//and the field
//unfortunately, unlike in the debugger, we don't have access to scopes
//(or similar) (well, unless we want to go to the effort of setting that
//up...). so we're just going to have to do some searching.
//so before we do anything else, let's set up what we'll later be searching.
const asts: Ast.AstNode[] = this.compilation.sources
.map(source => (source ? source.ast : undefined))
.filter(ast => ast !== undefined);
//we don't need to search *every* contract; internal functions can only come
//from the contract itself, base contracts, libraries, or free functions.
//I'm assuming that this assumption will continue to hold true in the future as well.
const sourceUnitsAndRelevantContracts = asts.concat(
Object.values(this.referenceDeclarations[this.compilation.id]).filter(
node =>
node.nodeType === "ContractDefinition" &&
(node.contractKind === "library" ||
this.contractNode.linearizedBaseContracts.includes(node.id))
)
);
//now, we can construct the table!
this.internalFunctionsTable = Object.assign(
//we start with the entry for the designated invalid function in index 0.
//all other functions should have index 1 or greater.
[{ isDesignatedInvalid: true }],
...Object.entries(this.contractNode.internalFunctionIDs).map(
([nodeIdAsString, index]) => {
const nodeId = Number(nodeIdAsString);
//now we perform that search we set up earlier
let contractNode = undefined;
let functionNode = undefined;
for (const parentNode of sourceUnitsAndRelevantContracts) {
const foundNode = parentNode.nodes.find(
node => node.id === nodeId
);
if (foundNode !== undefined) {
functionNode = foundNode;
contractNode =
parentNode.nodeType === "ContractDefinition"
? parentNode
: null;
break;
}
}
//if we didn't find it... oh well
if (functionNode === undefined) {
return {};
}
return {
[index]: {
//we're just going to omit the pointer-related fields...
//it's not worth the effort to determine these, it's not like
//they're even used for anything at present
isDesignatedInvalid: false,
sourceIndex: Number(functionNode.src.split(":")[2]), //to get the source index, we
//parse the node's source range, which has the form start:length:file
compilationId: this.compilation.id,
node: functionNode,
name: functionNode.name,
id: nodeId,
mutability: Codec.Ast.Utils.mutability(functionNode),
contractNode,
contractName: contractNode ? contractNode.name : null,
contractId: contractNode ? contractNode.id : null,
contractKind: contractNode ? contractNode.contractKind : null,
contractPayable: contractNode
? Codec.Ast.Utils.isContractPayable(contractNode)
: null
}
};
}
)
);
this.internalFunctionsTableKind = "index";
}
//otherwise just leave it undefined
}
private get context(): Contexts.Context {
return this.contexts[this.contextHash];
}
private checkAllocationSuccess(): void {
if (!this.contractNode) {
throw new ContractBeingDecodedHasNoNodeError(
this.contract.contractName,
this.compilation.id
);
}
if (!this.stateVariableReferences) {
throw new ContractAllocationFailedError(
this.contractNode.id,
this.contract.contractName,
this.compilation.id
);
}
}
private async decodeVariable(
variable: Storage.Allocate.StateVariableAllocation,
block: RegularizedBlockSpecifier
): Promise<DecoderTypes.StateVariable> {
const info: Codec.Evm.EvmInfo = {
state: {
storage: {},
code: Conversion.toBytes(this.contractCode)
},
mappingKeys: this.mappingKeys,
userDefinedTypes: this.userDefinedTypes,
allocations: this.allocations,
contexts: this.contexts,
currentContext: this.context,
internalFunctionsTable: this.internalFunctionsTable,
internalFunctionsTableKind: this.internalFunctionsTableKind
};
debug("this.contextHash: %s", this.contextHash);
const decoder = Codec.decodeVariable(
variable.definition,
variable.pointer,
info,
this.compilation.id
);
let result = decoder.next();
while (result.done === false) {
let request = result.value;
let response: Uint8Array;
switch (request.type) {
case "storage":
response = await this.getStorage(
this.contractAddress,
request.slot,
block
);
break;
case "code":
response = await this.getCode(request.address, block);
break;
case "ens-primary-name":
debug("ens request for: %s", request.address);
response = await this.reverseEnsResolve(request.address);
debug("response: %o", response);
break;
}
result = decoder.next(response);
}
//at this point, result.value holds the final value
debug("definedIn: %o", variable.definedIn);
let classType = Ast.Import.definitionToStoredType(
variable.definedIn,
this.compilation.id,
this.compiler
); //can skip reference decls
return {
name: variable.definition.name,
class: <Format.Types.ContractType>classType,
value: result.value
};
}
/**
* **This method is asynchronous.**
*
* Returns information about the state of the contract, but does not include
* information about the storage or decoded variables. See the documentation
* for the [[ContractState]] type for more.
* @param block The block to inspect the contract's state at. Defaults to latest.
* See [[BlockSpecifier]] for legal values.
*/
public async state(
block: BlockSpecifier = "latest"
): Promise<DecoderTypes.ContractState> {
let blockNumber = await this.regularizeBlock(block);
return {
class: Contexts.Import.contextToType(this.context),
address: this.contractAddress,
code: this.contractCode,
balanceAsBN: new BN(
await this.providerAdapter.getBalance(this.contractAddress, blockNumber)
),
nonceAsBN: new BN(
await this.providerAdapter.getTransactionCount(
this.contractAddress,
blockNumber
)
)
};
}
/**
* **This method is asynchronous.**
*
* Decodes the contract's variables; returns an array of these decoded variables.
* See the documentation of the [[DecodedVariable]] type for more.
*
* Note that variable decoding can only operate in full mode; if the decoder wasn't able to
* start up in full mode, this method will throw a [[ContractAllocationFailedError]].
*
* Note that decoding mappings requires first watching mapping keys in order to get any results;
* see the documentation for [[watchMappingKey]].
* Additional methods to make mapping decoding a less manual affair are planned for the future.
*
* Also, due to a technical limitation, it is not currently possible to
* usefully decode internal function pointers. See the
* [[Format.Values.FunctionInternalValue|FunctionInternalValue]]
* documentation and the README for more on how these are handled.
* @param block The block to inspect the contract's state at. Defaults to latest.
* See [[BlockSpecifier]] for legal values.
*/
public async variables(
block: BlockSpecifier = "latest"
): Promise<DecoderTypes.StateVariable[]> {
this.checkAllocationSuccess();
let blockNumber = await this.regularizeBlock(block);
let result: DecoderTypes.StateVariable[] = [];
for (const variable of this.stateVariableReferences) {
debug("about to decode %s", variable.definition.name);
const decodedVariable = await this.decodeVariable(variable, blockNumber);
debug("decoded");
result.push(decodedVariable);
}
return result;
}
/**
* **This method is asynchronous.**
*
* Decodes an individual contract variable; returns its value as a
* [[Format.Values.Result|Result]]. See the documentation for
* [[variables|variables()]] for various caveats that also apply here.
*
* If the variable can't be located, throws an exception.
* @param nameOrId The name (or numeric ID, if you know that) of the
* variable. Can be given as a qualified name, allowing one to get at
* shadowed variables from base contracts. If given by ID, can be given as a
* number or numeric string.
* @param block The block to inspect the contract's state at. Defaults to latest.
* See [[BlockSpecifier]] for legal values.
* @example Consider a contract `Derived` inheriting from a contract `Base`.
* Suppose `Derived` has a variable `x` and `Base` has variables `x` and
* `y`. One can access `Derived.x` as `variable("x")` or
* `variable("Derived.x")`, can access `Base.x` as `variable("Base.x")`,
* and can access `Base.y` as `variable("y")` or `variable("Base.y")`.
*/
public async variable(
nameOrId: string | number,
block: BlockSpecifier = "latest"
): Promise<Format.Values.Result | undefined> {
this.checkAllocationSuccess();
let blockNumber = await this.regularizeBlock(block);
let variable = this.findVariableByNameOrId(nameOrId);
if (variable === undefined) {
//if user put in a bad name
throw new VariableNotFoundError(nameOrId);
}
return (await this.decodeVariable(variable, blockNumber)).value;
}
private findVariableByNameOrId(
nameOrId: string | number
): Storage.Allocate.StateVariableAllocation | undefined {
//case 1: an ID was input
if (typeof nameOrId === "number" || nameOrId.match(/[0-9]+/)) {
return this.stateVariableReferences.find(
({ definition }) => definition.id === nameOrId
);
//there should be exactly one; returns undefined if none
}
//case 2: a name was input
else if (!nameOrId.includes(".")) {
//we want to search *backwards*, to get most derived version;
//we use slice().reverse() to clone before reversing since reverse modifies
return this.stateVariableReferences
.slice()
.reverse()
.find(({ definition }) => definition.name === nameOrId);
}
//case 3: a qualified name was input
else {
let [className, variableName] = nameOrId.split(".");
//again, we'll search backwards, although, uhhh...?
return this.stateVariableReferences
.slice()
.reverse()
.find(
({ definition, definedIn }) =>
definition.name === variableName && definedIn.name === className
);
}
}
private async getStorage(
address: string,
slot: BN,
block: RegularizedBlockSpecifier
): Promise<Uint8Array> {
//if pending, bypass the cache
if (block === "pending") {
return Conversion.toBytes(
await this.providerAdapter.getStorageAt(address, slot, block),
Codec.Evm.Utils.WORD_SIZE
);
}
//otherwise, start by setting up any preliminary layers as needed
if (this.storageCache[block] === undefined) {
this.storageCache[block] = {};
}
if (this.storageCache[block][address] === undefined) {
this.storageCache[block][address] = {};
}
//now, if we have it cached, just return it
if (this.storageCache[block][address][slot.toString()] !== undefined) {
return this.storageCache[block][address][slot.toString()];
}
//otherwise, get it, cache it, and return it
let word = Conversion.toBytes(
await this.providerAdapter.getStorageAt(address, slot, block),
Codec.Evm.Utils.WORD_SIZE
);
this.storageCache[block][address][slot.toString()] = word;
return word;
}
private async getCode(
address: string,
block: RegularizedBlockSpecifier
): Promise<Uint8Array> {
return await this.projectDecoder.getCode(address, block);
}
private async reverseEnsResolve(address: string): Promise<Uint8Array | null> {
return await this.projectDecoder.reverseEnsResolve(address);
}
private async regularizeBlock(
block: BlockSpecifier
): Promise<RegularizedBlockSpecifier> {
return await this.projectDecoder.regularizeBlock(block);
}
/**
* **This method is asynchronous.**
*
* Watches a mapping key; adds it to the decoder's list of watched mapping
* keys. This affects the results of both [[variables|variables()]] and
* [[variable|variable()]]. When a mapping is decoded, only the values at
* its watched keys will be included in its value.
*
* Note that it is possible
* to watch mappings that are inside structs, arrays, other mappings, etc;
* see below for more on how to do this.
*
* Note that watching mapping keys is
* only possible in full mode; if the decoder wasn't able to start up in full
* mode, this method will throw an exception.
*
* (A bad variable name will cause an exception though; that input is checked.)
* @param variable The variable that the mapping lives under; this works like
* the nameOrId argument to [[variable|variable()]]. If the mapping is a
* top-level state variable, put the mapping itself here. Otherwise, put the
* top-level state variable it lives under.
* @param indices Further arguments to watchMappingKey, if given, will be
* interpreted as indices into or members of the variable identified by the
* variable argument; see the example. Array indices and mapping
* keys are specified by value; struct members are specified by name.
*
* Values (for array indices and mapping keys) may be given in any format
* understood by Truffle Encoder; see the documentation for
* [[Encoder.ProjectEncoder.wrap|ProjectEncoder.wrap]] for details.
*
* Note that if the path to a given mapping key
* includes mapping keys above it, any ancestors will also be watched
* automatically.
* @example First, a simple example. Say we have a mapping `m` of type
* `mapping(uint => uint)`. You could call `watchMappingKey("m", 0)` to
* watch `m[0]`.
* @example Now for a slightly more complicated example. Say `m` is of type
* `mapping(uint => mapping(uint => uint))`, then to watch `m[3][5]`, you
* can call `watchMappingKey("m", 3, 5)`. This will also automatically
* watch `m[3]`; otherwise, watching `m[3][5]` wouldn't do much of
* anything.
* @example Now for a well more complicated example. Say we have a struct
* type `MapStruct` with a member called `map` which is a `mapping(string => string)`,
* and say we have a variable `arr` of type `MapStruct[]`, then one could
* watch `arr[3].map["hello"]` by calling `watchMappingKey("arr", 3, "map", "hello")`.
*/
public async watchMappingKey(
variable: number | string,
...indices: unknown[]
): Promise<void> {
this.checkAllocationSuccess();
let { slot } = await this.constructSlot(variable, ...indices);
//add mapping key and all ancestors
debug("slot: %O", slot);
while (
slot !== undefined &&
this.mappingKeys.every(
existingSlot => !Storage.Utils.equalSlots(existingSlot, slot)
//we put the newness requirement in the while condition rather than a
//separate if because if we hit one ancestor that's not new, the futher
//ones won't be either
)
) {
if (slot.key !== undefined) {
//only add mapping keys
this.mappingKeys = [...this.mappingKeys, slot];
}
slot = slot.path;
}
}
/**
* **This method is asynchronous.**
*
* Opposite of [[watchMappingKey]]; unwatches the specified mapping key. See
* watchMappingKey for more on how watching mapping keys works, and on how
* the parameters work.
*
* Note that unwatching a mapping key will also unwatch all its descendants.
* E.g., if `m` is of type `mapping(uint => mapping(uint => uint))`, then
* unwatching `m[0]` will also unwatch `m[0][0]`, `m[0][1]`, etc, if these
* are currently watched.
*/
public async unwatchMappingKey(
variable: number | string,
...indices: unknown[]
): Promise<void> {
this.checkAllocationSuccess();
let { slot } = await this.constructSlot(variable, ...indices);
if (slot === undefined) {
return; //not strictly necessary, but may as well
}
//remove mapping key and all descendants
this.mappingKeys = this.mappingKeys.filter(existingSlot => {
while (existingSlot !== undefined) {
if (Storage.Utils.equalSlots(existingSlot, slot)) {
return false; //if it matches, remove it
}
existingSlot = existingSlot.path;
}
return true; //if we didn't match, keep the key
});
}
//NOTE: if you decide to add a way to remove a mapping key *without* removing
//all descendants, you'll need to alter watchMappingKey to use an if rather
//than a while
/**
* **This method is asynchronous.**
*
* Behaves mostly as [[ProjectDecoder.decodeTransaction]]. However, it is
* capable of more robustly decoding transactions that were sent to this
* particular instance.
*/
public async decodeTransaction(
transaction: DecoderTypes.Transaction
): Promise<CalldataDecoding> {
return await this.projectDecoder.decodeTransactionWithAdditionalContexts(
transaction,
this.additionalContexts,
this.contractDecoder.getNoBytecodeAllocations()
);
}
/**
* **This method is asynchronous.**
*
* See [[ProjectDecoder.decodeLog]].
*/
public async decodeLog(
log: DecoderTypes.Log,
options: DecoderTypes.DecodeLogOptions = {}
): Promise<LogDecoding[]> {
return await this.projectDecoder.decodeLogWithAdditionalOptions(
log,
options,
this.additionalContexts
);
}
/**
* **This method is asynchronous.**
*
* See [[ContractDecoder.decodeReturnValue]].
*
* If the contract artifact is missing its bytecode, using this method,
* rather than the one in [[ContractDecoder]], can sometimes provide
* additional decoding information.
*/
public async decodeReturnValue(
abi: Abi.FunctionEntry,
data: string,
options: DecoderTypes.ReturnOptions = {}
): Promise<ReturndataDecoding[]> {
return await this.contractDecoder.decodeReturnValueWithAdditionalContexts(
abi,
data,
options,
this.additionalContexts
);
}
/**
* See [[ProjectDecoder.abifyCalldataDecoding]].
*/
public abifyCalldataDecoding(decoding: CalldataDecoding): CalldataDecoding {
return this.projectDecoder.abifyCalldataDecoding(decoding);
}
/**
* See [[ProjectDecoder.abifyLogDecoding]].
*/
public abifyLogDecoding(decoding: LogDecoding): LogDecoding {
return this.projectDecoder.abifyLogDecoding(decoding);
}
/**
* See [[ProjectDecoder.abifyReturndataDecoding]].
*/
public abifyReturndataDecoding(
decoding: ReturndataDecoding
): ReturndataDecoding {
return this.projectDecoder.abifyReturndataDecoding(decoding);
}
/**
* **This method is asynchronous.**
*
* This mostly behaves as [[ProjectDecoder.events]].
* However, unlike other variants of this function, this one, by default, restricts to events originating from this instance's address.
* If you don't want to restrict like that, you can explicitly use `address: undefined` in the options to disable this.
* (You can also of course set a different address to restrict to that.)
* @param options Used to determine what events to fetch; see the documentation on the [[EventOptions]] type for more.
*/
public async events(
options: DecoderTypes.EventOptions = {}
): Promise<DecoderTypes.DecodedLog[]> {
return await this.projectDecoder.eventsWithAdditionalContexts(
{ address: this.contractAddress, ...options },
this.additionalContexts
);
}
//in addition to returning the slot we want, it also returns a Type
//used in the recursive call
//HOW TO USE:
//variable may be a variable id (number or numeric string) or name (string) or qualified name (also string)
//struct members are given by name (string)
//array indices and numeric mapping keys may be BN, number, or numeric string
//string mapping keys should be given as strings. duh.
//bytes mapping keys should be given as hex strings beginning with "0x"
//address mapping keys are like bytes; checksum case is not required
//boolean mapping keys may be given either as booleans, or as string "true" or "false"
private async constructSlot(
variable: number | string,
...indices: unknown[]
): Promise<{ slot?: Storage.Slot; type?: Format.Types.Type }> {
//base case: we need to locate the variable and its definition
if (indices.length === 0) {
let allocation = this.findVariableByNameOrId(variable);
if (!allocation) {
throw new VariableNotFoundError(variable);
}
let dataType = Ast.Import.definitionToType(
allocation.definition,
this.compilation.id,
this.contract.compiler,
"storage"
);
let pointer = allocation.pointer;
if (pointer.location !== "storage") {
//i.e., if it's a constant
return { slot: undefined, type: undefined };
}
return { slot: pointer.range.from.slot, type: dataType };
}
//main case
let parentIndices = indices.slice(0, -1); //remove last index
let { slot: parentSlot, type: parentType } = await this.constructSlot(
variable,
...parentIndices
);
if (parentSlot === undefined) {
return { slot: undefined, type: undefined };
}
let rawIndex = indices[indices.length - 1];
let slot: Storage.Slot;
let dataType: Format.Types.Type;
switch (parentType.typeClass) {
case "array":
const wrappedIndex = <Format.Values.UintValue>(
await this.encoder.wrapElementaryValue(
{ typeClass: "uint", bits: 256 },
rawIndex
)
);
const index = wrappedIndex.value.asBN;
if (parentType.kind === "static" && index.gte(parentType.length)) {
throw new ArrayIndexOutOfBoundsError(
index,
parentType.length,
variable,
indices
);
}
dataType = parentType.baseType;
let size = Storage.Allocate.storageSize(
dataType,
this.userDefinedTypes,
this.allocations.storage
);
if (!Storage.Utils.isWordsLength(size)) {
return { slot: undefined, type: undefined };
}
slot = {
path: parentSlot,
offset: index.muln(size.words),
hashPath: parentType.kind === "dynamic"
};
break;
case "mapping":
let keyType = parentType.keyType;
const key = await this.encoder.wrapElementaryValue(keyType, rawIndex);
dataType = parentType.valueType;
slot = {
path: parentSlot,
key,
offset: new BN(0)
};
break;
case "struct":
//NOTE: due to the reliance on storage allocations,
//we don't need to use fullType or what have you
let allocation: Storage.Allocate.StorageMemberAllocation =
this.allocations.storage[parentType.id].members.find(
({ name }) => name === rawIndex
); //there should be exactly one
if (!allocation) {
const stringIndex =
typeof rawIndex === "string"
? rawIndex
: "specified by non-string argument";
throw new MemberNotFoundError(
stringIndex,
parentType,
variable,
indices
);
}
slot = {
path: parentSlot,
//need type coercion here -- we know structs don't contain constants but the compiler doesn't
offset: allocation.pointer.range.from.slot.offset.clone()
};
dataType = allocation.type;
break;
default:
return { slot: undefined, type: undefined };
}
return { slot, type: dataType };
}
}