aboutsummaryrefslogtreecommitdiffstats
path: root/packages/sol-cov/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/sol-cov/src')
-rw-r--r--packages/sol-cov/src/artifact_adapters/0x.ts41
-rw-r--r--packages/sol-cov/src/artifact_adapters/abstract.ts5
-rw-r--r--packages/sol-cov/src/artifact_adapters/truffle.ts43
-rw-r--r--packages/sol-cov/src/collect_contract_data.ts31
-rw-r--r--packages/sol-cov/src/coverage_manager.ts99
-rw-r--r--packages/sol-cov/src/coverage_subprovider.ts38
-rw-r--r--packages/sol-cov/src/index.ts4
-rw-r--r--packages/sol-cov/src/trace.ts100
8 files changed, 281 insertions, 80 deletions
diff --git a/packages/sol-cov/src/artifact_adapters/0x.ts b/packages/sol-cov/src/artifact_adapters/0x.ts
new file mode 100644
index 000000000..87d23b0aa
--- /dev/null
+++ b/packages/sol-cov/src/artifact_adapters/0x.ts
@@ -0,0 +1,41 @@
+import * as fs from 'fs';
+import * as glob from 'glob';
+import * as _ from 'lodash';
+import * as path from 'path';
+
+import { ContractData } from '../types';
+
+import { AbstractArtifactAdapter } from './abstract';
+
+export class ZeroExArtifactAdapter extends AbstractArtifactAdapter {
+ private _artifactsPath: string;
+ private _sourcesPath: string;
+ constructor(artifactsPath: string, sourcesPath: string) {
+ super();
+ this._artifactsPath = artifactsPath;
+ this._sourcesPath = sourcesPath;
+ }
+ public async collectContractsDataAsync(): Promise<ContractData[]> {
+ const artifactsGlob = `${this._artifactsPath}/**/*.json`;
+ const artifactFileNames = glob.sync(artifactsGlob, { absolute: true });
+ const contractsData: ContractData[] = [];
+ for (const artifactFileName of artifactFileNames) {
+ const artifact = JSON.parse(fs.readFileSync(artifactFileName).toString());
+ let sources = _.keys(artifact.sources);
+ sources = _.map(sources, relativeFilePath => path.resolve(this._sourcesPath, relativeFilePath));
+ const contractName = artifact.contractName;
+ // We don't compute coverage for dependencies
+ const sourceCodes = _.map(sources, (source: string) => fs.readFileSync(source).toString());
+ const contractData = {
+ sourceCodes,
+ sources,
+ bytecode: artifact.compilerOutput.evm.bytecode.object,
+ sourceMap: artifact.compilerOutput.evm.bytecode.sourceMap,
+ runtimeBytecode: artifact.compilerOutput.evm.deployedBytecode.object,
+ sourceMapRuntime: artifact.compilerOutput.evm.deployedBytecode.sourceMap,
+ };
+ contractsData.push(contractData);
+ }
+ return contractsData;
+ }
+}
diff --git a/packages/sol-cov/src/artifact_adapters/abstract.ts b/packages/sol-cov/src/artifact_adapters/abstract.ts
new file mode 100644
index 000000000..fcc6562ad
--- /dev/null
+++ b/packages/sol-cov/src/artifact_adapters/abstract.ts
@@ -0,0 +1,5 @@
+import { ContractData } from '../types';
+
+export abstract class AbstractArtifactAdapter {
+ public abstract async collectContractsDataAsync(): Promise<ContractData[]>;
+}
diff --git a/packages/sol-cov/src/artifact_adapters/truffle.ts b/packages/sol-cov/src/artifact_adapters/truffle.ts
new file mode 100644
index 000000000..e891bb464
--- /dev/null
+++ b/packages/sol-cov/src/artifact_adapters/truffle.ts
@@ -0,0 +1,43 @@
+import { Compiler, CompilerOptions } from '@0xproject/sol-compiler';
+import * as fs from 'fs';
+import * as glob from 'glob';
+import * as _ from 'lodash';
+import * as path from 'path';
+import * as rimraf from 'rimraf';
+
+import { ContractData } from '../types';
+
+import { ZeroExArtifactAdapter } from './0x';
+import { AbstractArtifactAdapter } from './abstract';
+
+export class TruffleArtifactAdapter extends AbstractArtifactAdapter {
+ private _solcVersion: string;
+ private _sourcesPath: string;
+ constructor(sourcesPath: string, solcVersion: string) {
+ super();
+ this._solcVersion = solcVersion;
+ this._sourcesPath = sourcesPath;
+ }
+ public async collectContractsDataAsync(): Promise<ContractData[]> {
+ const artifactsDir = '0x-artifacts';
+ const compilerOptions: CompilerOptions = {
+ contractsDir: this._sourcesPath,
+ artifactsDir,
+ compilerSettings: {
+ outputSelection: {
+ ['*']: {
+ ['*']: ['abi', 'evm.bytecode.object', 'evm.deployedBytecode.object'],
+ },
+ },
+ },
+ contracts: '*',
+ solcVersion: this._solcVersion,
+ };
+ const compiler = new Compiler(compilerOptions);
+ await compiler.compileAsync();
+ const zeroExArtifactAdapter = new ZeroExArtifactAdapter(artifactsDir, this._sourcesPath);
+ const contractsDataFrom0xArtifacts = await zeroExArtifactAdapter.collectContractsDataAsync();
+ rimraf.sync(artifactsDir);
+ return contractsDataFrom0xArtifacts;
+ }
+}
diff --git a/packages/sol-cov/src/collect_contract_data.ts b/packages/sol-cov/src/collect_contract_data.ts
deleted file mode 100644
index 2c2a12835..000000000
--- a/packages/sol-cov/src/collect_contract_data.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import * as fs from 'fs';
-import * as glob from 'glob';
-import * as _ from 'lodash';
-import * as path from 'path';
-
-import { ContractData } from './types';
-
-export const collectContractsData = (artifactsPath: string, sourcesPath: string) => {
- const artifactsGlob = `${artifactsPath}/**/*.json`;
- const artifactFileNames = glob.sync(artifactsGlob, { absolute: true });
- const contractsData: ContractData[] = [];
- _.forEach(artifactFileNames, artifactFileName => {
- const artifact = JSON.parse(fs.readFileSync(artifactFileName).toString());
- const sources = _.keys(artifact.sources);
- const contractName = artifact.contractName;
- // We don't compute coverage for dependencies
- const sourceCodes = _.map(sources, (source: string) =>
- fs.readFileSync(path.join(sourcesPath, source)).toString(),
- );
- const contractData = {
- sourceCodes,
- sources,
- bytecode: artifact.compilerOutput.evm.bytecode.object,
- sourceMap: artifact.compilerOutput.evm.bytecode.sourceMap,
- runtimeBytecode: artifact.compilerOutput.evm.deployedBytecode.object,
- sourceMapRuntime: artifact.compilerOutput.evm.deployedBytecode.sourceMap,
- };
- contractsData.push(contractData);
- });
- return contractsData;
-};
diff --git a/packages/sol-cov/src/coverage_manager.ts b/packages/sol-cov/src/coverage_manager.ts
index 800ca96dd..65a6086c0 100644
--- a/packages/sol-cov/src/coverage_manager.ts
+++ b/packages/sol-cov/src/coverage_manager.ts
@@ -6,7 +6,7 @@ import * as _ from 'lodash';
import * as mkdirp from 'mkdirp';
import * as path from 'path';
-import { collectContractsData } from './collect_contract_data';
+import { AbstractArtifactAdapter } from './artifact_adapters/abstract';
import { collectCoverageEntries } from './collect_coverage_entries';
import { constants } from './constants';
import { parseSourceMap } from './source_maps';
@@ -34,41 +34,23 @@ import { utils } from './utils';
const mkdirpAsync = promisify<undefined>(mkdirp);
export class CoverageManager {
- private _sourcesPath: string;
+ private _artifactAdapter: AbstractArtifactAdapter;
+ private _verbose: boolean;
private _traceInfos: TraceInfo[] = [];
- private _contractsData: ContractData[] = [];
private _getContractCodeAsync: (address: string) => Promise<string>;
- constructor(
- artifactsPath: string,
- sourcesPath: string,
- getContractCodeAsync: (address: string) => Promise<string>,
- ) {
- this._getContractCodeAsync = getContractCodeAsync;
- this._sourcesPath = sourcesPath;
- this._contractsData = collectContractsData(artifactsPath, this._sourcesPath);
- }
- public appendTraceInfo(traceInfo: TraceInfo): void {
- this._traceInfos.push(traceInfo);
- }
- public async writeCoverageAsync(): Promise<void> {
- const finalCoverage = await this._computeCoverageAsync();
- const stringifiedCoverage = JSON.stringify(finalCoverage, null, '\t');
- await mkdirpAsync('coverage');
- fs.writeFileSync('coverage/coverage.json', stringifiedCoverage);
- }
- private _getSingleFileCoverageForTrace(
+ private static _getSingleFileCoverageForTrace(
contractData: ContractData,
coveredPcs: number[],
pcToSourceRange: { [programCounter: number]: SourceRange },
fileIndex: number,
): Coverage {
- const fileName = contractData.sources[fileIndex];
+ const absoluteFileName = contractData.sources[fileIndex];
const coverageEntriesDescription = collectCoverageEntries(contractData.sourceCodes[fileIndex]);
let sourceRanges = _.map(coveredPcs, coveredPc => pcToSourceRange[coveredPc]);
sourceRanges = _.compact(sourceRanges); // Some PC's don't map to a source range and we just ignore them.
// By default lodash does a shallow object comparasion. We JSON.stringify them and compare as strings.
sourceRanges = _.uniqBy(sourceRanges, s => JSON.stringify(s)); // We don't care if one PC was covered multiple times within a single transaction
- sourceRanges = _.filter(sourceRanges, sourceRange => sourceRange.fileName === fileName);
+ sourceRanges = _.filter(sourceRanges, sourceRange => sourceRange.fileName === absoluteFileName);
const branchCoverage: BranchCoverage = {};
const branchIds = _.keys(coverageEntriesDescription.branchMap);
for (const branchId of branchIds) {
@@ -118,7 +100,6 @@ export class CoverageManager {
);
statementCoverage[modifierStatementId] = isModifierCovered;
}
- const absoluteFileName = path.join(this._sourcesPath, fileName);
const partialCoverage = {
[absoluteFileName]: {
...coverageEntriesDescription,
@@ -131,18 +112,53 @@ export class CoverageManager {
};
return partialCoverage;
}
+ constructor(
+ artifactAdapter: AbstractArtifactAdapter,
+ getContractCodeAsync: (address: string) => Promise<string>,
+ verbose: boolean,
+ ) {
+ this._getContractCodeAsync = getContractCodeAsync;
+ this._artifactAdapter = artifactAdapter;
+ this._verbose = verbose;
+ }
+ public appendTraceInfo(traceInfo: TraceInfo): void {
+ // console.log(JSON.stringify(traceInfo, null, '\n'));
+ this._traceInfos.push(traceInfo);
+ }
+ public async writeCoverageAsync(): Promise<void> {
+ const finalCoverage = await this._computeCoverageAsync();
+ const stringifiedCoverage = JSON.stringify(finalCoverage, null, '\t');
+ await mkdirpAsync('coverage');
+ fs.writeFileSync('coverage/coverage.json', stringifiedCoverage);
+ }
private async _computeCoverageAsync(): Promise<Coverage> {
+ const contractsData = await this._artifactAdapter.collectContractsDataAsync();
const collector = new Collector();
for (const traceInfo of this._traceInfos) {
if (traceInfo.address !== constants.NEW_CONTRACT) {
// Runtime transaction
let runtimeBytecode = (traceInfo as TraceInfoExistingContract).runtimeBytecode;
runtimeBytecode = addHexPrefix(runtimeBytecode);
- const contractData = _.find(this._contractsData, { runtimeBytecode }) as ContractData;
+ const contractData = _.find(contractsData, contractDataCandidate => {
+ // Library linking placeholder: __ConvertLib____________________________
+ let runtimeBytecodeRegex = contractDataCandidate.runtimeBytecode.replace(/_.*_/, '.*');
+ // Last 86 characters is solidity compiler metadata that's different between compilations
+ runtimeBytecodeRegex = runtimeBytecodeRegex.replace(/.{86}$/, '');
+ // Libraries contain their own address at the beginning of the code and it's impossible to know it in advance
+ runtimeBytecodeRegex = runtimeBytecodeRegex.replace(
+ /^0x730000000000000000000000000000000000000000/,
+ '0x73........................................',
+ );
+ return !_.isNull(runtimeBytecode.match(runtimeBytecodeRegex));
+ }) as ContractData;
if (_.isUndefined(contractData)) {
- throw new Error(`Transaction to an unknown address: ${traceInfo.address}`);
+ if (this._verbose) {
+ // tslint:disable-next-line:no-console
+ console.warn(`Transaction to an unknown address: ${traceInfo.address}`);
+ }
+ continue;
}
- const bytecodeHex = contractData.runtimeBytecode.slice(2);
+ const bytecodeHex = runtimeBytecode.slice(2);
const sourceMap = contractData.sourceMapRuntime;
const pcToSourceRange = parseSourceMap(
contractData.sourceCodes,
@@ -151,7 +167,7 @@ export class CoverageManager {
contractData.sources,
);
for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) {
- const singleFileCoverageForTrace = this._getSingleFileCoverageForTrace(
+ const singleFileCoverageForTrace = CoverageManager._getSingleFileCoverageForTrace(
contractData,
traceInfo.coveredPcs,
pcToSourceRange,
@@ -163,13 +179,26 @@ export class CoverageManager {
// Contract creation transaction
let bytecode = (traceInfo as TraceInfoNewContract).bytecode;
bytecode = addHexPrefix(bytecode);
- const contractData = _.find(this._contractsData, contractDataCandidate =>
- bytecode.startsWith(contractDataCandidate.bytecode),
- ) as ContractData;
+ const contractData = _.find(contractsData, contractDataCandidate => {
+ // Library linking placeholder: __ConvertLib____________________________
+ let bytecodeRegex = contractDataCandidate.bytecode.replace(/_.*_/, '.*');
+ // Last 86 characters is solidity compiler metadata that's different between compilations
+ bytecodeRegex = bytecodeRegex.replace(/.{86}$/, '');
+ // Libraries contain their own address at the beginning of the code and it's impossible to know it in advance
+ bytecodeRegex = bytecodeRegex.replace(
+ /^0x730000000000000000000000000000000000000000/,
+ '0x73........................................',
+ );
+ return !_.isNull(bytecode.match(bytecodeRegex));
+ }) as ContractData;
if (_.isUndefined(contractData)) {
- throw new Error(`Unknown contract creation transaction`);
+ if (this._verbose) {
+ // tslint:disable-next-line:no-console
+ console.warn(`Unknown contract creation transaction`);
+ }
+ continue;
}
- const bytecodeHex = contractData.bytecode.slice(2);
+ const bytecodeHex = bytecode.slice(2);
const sourceMap = contractData.sourceMap;
const pcToSourceRange = parseSourceMap(
contractData.sourceCodes,
@@ -178,7 +207,7 @@ export class CoverageManager {
contractData.sources,
);
for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) {
- const singleFileCoverageForTrace = this._getSingleFileCoverageForTrace(
+ const singleFileCoverageForTrace = CoverageManager._getSingleFileCoverageForTrace(
contractData,
traceInfo.coveredPcs,
pcToSourceRange,
diff --git a/packages/sol-cov/src/coverage_subprovider.ts b/packages/sol-cov/src/coverage_subprovider.ts
index 08efeaa24..f421a17fd 100644
--- a/packages/sol-cov/src/coverage_subprovider.ts
+++ b/packages/sol-cov/src/coverage_subprovider.ts
@@ -1,10 +1,13 @@
import { Callback, ErrorCallback, NextCallback, Subprovider } from '@0xproject/subproviders';
import { BlockParam, CallData, JSONRPCRequestPayload, TransactionTrace, TxData } from '@0xproject/types';
+import * as fs from 'fs';
import * as _ from 'lodash';
import { Lock } from 'semaphore-async-await';
+import { AbstractArtifactAdapter } from './artifact_adapters/abstract';
import { constants } from './constants';
import { CoverageManager } from './coverage_manager';
+import { getTracesByContractAddress } from './trace';
import { TraceInfoExistingContract, TraceInfoNewContract } from './types';
interface MaybeFakeTxData extends TxData {
@@ -26,15 +29,15 @@ export class CoverageSubprovider extends Subprovider {
private _defaultFromAddress: string;
/**
* Instantiates a CoverageSubprovider instance
- * @param artifactsPath Path to the smart contract artifacts
- * @param sourcesPath Path to the smart contract source files
+ * @param artifactAdapter Adapter for used artifacts format (0x, truffle, giveth, etc.)
* @param defaultFromAddress default from address to use when sending transactions
+ * @param verbose If true, we will log any unknown transactions. Otherwise we will ignore them
*/
- constructor(artifactsPath: string, sourcesPath: string, defaultFromAddress: string) {
+ constructor(artifactAdapter: AbstractArtifactAdapter, defaultFromAddress: string, verbose: boolean = true) {
super();
this._lock = new Lock();
this._defaultFromAddress = defaultFromAddress;
- this._coverageManager = new CoverageManager(artifactsPath, sourcesPath, this._getContractCodeAsync.bind(this));
+ this._coverageManager = new CoverageManager(artifactAdapter, this._getContractCodeAsync.bind(this), verbose);
}
/**
* Write the test coverage results to a file in Istanbul format.
@@ -119,8 +122,10 @@ export class CoverageSubprovider extends Subprovider {
};
const jsonRPCResponsePayload = await this.emitPayloadAsync(payload);
const trace: TransactionTrace = jsonRPCResponsePayload.result;
- const coveredPcs = _.map(trace.structLogs, log => log.pc);
if (address === constants.NEW_CONTRACT) {
+ // TODO handle calls to external contracts and contract creations from within the constructor
+ const structLogsOnDepth0 = _.filter(trace.structLogs, { depth: 0 });
+ const coveredPcs = _.map(structLogsOnDepth0, log => log.pc);
const traceInfo: TraceInfoNewContract = {
coveredPcs,
txHash,
@@ -129,15 +134,20 @@ export class CoverageSubprovider extends Subprovider {
};
this._coverageManager.appendTraceInfo(traceInfo);
} else {
- payload = { method: 'eth_getCode', params: [address, 'latest'] };
- const runtimeBytecode = (await this.emitPayloadAsync(payload)).result;
- const traceInfo: TraceInfoExistingContract = {
- coveredPcs,
- txHash,
- address,
- runtimeBytecode,
- };
- this._coverageManager.appendTraceInfo(traceInfo);
+ const tracesByContractAddress = getTracesByContractAddress(trace.structLogs, address);
+ for (const subcallAddress of _.keys(tracesByContractAddress)) {
+ payload = { method: 'eth_getCode', params: [subcallAddress, 'latest'] };
+ const runtimeBytecode = (await this.emitPayloadAsync(payload)).result;
+ const traceForThatSubcall = tracesByContractAddress[subcallAddress];
+ const coveredPcs = _.map(traceForThatSubcall, log => log.pc);
+ const traceInfo: TraceInfoExistingContract = {
+ coveredPcs,
+ txHash,
+ address: subcallAddress,
+ runtimeBytecode,
+ };
+ this._coverageManager.appendTraceInfo(traceInfo);
+ }
}
}
private async _recordCallTraceAsync(callData: Partial<CallData>, blockNumber: BlockParam): Promise<void> {
diff --git a/packages/sol-cov/src/index.ts b/packages/sol-cov/src/index.ts
index e5c5e5be3..18031372b 100644
--- a/packages/sol-cov/src/index.ts
+++ b/packages/sol-cov/src/index.ts
@@ -1 +1,5 @@
export { CoverageSubprovider } from './coverage_subprovider';
+export { ZeroExArtifactAdapter } from './artifact_adapters/0x';
+export { TruffleArtifactAdapter } from './artifact_adapters/truffle';
+export { AbstractArtifactAdapter } from './artifact_adapters/abstract';
+export { ContractData } from './types';
diff --git a/packages/sol-cov/src/trace.ts b/packages/sol-cov/src/trace.ts
new file mode 100644
index 000000000..6bc28989d
--- /dev/null
+++ b/packages/sol-cov/src/trace.ts
@@ -0,0 +1,100 @@
+import { StructLog, TransactionTrace } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+import { addHexPrefix, stripHexPrefix } from 'ethereumjs-util';
+import * as fs from 'fs';
+import * as _ from 'lodash';
+
+export interface TraceByContractAddress {
+ [contractAddress: string]: StructLog[];
+}
+function padZeros(address: string) {
+ return addHexPrefix(_.padStart(stripHexPrefix(address), 40, '0'));
+}
+
+export function getTracesByContractAddress(structLogs: StructLog[], startAddress: string): TraceByContractAddress {
+ const traceByContractAddress: TraceByContractAddress = {};
+ let currentTraceSegment = [];
+ const callStack = [startAddress];
+ // tslint:disable-next-line: prefer-for-of
+ for (let i = 0; i < structLogs.length; ++i) {
+ const structLog = structLogs[i];
+ if (structLog.depth !== callStack.length - 1) {
+ throw new Error("Malformed trace. trace depth doesn't match call stack depth");
+ }
+ // After that check we have a guarantee that call stack is never empty
+ // If it would: callStack.length - 1 === structLog.depth === -1
+ // That means that we can always safely pop from it
+ currentTraceSegment.push(structLog);
+
+ if (structLog.op === 'DELEGATECALL') {
+ const currentAddress = _.last(callStack) as string;
+ const jumpAddressOffset = 4;
+ const newAddress = padZeros(new BigNumber(addHexPrefix(structLog.stack[jumpAddressOffset])).toString(16));
+ callStack.push(newAddress);
+ traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat(
+ currentTraceSegment,
+ );
+ currentTraceSegment = [];
+ } else if (structLog.op === 'RETURN') {
+ const currentAddress = callStack.pop() as string;
+ traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat(
+ currentTraceSegment,
+ );
+ currentTraceSegment = [];
+ } else if (structLog.op === 'STOP') {
+ const currentAddress = callStack.pop() as string;
+ traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat(
+ currentTraceSegment,
+ );
+ currentTraceSegment = [];
+ if (i !== structLogs.length - 1) {
+ throw new Error('Malformed trace. STOP is not the last opcode executed');
+ }
+ } else if (structLog.op === 'CREATE') {
+ console.warn(
+ "Detected a contract created from within another contract. Sol-cov currently doesn't support that scenario. We'll just skip that trace",
+ );
+ return traceByContractAddress;
+ } else if (structLog.op === 'CALL') {
+ const currentAddress = _.last(callStack) as string;
+ const jumpAddressOffset = 2;
+ const newAddress = padZeros(new BigNumber(addHexPrefix(structLog.stack[jumpAddressOffset])).toString(16));
+ callStack.push(newAddress);
+ traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat(
+ currentTraceSegment,
+ );
+ currentTraceSegment = [];
+ } else if (structLog.op === 'CALLCODE') {
+ throw new Error('CALLCODE opcode unsupported by coverage');
+ } else if (structLog.op === 'STATICCALL') {
+ throw new Error('STATICCALL opcode unsupported by coverage');
+ } else if (structLog.op === 'REVERT') {
+ const currentAddress = callStack.pop() as string;
+ traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat(
+ currentTraceSegment,
+ );
+ currentTraceSegment = [];
+ if (i !== structLogs.length - 1) {
+ throw new Error('Malformed trace. REVERT is not the last opcode executed');
+ }
+ } else if (structLog.op === 'INVALID') {
+ const currentAddress = callStack.pop() as string;
+ traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat(
+ currentTraceSegment,
+ );
+ currentTraceSegment = [];
+ if (i !== structLogs.length - 1) {
+ throw new Error('Malformed trace. INVALID is not the last opcode executed');
+ }
+ } else if (structLog.op === 'SELFDESTRUCT') {
+ throw new Error('SELFDESTRUCT opcode unsupported by coverage');
+ }
+ }
+ if (callStack.length !== 0) {
+ throw new Error('Malformed trace. Call stack non empty at the end');
+ }
+ if (currentTraceSegment.length !== 0) {
+ throw new Error('Malformed trace. currentTraceSegment non empty at the end');
+ }
+ return traceByContractAddress;
+}