From ab6f8b0a1f85b4b75c1395308fe2093af32bef60 Mon Sep 17 00:00:00 2001 From: John Kane Date: Mon, 11 Sep 2023 14:03:06 +0100 Subject: [PATCH] feat: add `Held` status to exState status To support execution strategies returning `held` status in the future, we are adding it into the execution status of exStates. The return result of a deployment can now include a held property listing the futures that have been held and the reason for the hold. This is wired through to the ui as a new sort of Held result instead of treating it as success or failure. Resolves #451 --- packages/core/src/internal/batcher.ts | 1 + packages/core/src/internal/deployer.ts | 23 +++- .../internal/execution/execution-engine.ts | 6 +- .../helpers/complete-execution-state.ts | 2 + .../execution/types/execution-result.ts | 23 +++- .../execution/types/execution-state.ts | 1 + packages/core/src/internal/formatters.ts | 7 +- .../journal/utils/emitExecutionEvent.ts | 18 ++- ...n-failed.ts => has-execution-succeeded.ts} | 12 +- packages/core/src/types/deploy.ts | 10 +- packages/core/src/types/execution-events.ts | 17 ++- .../hardhat-plugin/src/ui/UiEventHandler.tsx | 10 +- .../src/ui/VerboseEventHandler.ts | 108 ++++++++++++------ .../components/execution/BatchExecution.tsx | 9 ++ packages/hardhat-plugin/src/ui/types.ts | 10 +- ...-deployment-result-to-exception-message.ts | 21 +++- ...-deployment-result-to-exception-message.ts | 35 ++++++ 17 files changed, 247 insertions(+), 66 deletions(-) rename packages/core/src/internal/views/{has-execution-failed.ts => has-execution-succeeded.ts} (66%) diff --git a/packages/core/src/internal/batcher.ts b/packages/core/src/internal/batcher.ts index 93c5f9efb..3c4ef0b90 100644 --- a/packages/core/src/internal/batcher.ts +++ b/packages/core/src/internal/batcher.ts @@ -74,6 +74,7 @@ export class Batcher { switch (executionState.status) { case ExecutionStatus.FAILED: case ExecutionStatus.TIMEOUT: + case ExecutionStatus.HELD: case ExecutionStatus.STARTED: return [f.id, VisitStatus.UNVISITED]; case ExecutionStatus.SUCCESS: diff --git a/packages/core/src/internal/deployer.ts b/packages/core/src/internal/deployer.ts index 3b0866811..76623b8e6 100644 --- a/packages/core/src/internal/deployer.ts +++ b/packages/core/src/internal/deployer.ts @@ -291,6 +291,26 @@ export class Deployer { successful: Object.values(deploymentState.executionStates) .filter((ex) => ex.status === ExecutionStatus.SUCCESS) .map((ex) => ex.id), + held: Object.values(deploymentState.executionStates) + .filter(canFail) + .filter((ex) => ex.status === ExecutionStatus.HELD) + .map((ex) => { + assertIgnitionInvariant( + ex.result !== undefined, + `Execution state ${ex.id} is marked as held but has no result` + ); + + assertIgnitionInvariant( + ex.result.type === ExecutionResultType.STRATEGY_HELD, + `Execution state ${ex.id} is marked as held but has ${ex.result.type} instead of a held result` + ); + + return { + futureId: ex.id, + heldId: ex.result.heldId, + reason: ex.result.reason, + }; + }), timedOut: Object.values(deploymentState.executionStates) .filter(canTimeout) .filter((ex) => ex.status === ExecutionStatus.TIMEOUT) @@ -304,7 +324,8 @@ export class Deployer { .map((ex) => { assertIgnitionInvariant( ex.result !== undefined && - ex.result.type !== ExecutionResultType.SUCCESS, + ex.result.type !== ExecutionResultType.SUCCESS && + ex.result.type !== ExecutionResultType.STRATEGY_HELD, `Execution state ${ex.id} is marked as failed but has no error result` ); diff --git a/packages/core/src/internal/execution/execution-engine.ts b/packages/core/src/internal/execution/execution-engine.ts index c80dbdd76..69b2bd6ac 100644 --- a/packages/core/src/internal/execution/execution-engine.ts +++ b/packages/core/src/internal/execution/execution-engine.ts @@ -13,7 +13,7 @@ import { import { DeploymentLoader } from "../deployment-loader/types"; import { getFuturesFromModule } from "../utils/get-futures-from-module"; import { getPendingNonceAndSender } from "../views/execution-state/get-pending-nonce-and-sender"; -import { hasExecutionFailed } from "../views/has-execution-failed"; +import { hasExecutionSucceeded } from "../views/has-execution-succeeded"; import { isBatchFinished } from "../views/is-batch-finished"; import { applyNewMessage } from "./deployment-state-helpers"; @@ -108,7 +108,9 @@ export class ExecutionEngine { deploymentState ); - if (executionBatch.some((f) => hasExecutionFailed(f, deploymentState))) { + if ( + !executionBatch.every((f) => hasExecutionSucceeded(f, deploymentState)) + ) { return deploymentState; } } diff --git a/packages/core/src/internal/execution/reducers/helpers/complete-execution-state.ts b/packages/core/src/internal/execution/reducers/helpers/complete-execution-state.ts index d99f80e95..ed81176a0 100644 --- a/packages/core/src/internal/execution/reducers/helpers/complete-execution-state.ts +++ b/packages/core/src/internal/execution/reducers/helpers/complete-execution-state.ts @@ -72,5 +72,7 @@ function _mapResultTypeToStatus( return ExecutionStatus.FAILED; case ExecutionResultType.STRATEGY_ERROR: return ExecutionStatus.FAILED; + case ExecutionResultType.STRATEGY_HELD: + return ExecutionStatus.HELD; } } diff --git a/packages/core/src/internal/execution/types/execution-result.ts b/packages/core/src/internal/execution/types/execution-result.ts index 52b4a009e..f43f41752 100644 --- a/packages/core/src/internal/execution/types/execution-result.ts +++ b/packages/core/src/internal/execution/types/execution-result.ts @@ -12,6 +12,7 @@ export enum ExecutionResultType { REVERTED_TRANSACTION = "REVERTED_TRANSACTION", STATIC_CALL_ERROR = "STATIC_CALL_ERROR", STRATEGY_ERROR = "STRATEGY_ERROR", + STRATEGY_HELD = "STRATEGY_HELD", } /** @@ -59,6 +60,16 @@ export interface StrategyErrorExecutionResult { error: string; } +/** + * The execution strategy returned a strategy-specific hold e.g. + * waiting for off-chain multi-sig confirmations. + */ +export interface StrategyHeldExecutionResult { + type: ExecutionResultType.STRATEGY_HELD; + heldId: number; + reason: string; +} + /** * A deployment was successfully executed. */ @@ -77,7 +88,8 @@ export type DeploymentExecutionResult = | StrategySimulationErrorExecutionResult | RevertedTransactionExecutionResult | FailedStaticCallExecutionResult - | StrategyErrorExecutionResult; + | StrategyErrorExecutionResult + | StrategyHeldExecutionResult; /** * A call future was successfully executed. @@ -95,7 +107,8 @@ export type CallExecutionResult = | StrategySimulationErrorExecutionResult | RevertedTransactionExecutionResult | FailedStaticCallExecutionResult - | StrategyErrorExecutionResult; + | StrategyErrorExecutionResult + | StrategyHeldExecutionResult; /** * A send data future was successfully executed. @@ -113,7 +126,8 @@ export type SendDataExecutionResult = | StrategySimulationErrorExecutionResult | RevertedTransactionExecutionResult | FailedStaticCallExecutionResult - | StrategyErrorExecutionResult; + | StrategyErrorExecutionResult + | StrategyHeldExecutionResult; /** * A static call future was successfully executed. @@ -129,4 +143,5 @@ export interface SuccessfulStaticCallExecutionResult { export type StaticCallExecutionResult = | SuccessfulStaticCallExecutionResult | FailedStaticCallExecutionResult - | StrategyErrorExecutionResult; + | StrategyErrorExecutionResult + | StrategyHeldExecutionResult; diff --git a/packages/core/src/internal/execution/types/execution-state.ts b/packages/core/src/internal/execution/types/execution-state.ts index af0b11725..c7c690574 100644 --- a/packages/core/src/internal/execution/types/execution-state.ts +++ b/packages/core/src/internal/execution/types/execution-state.ts @@ -26,6 +26,7 @@ export enum ExecutionStatus { STARTED = "STARATED", TIMEOUT = "TIMEOUT", SUCCESS = "SUCCESS", + HELD = "HELD", FAILED = "FAILED", } diff --git a/packages/core/src/internal/formatters.ts b/packages/core/src/internal/formatters.ts index d3dd46596..d027bc05b 100644 --- a/packages/core/src/internal/formatters.ts +++ b/packages/core/src/internal/formatters.ts @@ -31,19 +31,14 @@ export function formatExecutionError( return `Simulating the transaction failed with error: ${formatFailedEvmExecutionResult( result.error )}`; - case ExecutionResultType.STRATEGY_SIMULATION_ERROR: return `Simulating the transaction failed with error: ${result.error}`; - - case ExecutionResultType.REVERTED_TRANSACTION: { + case ExecutionResultType.REVERTED_TRANSACTION: return `Transaction ${result.txHash} reverted`; - } - case ExecutionResultType.STATIC_CALL_ERROR: return `Static call failed with error: ${formatFailedEvmExecutionResult( result.error )}`; - case ExecutionResultType.STRATEGY_ERROR: return `Execution failed with error: ${result.error}`; } diff --git a/packages/core/src/internal/journal/utils/emitExecutionEvent.ts b/packages/core/src/internal/journal/utils/emitExecutionEvent.ts index 427b44732..db7c4b12a 100644 --- a/packages/core/src/internal/journal/utils/emitExecutionEvent.ts +++ b/packages/core/src/internal/journal/utils/emitExecutionEvent.ts @@ -1,9 +1,9 @@ import { ExecutionEventListener, - ExecutionEventType, + ExecutionEventNetworkInteractionType, ExecutionEventResult, ExecutionEventResultType, - ExecutionEventNetworkInteractionType, + ExecutionEventType, } from "../../../types/execution-events"; import { SolidityParameterType } from "../../../types/module"; import { @@ -217,6 +217,13 @@ function convertExecutionResultToEventResult( error: "Transaction reverted", }; } + case ExecutionResultType.STRATEGY_HELD: { + return { + type: ExecutionEventResultType.HELD, + heldId: result.heldId, + reason: result.reason, + }; + } } } @@ -241,6 +248,13 @@ function convertStaticCallResultToExecutionEventResult( error: result.error, }; } + case ExecutionResultType.STRATEGY_HELD: { + return { + type: ExecutionEventResultType.HELD, + heldId: result.heldId, + reason: result.reason, + }; + } } } diff --git a/packages/core/src/internal/views/has-execution-failed.ts b/packages/core/src/internal/views/has-execution-succeeded.ts similarity index 66% rename from packages/core/src/internal/views/has-execution-failed.ts rename to packages/core/src/internal/views/has-execution-succeeded.ts index 004185ebb..16389ed55 100644 --- a/packages/core/src/internal/views/has-execution-failed.ts +++ b/packages/core/src/internal/views/has-execution-succeeded.ts @@ -3,23 +3,21 @@ import { DeploymentState } from "../execution/types/deployment-state"; import { ExecutionStatus } from "../execution/types/execution-state"; /** - * Returns true if the execution of the given future has failed. + * Returns true if the execution of the given future has succeeded. * * @param future The future. * @param deploymentState The deployment state to check against. - * @returns true if it failed. + * @returns true if it succeeded. */ -export function hasExecutionFailed( +export function hasExecutionSucceeded( future: Future, deploymentState: DeploymentState ): boolean { const exState = deploymentState.executionStates[future.id]; + if (exState === undefined) { return false; } - return ( - exState.status === ExecutionStatus.FAILED || - exState.status === ExecutionStatus.TIMEOUT - ); + return exState.status === ExecutionStatus.SUCCESS; } diff --git a/packages/core/src/types/deploy.ts b/packages/core/src/types/deploy.ts index 7e24e8ce3..736edf423 100644 --- a/packages/core/src/types/deploy.ts +++ b/packages/core/src/types/deploy.ts @@ -114,17 +114,23 @@ export interface ExecutionErrorDeploymentResult { type: DeploymentResultType.EXECUTION_ERROR; /** - * A list of all the future that have started executed but have not + * A list of all the futures that have started executing but have not * finished, neither successfully nor unsuccessfully. */ started: string[]; /** - * A list of all the future that have timed out, including details of the + * A list of all the futures that have timed out, including details of the * network interaction that timed out. */ timedOut: Array<{ futureId: string; networkInteractionId: number }>; + /** + * A list of all the futures that are being Held as determined by the execution + * strategy, i.e. an off-chain process is not yet complete. + */ + held: Array<{ futureId: string; heldId: number; reason: string }>; + /** * A list of all the future that have failed, including the details of * the network interaction that errored. diff --git a/packages/core/src/types/execution-events.ts b/packages/core/src/types/execution-events.ts index b870fd99e..a29d6f2c9 100644 --- a/packages/core/src/types/execution-events.ts +++ b/packages/core/src/types/execution-events.ts @@ -360,6 +360,7 @@ export enum ExecutionEventNetworkInteractionType { export enum ExecutionEventResultType { SUCCESS = "SUCCESS", ERROR = "ERROR", + HELD = "HELD", } /** @@ -367,7 +368,10 @@ export enum ExecutionEventResultType { * * @beta */ -export type ExecutionEventResult = ExecutionEventSuccess | ExecutionEventError; +export type ExecutionEventResult = + | ExecutionEventSuccess + | ExecutionEventError + | ExecutionEventHeld; /** * A successful result of a future's execution. @@ -389,6 +393,17 @@ export interface ExecutionEventError { error: string; } +/** + * A hold result of a future's execution. + * + * @beta + */ +export interface ExecutionEventHeld { + type: ExecutionEventResultType.HELD; + heldId: number; + reason: string; +} + /** * A mapping of execution event types to their corresponding event. * diff --git a/packages/hardhat-plugin/src/ui/UiEventHandler.tsx b/packages/hardhat-plugin/src/ui/UiEventHandler.tsx index 8e85f0adb..da998878f 100644 --- a/packages/hardhat-plugin/src/ui/UiEventHandler.tsx +++ b/packages/hardhat-plugin/src/ui/UiEventHandler.tsx @@ -40,6 +40,7 @@ import { UiBatches, UiFuture, UiFutureErrored, + UiFutureHeld, UiFutureStatusType, UiFutureSuccess, UiState, @@ -396,7 +397,7 @@ export class UiEventHandler implements ExecutionEventListener { private _getFutureStatusFromEventResult( result: ExecutionEventResult - ): UiFutureSuccess | UiFutureErrored { + ): UiFutureSuccess | UiFutureErrored | UiFutureHeld { switch (result.type) { case ExecutionEventResultType.SUCCESS: { return { @@ -410,6 +411,13 @@ export class UiEventHandler implements ExecutionEventListener { message: result.error, }; } + case ExecutionEventResultType.HELD: { + return { + type: UiFutureStatusType.HELD, + heldId: result.heldId, + reason: result.reason, + }; + } } } diff --git a/packages/hardhat-plugin/src/ui/VerboseEventHandler.ts b/packages/hardhat-plugin/src/ui/VerboseEventHandler.ts index 260eb022c..b8ad6e9cf 100644 --- a/packages/hardhat-plugin/src/ui/VerboseEventHandler.ts +++ b/packages/hardhat-plugin/src/ui/VerboseEventHandler.ts @@ -47,16 +47,24 @@ export class VerboseEventHandler implements ExecutionEventListener { public deploymentExecutionStateComplete( event: DeploymentExecutionStateCompleteEvent ): void { - if (event.result.type === ExecutionEventResultType.SUCCESS) { - console.log( - `Successfully completed the execution of deployment future ${ - event.futureId - } with address ${event.result.result ?? "undefined"}` - ); - } else { - console.log( - `Execution of future ${event.futureId} failed with reason: ${event.result.error}` - ); + switch (event.result.type) { + case ExecutionEventResultType.SUCCESS: { + return console.log( + `Successfully completed the execution of deployment future ${ + event.futureId + } with address ${event.result.result ?? "undefined"}` + ); + } + case ExecutionEventResultType.ERROR: { + return console.log( + `Execution of future ${event.futureId} failed with reason: ${event.result.error}` + ); + } + case ExecutionEventResultType.HELD: { + return console.log( + `Execution of future ${event.futureId}/${event.result.heldId} held with reason: ${event.result.reason}` + ); + } } } @@ -69,14 +77,22 @@ export class VerboseEventHandler implements ExecutionEventListener { public callExecutionStateComplete( event: CallExecutionStateCompleteEvent ): void { - if (event.result.type === ExecutionEventResultType.SUCCESS) { - console.log( - `Successfully completed the execution of call future ${event.futureId}` - ); - } else { - console.log( - `Execution of future ${event.futureId} failed with reason: ${event.result.error}` - ); + switch (event.result.type) { + case ExecutionEventResultType.SUCCESS: { + return console.log( + `Successfully completed the execution of call future ${event.futureId}` + ); + } + case ExecutionEventResultType.ERROR: { + return console.log( + `Execution of call future ${event.futureId} failed with reason: ${event.result.error}` + ); + } + case ExecutionEventResultType.HELD: { + return console.log( + `Execution of call future ${event.futureId}/${event.result.heldId} held with reason: ${event.result.reason}` + ); + } } } @@ -89,16 +105,24 @@ export class VerboseEventHandler implements ExecutionEventListener { public staticCallExecutionStateComplete( event: StaticCallExecutionStateCompleteEvent ): void { - if (event.result.type === ExecutionEventResultType.SUCCESS) { - console.log( - `Successfully completed the execution of static call future ${ - event.futureId - } with result ${event.result.result ?? "undefined"}` - ); - } else { - console.log( - `Execution of future ${event.futureId} failed with reason: ${event.result.error}` - ); + switch (event.result.type) { + case ExecutionEventResultType.SUCCESS: { + return console.log( + `Successfully completed the execution of static call future ${ + event.futureId + } with result ${event.result.result ?? "undefined"}` + ); + } + case ExecutionEventResultType.ERROR: { + return console.log( + `Execution of static call future ${event.futureId} failed with reason: ${event.result.error}` + ); + } + case ExecutionEventResultType.HELD: { + return console.log( + `Execution of static call future ${event.futureId}/${event.result.heldId} held with reason: ${event.result.reason}` + ); + } } } @@ -111,16 +135,24 @@ export class VerboseEventHandler implements ExecutionEventListener { public sendDataExecutionStateComplete( event: SendDataExecutionStateCompleteEvent ): void { - if (event.result.type === ExecutionEventResultType.SUCCESS) { - console.log( - `Successfully completed the execution of send data future ${ - event.futureId - } in tx ${event.result.result ?? "undefined"}` - ); - } else { - console.log( - `Execution of future ${event.futureId} failed with reason: ${event.result.error}` - ); + switch (event.result.type) { + case ExecutionEventResultType.SUCCESS: { + return console.log( + `Successfully completed the execution of send data future ${ + event.futureId + } in tx ${event.result.result ?? "undefined"}` + ); + } + case ExecutionEventResultType.ERROR: { + return console.log( + `Execution of future ${event.futureId} failed with reason: ${event.result.error}` + ); + } + case ExecutionEventResultType.HELD: { + return console.log( + `Execution of send future ${event.futureId}/${event.result.heldId} held with reason: ${event.result.reason}` + ); + } } } diff --git a/packages/hardhat-plugin/src/ui/components/execution/BatchExecution.tsx b/packages/hardhat-plugin/src/ui/components/execution/BatchExecution.tsx index e89a42529..16b028c46 100644 --- a/packages/hardhat-plugin/src/ui/components/execution/BatchExecution.tsx +++ b/packages/hardhat-plugin/src/ui/components/execution/BatchExecution.tsx @@ -78,6 +78,9 @@ const StatusBadge = ({ future }: { future: UiFuture }) => { case UiFutureStatusType.ERRORED: badge = ; break; + case UiFutureStatusType.HELD: + badge = 🔶; + break; } return ( @@ -139,6 +142,12 @@ function resolveFutureColors(future: UiFuture): { borderStyle: "bold", textColor: "white", }; + case UiFutureStatusType.HELD: + return { + borderColor: "yellow", + borderStyle: "bold", + textColor: "white", + }; } } diff --git a/packages/hardhat-plugin/src/ui/types.ts b/packages/hardhat-plugin/src/ui/types.ts index c99831e88..266d0ba0f 100644 --- a/packages/hardhat-plugin/src/ui/types.ts +++ b/packages/hardhat-plugin/src/ui/types.ts @@ -5,6 +5,7 @@ export enum UiFutureStatusType { SUCCESS = "SUCCESS", PENDING = "PENDING", ERRORED = "ERRORED", + HELD = "HELD", } export enum UiStateDeploymentStatus { @@ -31,11 +32,18 @@ export interface UiFutureErrored { message: string; } +export interface UiFutureHeld { + type: UiFutureStatusType.HELD; + heldId: number; + reason: string; +} + export type UiFutureStatus = | UiFutureUnstarted | UiFutureSuccess | UiFuturePending - | UiFutureErrored; + | UiFutureErrored + | UiFutureHeld; export interface UiFuture { status: UiFutureStatus; diff --git a/packages/hardhat-plugin/src/utils/error-deployment-result-to-exception-message.ts b/packages/hardhat-plugin/src/utils/error-deployment-result-to-exception-message.ts index 0498af3f5..9bf4fb8a4 100644 --- a/packages/hardhat-plugin/src/utils/error-deployment-result-to-exception-message.ts +++ b/packages/hardhat-plugin/src/utils/error-deployment-result-to-exception-message.ts @@ -59,6 +59,7 @@ function _convertExecutionError(result: ExecutionErrorDeploymentResult) { const messageDetails = { timeouts: result.timedOut.length > 0, failures: result.failed.length > 0, + held: result.held.length > 0, }; if (messageDetails.timeouts) { @@ -79,6 +80,14 @@ function _convertExecutionError(result: ExecutionErrorDeploymentResult) { sections.push(`Failures:\n\n${errorList.join("\n")}`); } + if (messageDetails.held) { + const reasonList = result.held.map( + ({ futureId, heldId, reason }) => ` * ${futureId}/${heldId}: ${reason}` + ); + + sections.push(`Held:\n\n${reasonList.join("\n")}`); + } + return `The deployment wasn't successful, there were ${_toText( messageDetails )}: @@ -89,16 +98,26 @@ ${sections.join("\n\n")}`; function _toText({ timeouts, failures, + held, }: { timeouts: boolean; failures: boolean; + held: boolean; }): string { - if (timeouts && failures) { + if (timeouts && failures && held) { + return "timeouts, failures and holds"; + } else if (timeouts && failures) { return "timeouts and failures"; + } else if (failures && held) { + return "failures and holds"; + } else if (timeouts && held) { + return "timeouts and holds"; } else if (timeouts) { return "timeouts"; } else if (failures) { return "failures"; + } else if (held) { + return "holds"; } throw new HardhatPluginError( diff --git a/packages/hardhat-plugin/test/utils/error-deployment-result-to-exception-message.ts b/packages/hardhat-plugin/test/utils/error-deployment-result-to-exception-message.ts index e53a880f0..0fabeec6a 100644 --- a/packages/hardhat-plugin/test/utils/error-deployment-result-to-exception-message.ts +++ b/packages/hardhat-plugin/test/utils/error-deployment-result-to-exception-message.ts @@ -66,6 +66,7 @@ describe("display error deployment result", () => { { futureId: "MyModule:MyContract", networkInteractionId: 1 }, { futureId: "MyModule:AnotherContract", networkInteractionId: 3 }, ], + held: [], failed: [], successful: [], }; @@ -81,11 +82,44 @@ Timed out: ); }); + it("should display an execution error with holds", () => { + const result: ExecutionErrorDeploymentResult = { + type: DeploymentResultType.EXECUTION_ERROR, + started: [], + timedOut: [], + held: [ + { + futureId: "MyModule:MyContract", + heldId: 1, + reason: "Vote is not complete", + }, + { + futureId: "MyModule:AnotherContract", + heldId: 3, + reason: "Server timed out", + }, + ], + failed: [], + successful: [], + }; + + assert.equal( + errorDeploymentResultToExceptionMessage(result), + `The deployment wasn't successful, there were holds: + +Held: + + * MyModule:MyContract/1: Vote is not complete + * MyModule:AnotherContract/3: Server timed out` + ); + }); + it("should display an execution error with execution failures", () => { const result: ExecutionErrorDeploymentResult = { type: DeploymentResultType.EXECUTION_ERROR, started: [], timedOut: [], + held: [], failed: [ { futureId: "MyModule:MyContract", @@ -120,6 +154,7 @@ Failures: { futureId: "MyModule:FirstContract", networkInteractionId: 1 }, { futureId: "MyModule:SecondContract", networkInteractionId: 3 }, ], + held: [], failed: [ { futureId: "MyModule:ThirdContract",