Skip to content
This repository has been archived by the owner on Feb 26, 2024. It is now read-only.

Allow adding compilations to an existing ProjectDecoder #5646

Merged
merged 10 commits into from
Nov 15, 2022
7 changes: 4 additions & 3 deletions packages/codec/lib/compilations/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -674,16 +674,17 @@ function projectInfoIsArtifacts(
}

export function infoToCompilations(
projectInfo: ProjectInfo | undefined
projectInfo: ProjectInfo | undefined,
nonceString?: string
): Compilation[] {
if (!projectInfo) {
throw new NoProjectInfoError();
}
if (projectInfoIsCodecStyle(projectInfo)) {
return projectInfo.compilations;
} else if (projectInfoIsCommonStyle(projectInfo)) {
return shimCompilations(projectInfo.commonCompilations);
return shimCompilations(projectInfo.commonCompilations, nonceString);
} else if (projectInfoIsArtifacts(projectInfo)) {
return shimArtifacts(projectInfo.artifacts);
return shimArtifacts(projectInfo.artifacts, undefined, nonceString);
}
}
153 changes: 152 additions & 1 deletion packages/decoder/lib/decoders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ import {
VariableNotFoundError,
MemberNotFoundError,
ArrayIndexOutOfBoundsError,
NoProviderError
NoProviderError,
RepeatCompilationIdError
} from "./errors";
import { Shims } from "@truffle/compile-common";
//sorry for the untyped import, but...
Expand All @@ -68,6 +69,8 @@ export class ProjectDecoder {

private ensSettings: DecoderTypes.EnsSettings;

private addProjectInfoNonce: number = 0;

/**
* @protected
*/
Expand All @@ -79,6 +82,14 @@ export class ProjectDecoder {
if (!provider) {
throw new NoProviderError();
}
//check for repeat compilation IDs
for (let i = 0; i < compilations.length; i++) {
for (let j = i + 1; j < compilations.length; j++) {
if (compilations[i].id === compilations[j].id) {
throw new RepeatCompilationIdError(compilations[i].id);
}
}
}
this.providerAdapter = new ProviderAdapter(provider);
this.compilations = compilations;
this.ensSettings = ensSettings || {};
Expand Down Expand Up @@ -133,6 +144,146 @@ export class ProjectDecoder {
debug("done with allocation");
}

/**
* **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)
);
//we use a find() rather than a some() so that we can put the ID in the error
const conflictingCompilation = compilations.find(compilation =>
existingIds.has(compilation.id)
);
if (conflictingCompilation !== undefined) {
throw new RepeatCompilationIdError(conflictingCompilation.id);
}
//also: check for repeats among the ones we're adding
for (let i = 0; i < compilations.length; i++) {
for (let j = i + 1; j < compilations.length; j++) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, they're expected to be in order! Can we assume the client breaks this guarantee?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're not expected to be in order. Why do you say they are?

if (compilations[i].id === compilations[j].id) {
throw new RepeatCompilationIdError(compilations[i].id);
}
}
}

//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
*/
Expand Down
13 changes: 13 additions & 0 deletions packages/decoder/lib/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,16 @@ export class NoProviderError extends Error {
this.name = "NoProviderError";
}
}

/**
* This error indicates there was an attempt to add multiple compilations
* with the same ID, or a compilation whose ID was already in use.
*/
export class RepeatCompilationIdError extends Error {
public id: string;
constructor(id: string) {
super(`Compilation id ${id} already in use.`);
this.id = id;
this.name = "RepeatCompilationIdError";
}
}
126 changes: 126 additions & 0 deletions packages/decoder/test/current/test/additional-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
const debug = require("debug")("decoder:test:additional-test");
const assert = require("chai").assert;
const Ganache = require("ganache");
const path = require("path");
const Web3 = require("web3");

const Decoder = require("../../..");

const { prepareContracts } = require("../../helpers");

describe("Adding compilations", function () {
let provider;
let abstractions;
let compilations;
let web3;

let Contracts;

before("Create Provider", async function () {
provider = Ganache.provider({
miner: { instamine: "strict" },
seed: "decoder",
gasLimit: 7000000,
logging: { quiet: true }
});
web3 = new Web3(provider);
});

after(async () => {
provider && (await provider.disconnect());
});

before("Prepare contracts and artifacts", async function () {
this.timeout(30000);

const prepared = await prepareContracts(
provider,
path.resolve(__dirname, "..")
);
abstractions = prepared.abstractions;
compilations = prepared.compilations;

Contracts = [
abstractions.WireTest,
abstractions.WireTestParent,
abstractions.WireTestLibrary,
abstractions.WireTestAbstract
];
});

it("should allow adding compilations", async function () {
const deployedContract = await abstractions.WireTestRedHerring.deployed();

const decoder = await Decoder.forProject({
provider: web3.currentProvider,
projectInfo: { artifacts: Contracts }
});

//emit an event we can't decode
const { receipt } = await deployedContract.run();
//attempt to decode it; this should fail
const failingEvents = await decoder.events({
fromBlock: receipt.blockNumber,
toBlock: receipt.blockNumber
});
assert.lengthOf(failingEvents, 1);
assert.lengthOf(failingEvents[0].decodings, 0);
//now add more info
await decoder.addAdditionalProjectInfo({
artifacts: [abstractions.WireTestRedHerring]
});
//attempt to decode it again, it should succeed
const suceedingEvents = await decoder.events({
fromBlock: receipt.blockNumber,
toBlock: receipt.blockNumber
});
assert.lengthOf(suceedingEvents, 1);
assert.lengthOf(suceedingEvents[0].decodings, 1);
});

it("should error on supplying two compilations with the same ID on startup", async function () {
try {
await Decoder.forProject({
provider: web3.currentProvider,
projectInfo: { compilations: [compilations[0], compilations[0]] }
});
assert.fail("Creation should have errored");
} catch (error) {
if (error.name !== "RepeatCompilationIdError") {
throw error; //rethrow unexpected errors
}
}
});

it("should error on supplying two compilations with the same ID on adding", async function () {
const decoder = await Decoder.forProject({
provider: web3.currentProvider,
projectInfo: { compilations: [] }
});
try {
await decoder.addAdditionalProjectInfo({
compilations: [compilations[0], compilations[0]]
});
assert.fail("Adding new should have errored");
} catch (error) {
if (error.name !== "RepeatCompilationIdError") {
throw error; //rethrow unexpected errors
}
}
});

it("should error on supplying compilation with an existing ID", async function () {
const decoder = await Decoder.forProject({
provider: web3.currentProvider,
projectInfo: { compilations }
});
try {
await decoder.addAdditionalProjectInfo({ compilations });
assert.fail("Adding new should have errored");
} catch (error) {
if (error.name !== "RepeatCompilationIdError") {
throw error; //rethrow unexpected errors
}
}
});
});