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
21 changes: 18 additions & 3 deletions packages/codec/lib/compilations/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -674,16 +674,31 @@ 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);
}
}

export function findRepeatCompilationIds(
compilations: Compilation[]
): Set<string> {
let repeats: Set<string> = new Set();
for (let i = 0; i < compilations.length; i++) {
for (let j = i + 1; j < compilations.length; j++) {
if (compilations[i].id === compilations[j].id) {
repeats.add(compilations[i].id);
}
}
}
return repeats;
}
13 changes: 13 additions & 0 deletions packages/codec/lib/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,16 @@ export class NoProjectInfoError extends Error {
this.name = "NoProjectInfoError";
}
}

/**
* 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 ids: string[];
constructor(ids: string[]) {
super(`Compilation id(s) ${ids.join(", ")} repeated or already in use.`);
this.ids = ids;
this.name = "RepeatCompilationIdError";
}
}
7 changes: 6 additions & 1 deletion packages/codec/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,12 @@ export {
decodeReturndata,
decodeRevert
} from "./core";
export { DecodingError, StopDecodingError, NoProjectInfoError } from "./errors";
export {
DecodingError,
StopDecodingError,
NoProjectInfoError,
RepeatCompilationIdError
} from "./errors";

//now: what types should we export? (other than the ones from ./format)
//public-facing types for the interface
Expand Down
145 changes: 145 additions & 0 deletions packages/decoder/lib/decoders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ export class ProjectDecoder {

private ensSettings: DecoderTypes.EnsSettings;

private addProjectInfoNonce: number = 0;

/**
* @protected
*/
Expand All @@ -79,6 +81,12 @@ export class ProjectDecoder {
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 = ensSettings || {};
Expand Down Expand Up @@ -133,6 +141,143 @@ 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)
);
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
*/
Expand Down
129 changes: 129 additions & 0 deletions packages/decoder/test/current/test/additional-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
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
}
assert.lengthOf(error.ids, 1);
}
});

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
}
assert.lengthOf(error.ids, 1);
}
});

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
}
assert.lengthOf(error.ids, 1);
}
});
});
8 changes: 8 additions & 0 deletions packages/encoder/lib/encoders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,14 @@ export class ProjectEncoder {
if (!info.compilations) {
throw new NoInternalInfoError();
}
//check for repeat compilation IDs
const repeatIds = Codec.Compilations.Utils.findRepeatCompilationIds(
info.compilations
);
if (repeatIds.size !== 0) {
throw new Codec.RepeatCompilationIdError([...repeatIds]);
}
//since that's good, save it and continue
this.compilations = info.compilations;
({
definitions: this.referenceDeclarations,
Expand Down