packages/codec/lib/abi-data/allocate/index.ts
import debugModule from "debug";
const debug = debugModule("codec:abi-data:allocate");
export * as Utils from "./utils";
import type * as Abi from "@truffle/abi-utils";
import * as Import from "@truffle/codec/abi-data/import";
import * as AbiDataUtils from "@truffle/codec/abi-data/utils";
import Web3Utils from "web3-utils";
import * as Evm from "@truffle/codec/evm";
import * as Common from "@truffle/codec/common";
import type * as Compiler from "@truffle/codec/compiler";
import * as Conversion from "@truffle/codec/conversion";
import * as Ast from "@truffle/codec/ast";
import type * as Contexts from "@truffle/codec/contexts";
import { makeTypeId } from "@truffle/codec/contexts/import";
import type * as Pointer from "@truffle/codec/pointer";
import type {
AbiAllocation,
AbiAllocations,
AbiMemberAllocation,
AbiSizeInfo,
CalldataAndReturndataAllocation,
FunctionCalldataAndReturndataAllocation,
ConstructorCalldataAndReturndataAllocation,
CalldataAllocation,
ReturndataAllocation,
ReturnValueReturndataAllocation,
RevertReturndataAllocation,
ConstructorReturndataAllocation,
MessageReturndataAllocation,
BlankReturndataAllocation,
ReturnImmutableAllocation,
CalldataAllocations,
CalldataAllocationTemporary,
CalldataArgumentAllocation,
ContractAllocationInfo,
ContractAndContexts,
EventAllocation,
EventAllocations,
EventAllocationTemporary,
EventArgumentAllocation,
ReturndataAllocations
} from "./types";
import type { DecodingMode } from "@truffle/codec/types";
import * as Format from "@truffle/codec/format";
import partition from "lodash/partition";
export {
AbiAllocations,
AbiSizeInfo,
CalldataAllocation,
ReturndataAllocation,
ReturnValueReturndataAllocation,
RevertReturndataAllocation,
ConstructorReturndataAllocation,
MessageReturndataAllocation,
BlankReturndataAllocation,
CalldataAndReturndataAllocation,
FunctionCalldataAndReturndataAllocation,
ContractAllocationInfo,
ContractAndContexts,
EventAllocation,
ReturndataAllocations
};
interface AbiAllocationInfo {
size?: number; //left out for types that don't go in the abi
dynamic?: boolean; //similarly
allocations: AbiAllocations;
}
interface EventParameterInfo {
type: Format.Types.Type;
name: string;
indexed: boolean;
}
export const FallbackOutputAllocation: MessageReturndataAllocation = {
kind: "returnmessage",
selector: new Uint8Array(), //empty
allocationMode: "full"
};
export function getAbiAllocations(
userDefinedTypes: Format.Types.TypesById
): AbiAllocations {
let allocations: AbiAllocations = {};
for (const dataType of Object.values(userDefinedTypes)) {
if (dataType.typeClass === "struct") {
try {
allocations = allocateStruct(dataType, userDefinedTypes, allocations);
} catch (_) {
//if allocation fails... oh well, allocation fails, we do nothing and just move on :P
//note: a better way of handling this would probably be to *mark* it
//as failed rather than throwing an exception as that would lead to less
//recomputation, but this is simpler and I don't think the recomputation
//should really be a problem
}
}
}
return allocations;
}
function allocateStruct(
dataType: Format.Types.StructType,
userDefinedTypes: Format.Types.TypesById,
existingAllocations: AbiAllocations
): AbiAllocations {
//NOTE: dataType here should be a *stored* type!
//it is up to the caller to take care of this
return allocateMembers(
dataType.id,
dataType.memberTypes,
userDefinedTypes,
existingAllocations
);
}
//note: we will still allocate circular structs, even though they're not allowed in the abi, because it's
//not worth the effort to detect them. However on mappings or internal functions, we'll vomit (allocate null)
function allocateMembers(
parentId: string,
members: Format.Types.NameTypePair[],
userDefinedTypes: Format.Types.TypesById,
existingAllocations: AbiAllocations,
start: number = 0
): AbiAllocations {
let dynamic: boolean = false;
//note that we will mutate the start argument also!
//don't allocate things that have already been allocated
if (parentId in existingAllocations) {
return existingAllocations;
}
let allocations = { ...existingAllocations }; //otherwise, we'll be adding to this, so we better clone
let memberAllocations: AbiMemberAllocation[] = [];
for (const member of members) {
let length: number;
let dynamicMember: boolean;
({
size: length,
dynamic: dynamicMember,
allocations
} = abiSizeAndAllocate(member.type, userDefinedTypes, allocations));
//vomit on illegal types in calldata -- note the short-circuit!
if (length === undefined) {
allocations[parentId] = null;
return allocations;
}
let pointer: Pointer.AbiPointer = {
location: "abi",
start,
length
};
memberAllocations.push({
name: member.name,
type: member.type,
pointer
});
start += length;
dynamic = dynamic || dynamicMember;
}
allocations[parentId] = {
members: memberAllocations,
length: dynamic ? Evm.Utils.WORD_SIZE : start,
dynamic
};
return allocations;
}
//first return value is the actual size.
//second return value is whether the type is dynamic
//both will be undefined if type is a mapping or internal function
//third return value is resulting allocations, INCLUDING the ones passed in
function abiSizeAndAllocate(
dataType: Format.Types.Type,
userDefinedTypes: Format.Types.TypesById,
existingAllocations?: AbiAllocations
): AbiAllocationInfo {
switch (dataType.typeClass) {
case "bool":
case "address":
case "contract":
case "int":
case "uint":
case "fixed":
case "ufixed":
case "enum":
case "userDefinedValueType":
return {
size: Evm.Utils.WORD_SIZE,
dynamic: false,
allocations: existingAllocations
};
case "string":
return {
size: Evm.Utils.WORD_SIZE,
dynamic: true,
allocations: existingAllocations
};
case "bytes":
return {
size: Evm.Utils.WORD_SIZE,
dynamic: dataType.kind === "dynamic",
allocations: existingAllocations
};
case "mapping":
return {
allocations: existingAllocations
};
case "function":
switch (dataType.visibility) {
case "external":
return {
size: Evm.Utils.WORD_SIZE,
dynamic: false,
allocations: existingAllocations
};
case "internal":
return {
allocations: existingAllocations
};
}
case "array": {
switch (dataType.kind) {
case "dynamic":
return {
size: Evm.Utils.WORD_SIZE,
dynamic: true,
allocations: existingAllocations
};
case "static":
if (dataType.length.isZero()) {
//arrays of length 0 are static regardless of base type
return {
size: 0,
dynamic: false,
allocations: existingAllocations
};
}
const {
size: baseSize,
dynamic,
allocations
} = abiSizeAndAllocate(
dataType.baseType,
userDefinedTypes,
existingAllocations
);
return {
//WARNING! The use of toNumber() here may throw an exception!
//I'm judging this OK since if you have arrays that large we have bigger problems :P
size: dataType.length.toNumber() * baseSize,
dynamic,
allocations
};
}
}
case "struct": {
let allocations: AbiAllocations = existingAllocations;
let allocation: AbiAllocation | null | undefined =
allocations[dataType.id];
if (allocation === undefined) {
//if we don't find an allocation, we'll have to do the allocation ourselves
const storedType = <Format.Types.StructType>(
userDefinedTypes[dataType.id]
);
if (!storedType) {
throw new Common.UnknownUserDefinedTypeError(
dataType.id,
Format.Types.typeString(dataType)
);
}
allocations = allocateStruct(
storedType,
userDefinedTypes,
existingAllocations
);
allocation = allocations[storedType.id];
}
//having found our allocation, if it's not null, we can just look up its size and dynamicity
if (allocation !== null) {
return {
size: allocation.length,
dynamic: allocation.dynamic,
allocations
};
}
//if it is null, this type doesn't go in the abi
else {
return {
allocations
};
}
}
case "tuple": {
//Warning! Yucky wasteful recomputation here!
let size = 0;
let dynamic = false;
//note that we don't just invoke allocateStruct here!
//why not? because it has no ID to store the result in!
//and we can't use a fake like -1 because there might be a recursive call to it,
//and then the results would overwrite each other
//I mean, we could do some hashing thing or something, but I think it's easier to just
//copy the logic in this one case (sorry)
for (let member of dataType.memberTypes) {
let { size: memberSize, dynamic: memberDynamic } = abiSizeAndAllocate(
member.type,
userDefinedTypes,
existingAllocations
);
size += memberSize;
dynamic = dynamic || memberDynamic;
}
return { size, dynamic, allocations: existingAllocations };
}
}
}
//assumes you've already done allocation! don't use if you haven't!
/**
* @protected
*/
export function abiSizeInfo(
dataType: Format.Types.Type,
allocations?: AbiAllocations
): AbiSizeInfo {
let { size, dynamic } = abiSizeAndAllocate(dataType, null, allocations);
//the above line should work fine... as long as allocation is already done!
//the middle argument, userDefinedTypes, is only needed during allocation
//again, this function is only for use if allocation is done, so it's safe to pass null here
return { size, dynamic };
}
//allocates an external call
//NOTE: returns just a single allocation; assumes primary allocation is already complete!
//NOTE: returns undefined if attempting to allocate a constructor but we don't have the
//bytecode for the constructor
function allocateCalldataAndReturndata(
abiEntry: Abi.FunctionEntry | Abi.ConstructorEntry,
contractNode: Ast.AstNode | undefined,
referenceDeclarations: Ast.AstNodes,
userDefinedTypes: Format.Types.TypesById,
abiAllocations: AbiAllocations,
compilationId: string,
compiler: Compiler.CompilerVersion | undefined,
constructorContext?: Contexts.Context,
deployedContext?: Contexts.Context
): CalldataAndReturndataAllocation | undefined {
//first: determine the corresponding function node
//(simultaneously: determine the offset)
let node: Ast.AstNode | undefined = undefined;
let inputParametersFull: Ast.AstNode[];
let outputParametersFull: Ast.AstNode[];
let inputParametersAbi: Abi.Parameter[];
let outputParametersAbi: Abi.Parameter[];
let offset: number; //refers to INPUT offset; output offset is always 0
debug("allocating calldata and returndata");
switch (abiEntry.type) {
case "constructor":
if (!constructorContext) {
return undefined;
}
let rawLength = constructorContext.binary.length;
offset = (rawLength - 2) / 2; //number of bytes in 0x-prefixed bytestring
//for a constructor, we only want to search the particular contract
if (contractNode) {
node = contractNode.nodes.find(functionNode =>
AbiDataUtils.definitionMatchesAbi(
//note this needn't actually be a function node, but then it will
//return false (well, unless it's a getter node!)
abiEntry,
functionNode,
referenceDeclarations
)
);
}
//if we can't find it, we'll handle this below
break;
case "function":
offset = Evm.Utils.SELECTOR_SIZE;
//search through base contracts, from most derived (left) to most base (right)
if (contractNode) {
const linearizedBaseContracts = contractNode.linearizedBaseContracts;
debug("linearized: %O", linearizedBaseContracts);
node = findNodeAndContract(
linearizedBaseContracts,
referenceDeclarations,
functionNode =>
AbiDataUtils.definitionMatchesAbi(
abiEntry,
functionNode,
referenceDeclarations
),
contractNode
).node; //may be undefined! that's OK!
debug("found node: %o", Boolean(node));
}
break;
}
//now: get the parameters (both full-mode & ABI)
if (node) {
switch (node.nodeType) {
case "FunctionDefinition":
//normal case
inputParametersFull = node.parameters.parameters;
outputParametersFull = node.returnParameters.parameters; //this exists even for constructors!
break;
case "VariableDeclaration":
//getter case
({ inputs: inputParametersFull, outputs: outputParametersFull } =
Ast.Utils.getterParameters(node, referenceDeclarations));
break;
}
} else {
inputParametersFull = undefined;
outputParametersFull = undefined;
}
inputParametersAbi = abiEntry.inputs;
switch (abiEntry.type) {
case "function":
outputParametersAbi = abiEntry.outputs;
break;
case "constructor":
//we just leave this empty for constructors
outputParametersAbi = [];
break;
}
//now: do the allocation!
let { allocation: abiAllocationInput, mode: inputMode } =
allocateDataArguments(
inputParametersFull,
inputParametersAbi,
userDefinedTypes,
abiAllocations,
compilationId,
compiler,
offset
);
let { allocation: abiAllocationOutput, mode: outputMode } =
allocateDataArguments(
outputParametersFull,
outputParametersAbi,
userDefinedTypes,
abiAllocations,
compilationId,
compiler
//note no offset
);
debug("modes: %s in, %s out", inputMode, outputMode);
//finally: transform the allocation appropriately
let inputArgumentsAllocation = abiAllocationInput.members.map(member => ({
...member,
pointer: {
location: "calldata" as const,
start: member.pointer.start,
length: member.pointer.length
}
}));
let outputArgumentsAllocation = abiAllocationOutput.members.map(member => ({
...member,
pointer: {
location: "returndata" as const,
start: member.pointer.start,
length: member.pointer.length
}
}));
let inputsAllocation: CalldataAllocation = {
abi: abiEntry,
offset,
arguments: inputArgumentsAllocation,
allocationMode: inputMode
};
let outputsAllocation: ReturndataAllocation;
switch (abiEntry.type) {
case "function":
outputsAllocation = {
selector: new Uint8Array(), //empty by default
arguments: outputArgumentsAllocation,
allocationMode: outputMode,
kind: "return" as const
};
break;
case "constructor":
outputsAllocation = constructorOutputAllocation(
deployedContext,
contractNode,
referenceDeclarations,
outputMode
);
break;
}
return <CalldataAndReturndataAllocation>{
input: inputsAllocation,
output: outputsAllocation
}; //TS chokes on this for some reason
}
interface AbiAllocationAndMode {
allocation: AbiAllocation;
mode: DecodingMode;
}
//note: allocateEvent doesn't use this because it needs additional
//handling for indexed parameters (maybe these can be unified in
//the future though?)
function allocateDataArguments(
fullModeParameters: Ast.AstNode[] | undefined,
abiParameters: Abi.Parameter[],
userDefinedTypes: Format.Types.TypesById,
abiAllocations: AbiAllocations,
compilationId: string,
compiler: Compiler.CompilerVersion | undefined,
offset: number = 0
): AbiAllocationAndMode {
let allocationMode: DecodingMode = fullModeParameters ? "full" : "abi"; //can degrade
let parameterTypes: Format.Types.NameTypePair[];
let abiAllocation: AbiAllocation;
if (allocationMode === "full") {
let id = "-1"; //fake ID that doesn't matter
parameterTypes = fullModeParameters.map(parameter => ({
name: parameter.name,
type: Ast.Import.definitionToType(parameter, compilationId, compiler) //if node is defined, compiler had also better be!
}));
debug("parameterTypes: %O", parameterTypes);
//now: perform the allocation!
try {
abiAllocation = allocateMembers(
id,
parameterTypes,
userDefinedTypes,
abiAllocations,
offset
)[id];
} catch {
//if something goes wrong, switch to ABI mdoe
debug("falling back to ABI due to exception!");
allocationMode = "abi";
}
}
if (allocationMode === "abi") {
//THIS IS DELIBERATELY NOT AN ELSE
//this is the ABI case. we end up here EITHER
//if node doesn't exist, OR if something went wrong
//during allocation
let id = "-1"; //fake irrelevant ID
parameterTypes = abiParameters.map(parameter => ({
name: parameter.name,
type: Import.abiParameterToType(parameter)
}));
abiAllocation = allocateMembers(
id,
parameterTypes,
userDefinedTypes,
abiAllocations,
offset
)[id];
}
return { allocation: abiAllocation, mode: allocationMode };
}
interface EventParameterInfo {
name: string;
type: Format.Types.Type;
indexed: boolean;
}
//allocates an event
//NOTE: returns just a single allocation; assumes primary allocation is already complete!
function allocateEvent(
abiEntry: Abi.EventEntry,
eventNode: Ast.AstNode | undefined,
contractNode: Ast.AstNode | undefined,
referenceDeclarations: Ast.AstNodes,
userDefinedTypes: Format.Types.TypesById,
abiAllocations: AbiAllocations,
compilationId: string,
compiler: Compiler.CompilerVersion | undefined
): EventAllocation | undefined {
let parameterTypes: EventParameterInfo[];
let nodeId: string;
let id: string;
//first: determine the corresponding event node
//if we're doing inheritance processing, we search through base contracts,
//from most derived (right) to most base (left)
//if we're not doing inheritance processing (i.e. if eventNode was passed),
//we search through *all* contracts, plus the top level! even though we
//know the event node already, we still need to know where it's defined
let node: Ast.AstNode | undefined = undefined;
let definedInNode: Ast.AstNode | undefined = undefined;
let definedIn: Format.Types.ContractType | null | undefined = undefined;
let allocationMode: DecodingMode = "full"; //degrade to abi as needed
debug("allocating ABI: %O", abiEntry);
if (contractNode) {
if (eventNode) {
node = eventNode; //we already know this one!
//note: we don't use findNodeAndContract here because it's meant for searching
//through a list of base contracts, that's not really what's going on here
//(we don't need all its code here anyway)
definedInNode = Object.values(referenceDeclarations).find(
possibleContractNode =>
possibleContractNode.nodeType === "ContractDefinition" &&
possibleContractNode.nodes.some(
(possibleEventNode: Ast.AstNode) => possibleEventNode.id === node.id
)
);
if (
definedInNode &&
definedInNode.contractKind === "library" &&
definedInNode.id !== contractNode.id
) {
//skip library events! (unless this is the library they're from)
//those are always considered in-play no matter what,
//so we don't want to handle them here or we'd end up with them appearing twice
return undefined;
}
//if we failed to find what it's in... presumably it was defined at the file level.
//leave definedInNode undefined; it'll be handled below.
} else {
//first: check same contract for the event
node = contractNode.nodes.find(eventNode =>
AbiDataUtils.definitionMatchesAbi(
//note this needn't actually be an event node, but then it will
//return false
abiEntry,
eventNode,
referenceDeclarations
)
);
//if we found the node, great! If not...
if (node) {
definedInNode = contractNode;
} else {
debug("didn't find node in base contract...");
//let's search for the node among the base contracts.
//but if we find it...
//[note: the following code is overcomplicated; it was used
//when we were trying to get the actual node, it's overcomplicated
//now that we're just determining its presence. oh well]
let linearizedBaseContractsMinusSelf =
contractNode.linearizedBaseContracts.slice();
linearizedBaseContractsMinusSelf.shift(); //remove self
debug("checking contracts: %o", linearizedBaseContractsMinusSelf);
node = findNodeAndContract(
linearizedBaseContractsMinusSelf,
referenceDeclarations,
eventNode =>
AbiDataUtils.definitionMatchesAbi(
//note this needn't actually be a event node, but then it will return false
abiEntry,
eventNode,
referenceDeclarations
)
//don't pass deriveContractNode here, we're not checking the contract itself
).node; //may be undefined! that's OK!
if (node) {
//...if we find the node in an ancestor, we
//deliberately *don't* allocate! instead such cases
//will be handled during a later combination step
debug("bailing out for later handling!");
debug("ABI: %O", abiEntry);
return undefined;
}
}
}
}
//otherwise, leave node undefined
if (node) {
debug("found node");
//if we found the node, let's also turn it into a type
if (definedInNode) {
definedIn = <Format.Types.ContractType>(
Ast.Import.definitionToStoredType(
definedInNode,
compilationId,
compiler
)
); //can skip reference declarations argument here
} else {
definedIn = null; //for file-level events, once they exist
}
//...and set the ID
id = makeTypeId(node.id, compilationId);
} else {
//if no node, have to fall back into ABI mode
debug("falling back to ABI because no node");
allocationMode = "abi";
}
//now: construct the list of parameter types, attaching indexedness info
//and overall position (for later reconstruction)
let indexed: EventParameterInfo[];
let nonIndexed: EventParameterInfo[];
let abiAllocation: AbiAllocation; //the untransformed allocation for the non-indexed parameters
if (allocationMode === "full") {
nodeId = node.id.toString();
let parameters = node.parameters.parameters;
parameterTypes = parameters.map(definition => ({
//note: if node is defined, compiler had better be defined, too!
type: Ast.Import.definitionToType(definition, compilationId, compiler),
name: definition.name,
indexed: definition.indexed
}));
//now: split the list of parameters into indexed and non-indexed
[indexed, nonIndexed] = partition(
parameterTypes,
(parameter: EventParameterInfo) => parameter.indexed
);
try {
//now: perform the allocation for the non-indexed parameters!
abiAllocation = allocateMembers(
nodeId,
nonIndexed,
userDefinedTypes,
abiAllocations
)[nodeId]; //note the implicit conversion from EventParameterInfo to NameTypePair
} catch {
allocationMode = "abi";
}
}
if (allocationMode === "abi") {
//THIS IS DELIBERATELY NOT AN ELSE
nodeId = "-1"; //fake irrelevant ID
parameterTypes = abiEntry.inputs.map(abiParameter => ({
type: Import.abiParameterToType(abiParameter),
name: abiParameter.name,
indexed: abiParameter.indexed
}));
//now: split the list of parameters into indexed and non-indexed
[indexed, nonIndexed] = partition(
parameterTypes,
(parameter: EventParameterInfo) => parameter.indexed
);
//now: perform the allocation for the non-indexed parameters!
abiAllocation = allocateMembers(
nodeId,
nonIndexed,
userDefinedTypes,
abiAllocations
)[nodeId]; //note the implicit conversion from EventParameterInfo to NameTypePair
}
//now: transform the result appropriately
const nonIndexedArgumentsAllocation = abiAllocation.members.map(member => ({
...member,
pointer: {
location: "eventdata" as const,
start: member.pointer.start,
length: member.pointer.length
}
}));
//now: allocate the indexed parameters
const startingTopic = abiEntry.anonymous ? 0 : 1; //if not anonymous, selector takes up topic 0
const indexedArgumentsAllocation = indexed.map(
({ type, name }, position) => ({
type,
name,
pointer: {
location: "eventtopic" as const,
topic: startingTopic + position
}
})
);
//finally: weave these back together
let argumentsAllocation: EventArgumentAllocation[] = [];
for (let parameter of parameterTypes) {
let arrayToGrabFrom = parameter.indexed
? indexedArgumentsAllocation
: nonIndexedArgumentsAllocation;
argumentsAllocation.push(arrayToGrabFrom.shift()); //note that push and shift both modify!
}
//...and return
return {
abi: abiEntry,
contextHash: undefined, //leave this for later (HACK)
definedIn,
id,
arguments: argumentsAllocation,
allocationMode,
anonymous: abiEntry.anonymous
};
}
function allocateError(
abiEntry: Abi.ErrorEntry,
errorNode: Ast.AstNode | undefined,
referenceDeclarations: Ast.AstNodes,
userDefinedTypes: Format.Types.TypesById,
abiAllocations: AbiAllocations,
compilationId: string,
compiler: Compiler.CompilerVersion | undefined
): RevertReturndataAllocation {
//first: if we got passed just a node & no abi entry,
let id: string | undefined = undefined;
let definedIn: Format.Types.ContractType | undefined | null = undefined;
let parametersFull: Ast.AstNode[] | undefined = undefined;
const parametersAbi: Abi.Parameter[] = abiEntry.inputs;
if (errorNode) {
//first, set parametersFull
parametersFull = errorNode.parameters.parameters;
//now, set id
id = makeTypeId(errorNode.id, compilationId);
//now, set definedIn
let contractNode: Ast.AstNode | null = null;
for (const node of Object.values(referenceDeclarations)) {
if (node.nodeType === "ContractDefinition") {
if (
node.nodes.some((subNode: Ast.AstNode) => subNode.id === errorNode.id)
) {
contractNode = node;
break;
}
}
//if we didn't find it, then contractNode is null
//(and thus so will be definedIn)
}
if (contractNode === null) {
definedIn = null;
} else {
definedIn = <Format.Types.ContractType>(
Ast.Import.definitionToStoredType(contractNode, compilationId, compiler)
);
}
}
//otherwise, leave parametersFull, id, and definedIn undefined
const { allocation: abiAllocation, mode: allocationMode } =
allocateDataArguments(
parametersFull,
parametersAbi,
userDefinedTypes,
abiAllocations,
compilationId,
compiler,
Evm.Utils.SELECTOR_SIZE //errors use a 4-byte selector
);
//finally: transform the allocation appropriately
const argumentsAllocation = abiAllocation.members.map(member => ({
...member,
pointer: {
location: "returndata" as const,
start: member.pointer.start,
length: member.pointer.length
}
}));
const selector = Conversion.toBytes(AbiDataUtils.abiSelector(abiEntry));
return {
kind: "revert",
selector,
abi: abiEntry,
id,
definedIn,
arguments: argumentsAllocation,
allocationMode
};
}
function getCalldataAllocationsForContract(
abi: Abi.Abi,
contractNode: Ast.AstNode,
constructorContext: Contexts.Context,
deployedContext: Contexts.Context,
referenceDeclarations: Ast.AstNodes,
userDefinedTypes: Format.Types.TypesById,
abiAllocations: AbiAllocations,
compilationId: string,
compiler: Compiler.CompilerVersion
): CalldataAllocationTemporary {
let allocations: CalldataAllocationTemporary = {
constructorAllocation: undefined,
//(if it doesn't then it will remain as default)
functionAllocations: {}
};
if (!abi) {
//if no ABI, can't do much!
allocations.constructorAllocation = defaultConstructorAllocation(
constructorContext,
contractNode,
referenceDeclarations,
deployedContext
);
return allocations;
}
for (let abiEntry of abi) {
if (
AbiDataUtils.abiEntryIsObviouslyIllTyped(abiEntry) ||
AbiDataUtils.abiEntryHasStorageParameters(abiEntry)
) {
//the first of these conditions is a hack workaround for a Solidity bug.
//the second of these is because... seriously? we're not handling these
//(at least not for now!) (these only exist prior to Solidity 0.5.6,
//thankfully)
continue;
}
switch (abiEntry.type) {
case "constructor":
allocations.constructorAllocation = <
ConstructorCalldataAndReturndataAllocation
>allocateCalldataAndReturndata(
abiEntry,
contractNode,
referenceDeclarations,
userDefinedTypes,
abiAllocations,
compilationId,
compiler,
constructorContext,
deployedContext
);
debug("constructor alloc: %O", allocations.constructorAllocation);
break;
case "function":
allocations.functionAllocations[AbiDataUtils.abiSelector(abiEntry)] = <
FunctionCalldataAndReturndataAllocation
>allocateCalldataAndReturndata(
abiEntry,
contractNode,
referenceDeclarations,
userDefinedTypes,
abiAllocations,
compilationId,
compiler,
constructorContext,
deployedContext
);
break;
default:
//skip over fallback, error, and event
break;
}
}
if (!allocations.constructorAllocation) {
//set a default constructor allocation if we haven't allocated one yet
allocations.constructorAllocation = defaultConstructorAllocation(
constructorContext,
contractNode,
referenceDeclarations,
deployedContext
);
debug("default constructor alloc: %O", allocations.constructorAllocation);
}
return allocations;
}
function defaultConstructorAllocation(
constructorContext: Contexts.Context,
contractNode: Ast.AstNode | undefined,
referenceDeclarations: Ast.AstNodes,
deployedContext?: Contexts.Context
): ConstructorCalldataAndReturndataAllocation | undefined {
if (!constructorContext) {
return undefined;
}
const rawLength = constructorContext.binary.length;
const offset = (rawLength - 2) / 2; //number of bytes in 0x-prefixed bytestring
const input = {
offset,
abi: AbiDataUtils.DEFAULT_CONSTRUCTOR_ABI,
arguments: [] as CalldataArgumentAllocation[],
allocationMode: "full" as const
};
const output = constructorOutputAllocation(
deployedContext,
contractNode,
referenceDeclarations,
"full"
); //assume full, degrade as necessary
return { input, output };
}
//note: context should be deployed context!
function constructorOutputAllocation(
context: Contexts.Context | undefined,
contractNode: Ast.AstNode | undefined,
referenceDeclarations: Ast.AstNodes,
allocationMode: DecodingMode
): ConstructorReturndataAllocation {
if (!context) {
//just return a default abi mode result
return {
selector: new Uint8Array(), //always empty for constructor output
allocationMode: "abi",
kind: "bytecode" as const,
delegatecallGuard: false
};
}
const { immutableReferences, compilationId, compiler, contractKind, binary } =
context;
let immutables: ReturnImmutableAllocation[] | undefined;
if (allocationMode === "full" && immutableReferences) {
if (contractNode) {
debug("allocating immutables");
immutables = [];
for (const [id, references] of Object.entries(immutableReferences)) {
if (references.length === 0) {
continue; //don't allocate immutables that don't exist
}
const astId: number = parseInt(id);
//get the corresponding variable node; potentially fail
const { node: definition, contract: definedIn } = findNodeAndContract(
contractNode.linearizedBaseContracts,
referenceDeclarations,
node => node.id === astId,
contractNode
);
if (!definition || definition.nodeType !== "VariableDeclaration") {
debug("didn't find definition for %d!", astId);
allocationMode = "abi";
immutables = undefined;
break;
}
const definedInClass = <Format.Types.ContractType>(
Ast.Import.definitionToStoredType(definedIn, compilationId, compiler)
); //can skip reference declarations argument here
const dataType = Ast.Import.definitionToType(
definition,
compilationId,
compiler
);
immutables.push({
name: definition.name,
definedIn: definedInClass,
type: dataType,
pointer: {
location: "returndata" as const,
start: references[0].start,
length: references[0].length
}
});
}
} else if (Object.entries(immutableReferences).length > 0) {
//if there are immutables, but no contract mode, go to abi mode
debug("immutables but no node!");
allocationMode = "abi";
}
} else {
debug("no immutables");
}
//now, is there a delegatecall guard?
let delegatecallGuard: boolean = false;
if (contractKind === "library") {
//note: I am relying on this being present!
//(also this part is a bit HACKy)
const pushAddressInstruction = (0x5f + Evm.Utils.ADDRESS_SIZE).toString(16); //"73"
const delegateCallGuardString =
"0x" + pushAddressInstruction + "..".repeat(Evm.Utils.ADDRESS_SIZE);
if (binary.startsWith(delegateCallGuardString)) {
delegatecallGuard = true;
}
}
return {
selector: new Uint8Array(), //always empty for constructor output
allocationMode,
kind: "bytecode" as const,
immutables,
delegatecallGuard
};
}
export function getCalldataAllocations(
contracts: ContractAllocationInfo[],
referenceDeclarations: { [compilationId: string]: Ast.AstNodes },
userDefinedTypes: Format.Types.TypesById,
abiAllocations: AbiAllocations
): CalldataAllocations {
let allocations: CalldataAllocations = {
constructorAllocations: {},
functionAllocations: {}
};
for (let contract of contracts) {
const contractAllocations = getCalldataAllocationsForContract(
contract.abi,
contract.contractNode,
contract.constructorContext,
contract.deployedContext,
referenceDeclarations[contract.compilationId],
userDefinedTypes,
abiAllocations,
contract.compilationId,
contract.compiler
);
if (contract.constructorContext) {
allocations.constructorAllocations[contract.constructorContext.context] =
contractAllocations.constructorAllocation;
}
if (contract.deployedContext) {
allocations.functionAllocations[contract.deployedContext.context] =
contractAllocations.functionAllocations;
//set this up under both constructor *and* deployed! this is to handle
//constructor returndata decoding
allocations.constructorAllocations[contract.deployedContext.context] =
contractAllocations.constructorAllocation;
}
}
return allocations;
}
function getReturndataAllocationsForContract(
abi: Abi.Abi,
contractNode: Ast.AstNode | undefined,
referenceDeclarations: Ast.AstNodes,
userDefinedTypes: Format.Types.TypesById,
abiAllocations: AbiAllocations,
compilationId: string,
compiler: Compiler.CompilerVersion | undefined
): RevertReturndataAllocation[] {
let useAst = Boolean(contractNode && contractNode.usedErrors);
if (useAst) {
const errorNodes = contractNode.usedErrors.map(
errorNodeId => referenceDeclarations[errorNodeId]
);
let abis: Abi.ErrorEntry[];
try {
abis = errorNodes.map(
errorNode =>
<Abi.ErrorEntry>(
Ast.Utils.definitionToAbi(errorNode, referenceDeclarations)
)
);
} catch {
useAst = false;
}
if (useAst) {
//i.e. if the above operation succeeded
return contractNode.usedErrors
.map(errorNodeId => referenceDeclarations[errorNodeId])
.map((errorNode, index) =>
allocateError(
abis[index],
errorNode,
referenceDeclarations,
userDefinedTypes,
abiAllocations,
compilationId,
compiler
)
);
}
}
if (!useAst && abi) {
//deliberately *not* an else!
return abi
.filter((abiEntry: Abi.Entry) => abiEntry.type === "error")
.filter(
(abiEntry: Abi.ErrorEntry) =>
!AbiDataUtils.abiEntryIsObviouslyIllTyped(abiEntry)
) //hack workaround
.map((abiEntry: Abi.ErrorEntry) =>
allocateError(
abiEntry,
undefined,
referenceDeclarations,
userDefinedTypes,
abiAllocations,
compilationId,
compiler
)
);
}
//otherwise just return nothing
return [];
}
export function getReturndataAllocations(
contracts: ContractAllocationInfo[],
referenceDeclarations: { [compilationId: string]: Ast.AstNodes },
userDefinedTypes: Format.Types.TypesById,
abiAllocations: AbiAllocations
): ReturndataAllocations {
let allContexts: string[] = []
.concat(
...contracts.map(({ deployedContext, constructorContext }) => [
deployedContext,
constructorContext
])
)
.filter(x => x) //filter out nonexistent contexts
.map(context => context.context);
allContexts.push(""); //HACK: add fictional empty-string context to represent no-context
//holds allocations for a given context
let selfAllocations: { [contextHash: string]: RevertReturndataAllocation[] } =
{};
//holds allocations for *other* contexts
let additionalAllocations: {
[contextHash: string]: RevertReturndataAllocation[];
} = {};
//now: process the allocations for each contract. we'll add each contract's
//allocations to *its* entries in allocations, and to every *other* entry
//in additionalAllocations.
for (const contract of contracts) {
const contractAllocations = getReturndataAllocationsForContract(
contract.abi,
contract.contractNode,
referenceDeclarations[contract.compilationId],
userDefinedTypes,
abiAllocations,
contract.compilationId,
contract.compiler
);
const contexts: string[] = [
//contexts for this contract
contract.deployedContext,
contract.constructorContext
]
.filter(x => x) //filter out nonexistent contexts
.map(context => context.context);
const otherContexts: string[] = allContexts.filter(
//contexts for all other contracts
contextHash => !contexts.includes(contextHash)
);
//add them to selfAllocations
for (const contextHash of contexts) {
selfAllocations[contextHash] = contractAllocations;
}
//add them to additionalAllocations
for (const contextHash of otherContexts) {
if (additionalAllocations[contextHash] === undefined) {
additionalAllocations[contextHash] = [];
}
additionalAllocations[contextHash] =
additionalAllocations[contextHash].concat(contractAllocations);
}
}
let allocations: ReturndataAllocations = Object.assign(
{},
...allContexts.map(contextHash => ({ [contextHash]: {} }))
);
//now: perform coalescense!
for (const contract of contracts) {
//we're setting up contexts again, sorry >_>
const contexts: string[] = [
//contexts for this contract
contract.deployedContext,
contract.constructorContext
]
.filter(x => x) //filter out nonexistent contexts
.map(context => context.context);
for (const contextHash of contexts) {
allocations[contextHash] = coalesceReturndataAllocations(
selfAllocations[contextHash] || [],
additionalAllocations[contextHash] || []
);
debug("allocations: %O", allocations[contextHash]);
}
}
//...also coalesce the fake "" context
allocations[""] = coalesceReturndataAllocations(
[],
additionalAllocations[""] || []
);
debug("error allocations: %O", allocations);
return allocations;
}
function coalesceReturndataAllocations(
selfAllocations: RevertReturndataAllocation[],
additionalAllocations: RevertReturndataAllocation[]
): { [selector: string]: RevertReturndataAllocation[] } {
let bySelector: { [selector: string]: RevertReturndataAllocation[] } = {};
//start with the additional allocations; we want to process
//the self allocations last, due to special handling of no-ID allocations there
for (const allocation of additionalAllocations) {
const signature = AbiDataUtils.abiSignature(allocation.abi);
const selector = Web3Utils.soliditySha3({
type: "string",
value: signature
}).slice(0, 2 + 2 * Evm.Utils.SELECTOR_SIZE); //arithmetic to account for hex string
if (bySelector[selector]) {
//note: at this point, for any given signature, there should only be a
//no-ID allocation for that signature if it's the only one
if (allocation.id !== undefined) {
//delete anything with that signature but w/o an ID, or with this same ID
bySelector[selector] = bySelector[selector].filter(
({ abi, id }) =>
!(
AbiDataUtils.abiSignature(abi) === signature &&
(id === undefined || id === allocation.id)
)
);
//add this allocation
bySelector[selector].push(allocation);
} else if (
!bySelector[selector].some(
({ abi }) => AbiDataUtils.abiSignature(abi) === signature
)
) {
//only add ID-less ones if there isn't anything of that signature already
bySelector[selector].push(allocation);
}
} else {
//if there's nothing there thus far, add it
bySelector[selector] = [allocation];
}
}
//now we're going to perform a modified version of this procedure for the self allocations:
//1. we're going to add to the front, not the back
//2. we can add an ID-less one even if there are already ones with IDs there
//(sorry for the copypaste)
for (const allocation of selfAllocations) {
const signature = AbiDataUtils.abiSignature(allocation.abi);
const selector = Web3Utils.soliditySha3({
type: "string",
value: signature
}).slice(0, 2 + 2 * Evm.Utils.SELECTOR_SIZE); //arithmetic to account for hex string
if (bySelector[selector]) {
//delete anything with that signature but w/o an ID, or with this same ID
//(if this alloc has no ID, this will only delete ID-less ones :) )
bySelector[selector] = bySelector[selector].filter(
({ abi, id }) =>
!(
AbiDataUtils.abiSignature(abi) === signature &&
(id === undefined || id === allocation.id)
)
);
//add this allocation to front, not back!
bySelector[selector].unshift(allocation);
} else {
//if there's nothing there thus far, add it
bySelector[selector] = [allocation];
}
}
return bySelector;
}
function getEventAllocationsForContract(
abi: Abi.Abi,
contractNode: Ast.AstNode | undefined,
referenceDeclarations: Ast.AstNodes,
userDefinedTypes: Format.Types.TypesById,
abiAllocations: AbiAllocations,
compilationId: string,
compiler: Compiler.CompilerVersion | undefined
): EventAllocationTemporary[] {
let useAst = Boolean(contractNode && contractNode.usedEvents);
if (useAst) {
const eventNodes = contractNode.usedEvents.map(
eventNodeId => referenceDeclarations[eventNodeId]
);
let abis: Abi.EventEntry[];
try {
abis = eventNodes.map(
eventNode =>
<Abi.EventEntry>(
Ast.Utils.definitionToAbi(eventNode, referenceDeclarations)
)
);
} catch {
useAst = false;
}
if (useAst) {
//i.e. if the above operation succeeded
return contractNode.usedEvents
.map(eventNodeId => referenceDeclarations[eventNodeId])
.map((eventNode, index) => ({
selector: AbiDataUtils.abiSelector(abis[index]),
anonymous: abis[index].anonymous,
topics: AbiDataUtils.topicsCount(abis[index]),
allocation: allocateEvent(
abis[index],
eventNode,
contractNode,
referenceDeclarations,
userDefinedTypes,
abiAllocations,
compilationId,
compiler
)
}))
.filter(
allocationTemporary => allocationTemporary.allocation !== undefined
);
//filter out library events
}
}
if (!useAst && abi) {
return abi
.filter((abiEntry: Abi.Entry) => abiEntry.type === "event")
.filter(
(abiEntry: Abi.EventEntry) =>
!AbiDataUtils.abiEntryIsObviouslyIllTyped(abiEntry)
) //hack workaround
.map((abiEntry: Abi.EventEntry) => ({
selector: AbiDataUtils.abiSelector(abiEntry),
anonymous: abiEntry.anonymous,
topics: AbiDataUtils.topicsCount(abiEntry),
allocation: allocateEvent(
abiEntry,
undefined, //we don't know the event node
contractNode,
referenceDeclarations,
userDefinedTypes,
abiAllocations,
compilationId,
compiler
)
//note we do *not* filter out undefined allocations; we need these as placeholders
}));
}
//otherwise just return nothing
return [];
}
//WARNING: this function is full of hacks... sorry
export function getEventAllocations(
contracts: ContractAllocationInfo[],
referenceDeclarations: { [compilationId: string]: Ast.AstNodes },
userDefinedTypes: Format.Types.TypesById,
abiAllocations: AbiAllocations,
allowConstructorEvents: boolean = false
): EventAllocations {
//first: do allocations for individual contracts
let individualAllocations: {
[contractKey: string]: {
[selector: string]: {
context: Contexts.Context;
contractNode: Ast.AstNode;
allocationTemporary: EventAllocationTemporary;
compilationId: string;
};
};
} = {};
let groupedAllocations: {
[contractKey: string]: {
[selector: string]: {
context: Contexts.Context;
contractNode: Ast.AstNode;
allocationsTemporary: EventAllocationTemporary[];
};
};
} = {};
let contextSwapMap: { [contextHash: string]: string } = {}; //maps deployed to constructor & vice versa
let allocations: EventAllocations = {};
for (const {
abi,
deployedContext,
constructorContext,
contractNode,
compilationId,
compiler
} of contracts) {
if (!deployedContext && !constructorContext && !contractNode) {
//we'll need *one* of these at least
continue;
}
const contractAllocations = getEventAllocationsForContract(
abi,
contractNode,
referenceDeclarations[compilationId],
userDefinedTypes,
abiAllocations,
compilationId,
compiler
);
const key = makeContractKey(
deployedContext || constructorContext,
contractNode ? contractNode.id : undefined,
compilationId
);
if (individualAllocations[key] === undefined) {
individualAllocations[key] = {};
}
for (const allocationTemporary of contractAllocations) {
//we'll use selector *even for anonymous* here, because it's just
//for determining what overrides what at this point
individualAllocations[key][allocationTemporary.selector] = {
context: deployedContext || constructorContext, //this is only used for determining contractKind, so we can use either one
contractNode,
allocationTemporary,
compilationId
};
}
//also: set up the swap map
if (deployedContext && constructorContext) {
contextSwapMap[deployedContext.context] = constructorContext.context;
contextSwapMap[constructorContext.context] = deployedContext.context;
}
}
//now: put things together for inheritance
//note how we always put things in order from most derived to most base
for (let contextOrId in individualAllocations) {
groupedAllocations[contextOrId] = {};
for (let selector in individualAllocations[contextOrId]) {
let { context, contractNode, allocationTemporary, compilationId } =
individualAllocations[contextOrId][selector];
debug("allocationTemporary: %O", allocationTemporary);
let allocationsTemporary = allocationTemporary.allocation
? [allocationTemporary]
: []; //filter out undefined allocations
//first, copy from individual allocations
groupedAllocations[contextOrId][selector] = {
context,
contractNode,
allocationsTemporary
};
//if no contract node, or if we're dealing with a contract node that
//lists it's used events for us, that's all. but otherwise...
if (contractNode && contractNode.usedEvents === undefined) {
//...we have to do inheritance processing
debug("contract Id: %d", contractNode.id);
debug("base contracts: %o", contractNode.linearizedBaseContracts);
let linearizedBaseContractsMinusSelf =
contractNode.linearizedBaseContracts.slice();
linearizedBaseContractsMinusSelf.shift(); //remove contract itself; only want ancestors
for (let baseId of linearizedBaseContractsMinusSelf) {
debug("checking baseId: %d", baseId);
let baseNode = referenceDeclarations[compilationId][baseId];
if (!baseNode || baseNode.nodeType !== "ContractDefinition") {
debug("failed to find node for baseId: %d", baseId);
break; //not a continue!
//if we can't find the base node, it's better to stop the loop,
//rather than continue to potentially erroneous things
}
//note: we're not actually going to *use* the baseNode here.
//we're just checking for whether we can *find* it
//why? because if we couldn't find it, that means that events defined in
//base contracts *weren't* skipped earlier, and so we shouldn't now add them in
let baseContractInfo = contracts.find(
contractAllocationInfo =>
contractAllocationInfo.compilationId === compilationId &&
contractAllocationInfo.contractNode &&
contractAllocationInfo.contractNode.id === baseId
);
if (!baseContractInfo) {
//similar to above... this failure case can happen when there are
//two contracts with the same name and you attempt to use the
//artifacts; say you have contracts A, B, and B', where A inherits
//from B, and B and B' have the same name, and B' is the one that
//gets the artifact; B will end up in reference declarations and so
//get found above, but it won't appear in contracts, causing the
//problem here. Unfortunately I don't know any great way to handle this,
//so, uh, we treat it as a failure same as above.
debug("failed to find contract info for baseId: %d", baseId);
break;
}
let baseContext =
baseContractInfo.deployedContext ||
baseContractInfo.constructorContext;
let baseKey = makeContractKey(baseContext, baseId, compilationId);
if (individualAllocations[baseKey][selector] !== undefined) {
let baseAllocation =
individualAllocations[baseKey][selector].allocationTemporary;
debug("(probably) pushing inherited alloc from baseId: %d", baseId);
if (baseAllocation.allocation) {
//don't push undefined!
groupedAllocations[contextOrId][
selector
].allocationsTemporary.push(baseAllocation);
}
}
}
}
}
}
//finally: transform into final form & return,
//filtering out things w/o a context
for (let contractKey in groupedAllocations) {
if (!hasContext(contractKey)) {
continue;
//(this filters out ones that had no context and therefore were
//given by ID; we needed these at the previous stage but from
//here on they're irrelevant)
}
let contextHash = contextHashForKey(contractKey);
for (let selector in groupedAllocations[contextHash]) {
let { allocationsTemporary, context } =
groupedAllocations[contextHash][selector];
for (let { anonymous, topics, allocation } of allocationsTemporary) {
let contractKind = context.contractKind; //HACK: this is the wrong context, but libraries can't inherit, so it's OK
if (contractKind !== "library") {
contractKind = "contract"; //round off interfaces to being contracts for our purposes :P
}
allocation = {
...allocation,
contextHash
}; //the allocation's context hash at this point depends on where it was defined, but
//that's not what we want going in the final allocation table!
if (allocations[topics] === undefined) {
allocations[topics] = {
bySelector: {},
anonymous: { contract: {}, library: {} }
};
}
if (!anonymous) {
if (allocations[topics].bySelector[selector] === undefined) {
allocations[topics].bySelector[selector] = {
contract: {},
library: {}
};
}
//push the allocation (non-anonymous case)
if (
allocations[topics].bySelector[selector][contractKind][
contextHash
] === undefined
) {
allocations[topics].bySelector[selector][contractKind][
contextHash
] = [];
}
allocations[topics].bySelector[selector][contractKind][
contextHash
].push(allocation);
//...and push it in the swapped context too if that exists
//HACK: don't do this for libraries! library events are already
//considered always in play, so including them *twice* would cause
//problems... fortunately library constructors don't emit events!
if (
allowConstructorEvents &&
contextHash in contextSwapMap &&
contractKind !== "library"
) {
const swappedHash = contextSwapMap[contextHash];
if (
allocations[topics].bySelector[selector][contractKind][
swappedHash
] === undefined
) {
allocations[topics].bySelector[selector][contractKind][
swappedHash
] = [];
}
allocations[topics].bySelector[selector][contractKind][
swappedHash
].push(allocation);
}
} else {
//push the allocation (anonymous case)
if (
allocations[topics].anonymous[contractKind][contextHash] ===
undefined
) {
allocations[topics].anonymous[contractKind][contextHash] = [];
}
allocations[topics].anonymous[contractKind][contextHash].push(
allocation
);
//...and push it in the swapped context too if that exists
//(and it's not a library, see above)
if (
allowConstructorEvents &&
contextHash in contextSwapMap &&
contractKind !== "library"
) {
const swappedHash = contextSwapMap[contextHash];
if (
allocations[topics].anonymous[contractKind][swappedHash] ===
undefined
) {
allocations[topics].anonymous[contractKind][swappedHash] = [];
}
allocations[topics].anonymous[contractKind][swappedHash].push(
allocation
);
}
}
}
}
}
return allocations;
}
interface NodeAndContract {
node: Ast.AstNode | undefined;
contract: Ast.AstNode | undefined;
}
//if derivedContractNode is passed, we check that before referenceDeclarations
function findNodeAndContract(
linearizedBaseContracts: number[],
referenceDeclarations: Ast.AstNodes,
condition: (node: Ast.AstNode) => boolean,
derivedContractNode?: Ast.AstNode
): NodeAndContract {
const searchResult: NodeAndContract | null | undefined =
linearizedBaseContracts.reduce(
(
foundNodeAndContract: NodeAndContract | undefined | null,
baseContractId: number
) => {
if (foundNodeAndContract !== undefined) {
return foundNodeAndContract; //once we've found something, we don't need to keep looking
}
debug("searching contract %d", baseContractId);
let baseContractNode =
derivedContractNode && baseContractId === derivedContractNode.id
? derivedContractNode //skip the lookup if we already have the right node! this is to reduce errors from collision
: referenceDeclarations[baseContractId];
if (
baseContractNode === undefined ||
baseContractNode.nodeType !== "ContractDefinition"
) {
debug("bad contract node!");
return null; //return null rather than undefined so that this will propagate through
//(i.e. by returning null here we give up the search)
//(we don't want to continue due to possibility of grabbing the wrong override)
}
const node = baseContractNode.nodes.find(condition); //may be undefined! that's OK!
if (node) {
debug("found node: %o", node);
return {
node,
contract: baseContractNode
};
} else {
return undefined;
}
},
undefined //start with no node found
);
return searchResult || { node: undefined, contract: undefined };
}
function makeContractKey(
context: Contexts.Context | undefined,
id: number,
compilationId: string
): string {
return context ? context.context : id + ":" + compilationId; //HACK!
}
function hasContext(key: string): boolean {
return key.startsWith("0x"); //HACK!
}
function contextHashForKey(key: string): string {
return hasContext(key)
? key //HACK!
: undefined;
}