Repository: NomicFoundation/hardhat-ignition
Pull request #548 (2023-10-12 22:40:32)

feat/update status UI
Update the status commands UI to match the updates in the deploy command.

## TODO

- [x] Separate out display logic from cli code
- [x] Add tests against existing UI
- [x] Update UI to match new layout


Patch:
diff --git a/packages/hardhat-plugin/src/index.ts b/packages/hardhat-plugin/src/index.ts
index 3056c91a..472fe289 100644
--- a/packages/hardhat-plugin/src/index.ts
+++ b/packages/hardhat-plugin/src/index.ts
@@ -4,13 +4,13 @@ import {
   IgnitionError,
   StatusResult,
 } from "@nomicfoundation/ignition-core";
-import chalk from "chalk";
 import { readdirSync } from "fs-extra";
 import { extendConfig, extendEnvironment, scope } from "hardhat/config";
 import { NomicLabsHardhatPluginError, lazyObject } from "hardhat/plugins";
 import path from "path";
 
 import "./type-extensions";
+import { calculateDeploymentStatusDisplay } from "./ui/helpers/calculate-deployment-status-display";
 
 /* ignition config defaults */
 const IGNITION_DIR = "ignition";
@@ -253,86 +253,7 @@ ignitionScope
       throw e;
     }
 
-    if (statusResult.started.length > 0) {
-      console.log("");
-      console.log(
-        chalk.bold(
-          `⛔ Deployment ${deploymentId} has not fully completed, there are futures have started but not finished`
-        )
-      );
-      console.log("");
-
-      for (const futureId of Object.values(statusResult.started)) {
-        console.log(` - ${futureId}`);
-      }
-
-      console.log("");
-      return;
-    }
-
-    if (
-      statusResult.timedOut.length > 0 ||
-      statusResult.failed.length > 0 ||
-      statusResult.held.length > 0
-    ) {
-      console.log("");
-      console.log(chalk.bold(toErrorResultHeading(deploymentId, statusResult)));
-      console.log("");
-
-      if (statusResult.timedOut.length > 0) {
-        console.log(chalk.yellow("There are timed-out futures:"));
-        console.log("");
-
-        for (const { futureId } of Object.values(statusResult.timedOut)) {
-          console.log(` - ${futureId}`);
-        }
-
-        console.log("");
-      }
-
-      if (statusResult.failed.length > 0) {
-        console.log(chalk.red("There are failed futures"));
-        console.log("");
-
-        for (const { futureId, networkInteractionId, error } of Object.values(
-          statusResult.failed
-        )) {
-          console.log(` - ${futureId}/${networkInteractionId}: ${error}`);
-        }
-
-        console.log("");
-      }
-
-      if (statusResult.held.length > 0) {
-        console.log(chalk.yellow("There are futures that the strategy held:"));
-        console.log("");
-
-        for (const { futureId, heldId, reason } of Object.values(
-          statusResult.held
-        )) {
-          console.log(` - ${futureId}/${heldId}: ${reason}`);
-        }
-
-        console.log("");
-      }
-
-      return;
-    }
-
-    console.log("");
-    console.log(chalk.bold(`🚀 Deployment ${deploymentId} Complete `));
-    console.log("");
-
-    if (Object.values(statusResult.contracts).length === 0) {
-      console.log(chalk.italic("No contracts were deployed"));
-    } else {
-      console.log("Deployed Addresses");
-      console.log("");
-
-      for (const contract of Object.values(statusResult.contracts)) {
-        console.log(`${contract.id} - ${contract.address}`);
-      }
-    }
+    console.log(calculateDeploymentStatusDisplay(deploymentId, statusResult));
   });
 
 ignitionScope
@@ -412,35 +333,3 @@ function resolveParametersString(paramString: string): DeploymentParameters {
     process.exit(0);
   }
 }
-
-function toErrorResultHeading(
-  deploymentId: string,
-  statusResult: StatusResult
-): string {
-  const didTimeout = statusResult.timedOut.length > 0;
-  const didFailed = statusResult.failed.length > 0;
-  const didHeld = statusResult.held.length > 0;
-
-  let reasons = "";
-  if (didTimeout && didFailed && didHeld) {
-    reasons = "timeouts, failures and holds";
-  } else if (didTimeout && didFailed) {
-    reasons = "timeouts and failures";
-  } else if (didFailed && didHeld) {
-    reasons = "failures and holds";
-  } else if (didTimeout && didHeld) {
-    reasons = "timeouts and holds";
-  } else if (didTimeout) {
-    reasons = "timeouts";
-  } else if (didFailed) {
-    reasons = "failures";
-  } else if (didHeld) {
-    reasons = "holds";
-  }
-
-  return chalk.bold(
-    `⛔ Deployment ${deploymentId} did ${chalk.bold(
-      "not"
-    )} complete as there were ${reasons}`
-  );
-}
diff --git a/packages/hardhat-plugin/src/ui/helpers/calculate-deployment-complete-display.ts b/packages/hardhat-plugin/src/ui/helpers/calculate-deployment-complete-display.ts
index ada65264..31cc9905 100644
--- a/packages/hardhat-plugin/src/ui/helpers/calculate-deployment-complete-display.ts
+++ b/packages/hardhat-plugin/src/ui/helpers/calculate-deployment-complete-display.ts
@@ -65,11 +65,13 @@ function _displayValidationErrors(
   let text = `
 [ ${moduleName} ] validation failed ⛔
 
+The module contains futures that would fail to execute:
+
 `;
 
   text += Object.entries(result.errors)
     .map(([futureId, errors]) => {
-      let futureSection = `${futureId} errors:\n`;
+      let futureSection = `${futureId}:\n`;
 
       futureSection += errors.map((error) => ` - ${error}`).join("\n");
 
@@ -77,6 +79,8 @@ function _displayValidationErrors(
     })
     .join("\n\n");
 
+  text += `\n\nUpdate the invalid futures and rerun the deployment.`;
+
   return text;
 }
 
@@ -87,11 +91,13 @@ function _displayReconciliationErrors(
   let text = `
 [ ${moduleName} ] reconciliation failed ⛔
 
+The module contains changes to executed futures:
+
 `;
 
   text += Object.entries(result.errors)
     .map(([futureId, errors]) => {
-      let errorSection = `${futureId} errors:\n`;
+      let errorSection = `${futureId}:\n`;
 
       errorSection += errors.map((error) => ` - ${error}`).join("\n");
 
@@ -99,6 +105,8 @@ function _displayReconciliationErrors(
     })
     .join("\n\n");
 
+  text += `\n\nConsider modifying your module to remove the inconsistencies with deployed futures.`;
+
   return text;
 }
 
@@ -114,9 +122,7 @@ function _displayPreviousRunErrors(
     .map((futureId) => ` - ${futureId}`)
     .join("\n");
 
-  text += `\n\nUse the ${chalk.italic(
-    "wipe"
-  )} task to reset them.\nCheck out the docs to learn more: <LINK>`;
+  text += `\n\nUse the ${chalk.italic("wipe")} task to reset them.`;
 
   return text;
 }
@@ -130,20 +136,19 @@ function _displayExecutionErrors(
   let text = `\n[ ${moduleName} ] failed ⛔\n\n`;
 
   if (result.timedOut.length > 0) {
-    let timedOutSection = `Transaction remains unconfirmed after fee bump:\n`;
+    let timedOutSection = `Transactions remain unconfirmed after fee bump:\n`;
 
     timedOutSection += Object.values(result.timedOut)
       .map(({ futureId }) => ` - ${futureId}`)
       .join("\n");
 
-    timedOutSection +=
-      "\n\nConsider increasing the fee in your config.\nCheck out the docs to learn more: <LINK>";
+    timedOutSection += "\n\nConsider increasing the fee in your config.";
 
     sections.push(timedOutSection);
   }
 
   if (result.failed.length > 0) {
-    let failedSection = `Failures:\n`;
+    let failedSection = `Futures failed during execution:\n`;
 
     failedSection += Object.values(result.failed)
       .map(
@@ -152,6 +157,9 @@ function _displayExecutionErrors(
       )
       .join("\n");
 
+    failedSection +=
+      "\n\nConsider addressing the cause of the errors and rerunning the deployment.";
+
     sections.push(failedSection);
   }
 
diff --git a/packages/hardhat-plugin/src/ui/helpers/calculate-deployment-status-display.ts b/packages/hardhat-plugin/src/ui/helpers/calculate-deployment-status-display.ts
new file mode 100644
index 00000000..843f4e97
--- /dev/null
+++ b/packages/hardhat-plugin/src/ui/helpers/calculate-deployment-status-display.ts
@@ -0,0 +1,100 @@
+import { StatusResult } from "@nomicfoundation/ignition-core";
+import chalk from "chalk";
+
+export function calculateDeploymentStatusDisplay(
+  deploymentId: string,
+  statusResult: StatusResult
+): string {
+  if (statusResult.started.length > 0) {
+    return _calculateStartedButUnfinished(deploymentId, statusResult);
+  }
+
+  if (
+    statusResult.timedOut.length > 0 ||
+    statusResult.failed.length > 0 ||
+    statusResult.held.length > 0
+  ) {
+    return _calculateFailed(deploymentId, statusResult);
+  }
+
+  return _calculateSuccess(deploymentId, statusResult);
+}
+
+function _calculateSuccess(deploymentId: string, statusResult: StatusResult) {
+  let successText = `\n[ ${deploymentId} ] successfully deployed 🚀\n\n`;
+
+  if (Object.values(statusResult.contracts).length === 0) {
+    successText += chalk.italic("No contracts were deployed");
+  } else {
+    successText += `${chalk.bold("Deployed Addresses")}\n\n`;
+
+    successText += Object.values(statusResult.contracts)
+      .map((contract) => `${contract.id} - ${contract.address}`)
+      .join("\n");
+  }
+
+  return successText;
+}
+
+function _calculateStartedButUnfinished(
+  deploymentId: string,
+  statusResult: StatusResult
+) {
+  let startedText = `\n[ ${deploymentId} ] has futures that have started but not finished ⛔\n\n`;
+
+  startedText += Object.values(statusResult.started)
+    .map((futureId) => ` - ${futureId}`)
+    .join("\n");
+
+  return startedText;
+}
+
+function _calculateFailed(deploymentId: string, statusResult: StatusResult) {
+  let failedExecutionText = `\n[ ${deploymentId} ] failed ⛔\n`;
+
+  const sections: string[] = [];
+
+  if (statusResult.timedOut.length > 0) {
+    let timedOutSection = `\nTransactions remain unconfirmed after fee bump:\n`;
+
+    timedOutSection += Object.values(statusResult.timedOut)
+      .map(({ futureId }) => ` - ${futureId}`)
+      .join("\n");
+
+    timedOutSection += "\n\nConsider increasing the fee in your config.";
+
+    sections.push(timedOutSection);
+  }
+
+  if (statusResult.failed.length > 0) {
+    let failedSection = `\nFutures failed during execution:\n`;
+
+    failedSection += Object.values(statusResult.failed)
+      .map(
+        ({ futureId, networkInteractionId, error }) =>
+          ` - ${futureId}/${networkInteractionId}: ${error}`
+      )
+      .join("\n");
+
+    failedSection +=
+      "\n\nConsider addressing the cause of the errors and rerunning the deployment.";
+
+    sections.push(failedSection);
+  }
+
+  if (statusResult.held.length > 0) {
+    let heldSection = `\nFutures where held by the strategy:\n`;
+
+    heldSection += Object.values(statusResult.held)
+      .map(
+        ({ futureId, heldId, reason }) => ` - ${futureId}/${heldId}: ${reason}`
+      )
+      .join("\n");
+
+    sections.push(heldSection);
+  }
+
+  failedExecutionText += sections.join("\n");
+
+  return failedExecutionText;
+}


Test patch:
diff --git a/packages/hardhat-plugin/test/ui/helpers/calculate-deployment-complete-display.ts b/packages/hardhat-plugin/test/ui/helpers/calculate-deployment-complete-display.ts
index 210c02f2..bff11fad 100644
--- a/packages/hardhat-plugin/test/ui/helpers/calculate-deployment-complete-display.ts
+++ b/packages/hardhat-plugin/test/ui/helpers/calculate-deployment-complete-display.ts
@@ -84,12 +84,16 @@ describe("ui - calculate deployment complete display", () => {
 
         [ MyModule ] validation failed ⛔
 
-        MyModule:MyContract errors:
+        The module contains futures that would fail to execute:
+
+        MyModule:MyContract:
          - The number of params does not match the constructor
          - The name of the contract is invalid
 
-        MyModule:AnotherContract errors:
-         - No library provided`);
+        MyModule:AnotherContract:
+         - No library provided
+
+        Update the invalid futures and rerun the deployment.`);
 
       const result: ValidationErrorDeploymentResult = {
         type: DeploymentResultType.VALIDATION_ERROR,
@@ -121,12 +125,16 @@ describe("ui - calculate deployment complete display", () => {
 
         [ MyModule ] reconciliation failed ⛔
 
-        MyModule:MyContract errors:
+        The module contains changes to executed futures:
+
+        MyModule:MyContract:
          - The params don't match
          - The value doesn't match
 
-        MyModule:AnotherContract errors:
-         - The artifact bytecode has changed`);
+        MyModule:AnotherContract:
+         - The artifact bytecode has changed
+
+        Consider modifying your module to remove the inconsistencies with deployed futures.`);
 
       const result: ReconciliationErrorDeploymentResult = {
         type: DeploymentResultType.RECONCILIATION_ERROR,
@@ -162,8 +170,7 @@ describe("ui - calculate deployment complete display", () => {
          - MyModule:MyContract
          - MyModule:AnotherContract
 
-        Use the ${chalk.italic("wipe")} task to reset them.
-        Check out the docs to learn more: <LINK>`);
+        Use the ${chalk.italic("wipe")} task to reset them.`);
 
       const result: PreviousRunErrorDeploymentResult = {
         type: DeploymentResultType.PREVIOUS_RUN_ERROR,
@@ -192,17 +199,18 @@ describe("ui - calculate deployment complete display", () => {
 
         [ MyModule ] failed ⛔
 
-        Transaction remains unconfirmed after fee bump:
+        Transactions remain unconfirmed after fee bump:
          - MyModule:MyContract1
          - MyModule:AnotherContract1
 
         Consider increasing the fee in your config.
-        Check out the docs to learn more: <LINK>
 
-        Failures:
+        Futures failed during execution:
          - MyModule:MyContract3/1: Reverted with reason x
          - MyModule:AnotherContract3/3: Reverted with reason y
 
+        Consider addressing the cause of the errors and rerunning the deployment.
+
         Held:
          - MyModule:MyContract2/1: Vote is not complete
          - MyModule:AnotherContract2/3: Server timed out`);
diff --git a/packages/hardhat-plugin/test/ui/helpers/calculate-deployment-status-display.ts b/packages/hardhat-plugin/test/ui/helpers/calculate-deployment-status-display.ts
new file mode 100644
index 00000000..c6d750e1
--- /dev/null
+++ b/packages/hardhat-plugin/test/ui/helpers/calculate-deployment-status-display.ts
@@ -0,0 +1,185 @@
+import { StatusResult } from "@nomicfoundation/ignition-core";
+import { assert } from "chai";
+import chalk from "chalk";
+
+import { calculateDeploymentStatusDisplay } from "../../../src/ui/helpers/calculate-deployment-status-display";
+
+describe("ui - calculate deployment status display", () => {
+  const exampleAddress = "0x1F98431c8aD98523631AE4a59f267346ea31F984";
+  const differentAddress = "0x0011223344556677889900112233445566778899";
+
+  const exampleStatusResult = {
+    started: [],
+    timedOut: [],
+    held: [],
+    failed: [],
+    successful: [],
+    contracts: {},
+  };
+
+  describe("successful deployment", () => {
+    it("should render a sucessful deployment", () => {
+      const expectedText = testFormat(`
+
+        [ deployment-01 ] successfully deployed 🚀
+
+        ${chalk.bold("Deployed Addresses")}
+
+        MyModule#Token - 0x1F98431c8aD98523631AE4a59f267346ea31F984
+        MyModule#AnotherToken - 0x0011223344556677889900112233445566778899`);
+
+      const statusResult: StatusResult = {
+        ...exampleStatusResult,
+        successful: ["MyModule#Token", "MyModule#AnotherToken"],
+        contracts: {
+          "MyModule#Token": {
+            id: "MyModule#Token",
+            address: exampleAddress,
+            contractName: "Token",
+          },
+          "MyModule#AnotherToken": {
+            id: "MyModule#AnotherToken",
+            address: differentAddress,
+            contractName: "AnotherToken",
+          },
+        },
+      };
+
+      const actualText = calculateDeploymentStatusDisplay(
+        "deployment-01",
+        statusResult
+      );
+
+      assert.equal(actualText, expectedText);
+    });
+
+    it("should render a sucessful deployment with no deploys", () => {
+      const expectedText = testFormat(`
+
+        [ deployment-01 ] successfully deployed 🚀
+
+        ${chalk.italic("No contracts were deployed")}`);
+
+      const statusResult: StatusResult = {
+        ...exampleStatusResult,
+        successful: ["MyModule#a_call"],
+        contracts: {},
+      };
+
+      const actualText = calculateDeploymentStatusDisplay(
+        "deployment-01",
+        statusResult
+      );
+
+      assert.equal(actualText, expectedText);
+    });
+  });
+
+  describe("failed deployment", () => {
+    it("should render an execution failure with multiple of each problem type", () => {
+      const expectedText = testFormat(`
+
+        [ deployment-01 ] failed ⛔
+
+        Transactions remain unconfirmed after fee bump:
+         - MyModule:MyContract1
+         - MyModule:AnotherContract1
+
+        Consider increasing the fee in your config.
+
+        Futures failed during execution:
+         - MyModule:MyContract3/1: Reverted with reason x
+         - MyModule:AnotherContract3/3: Reverted with reason y
+
+        Consider addressing the cause of the errors and rerunning the deployment.
+
+        Futures where held by the strategy:
+         - MyModule:MyContract2/1: Vote is not complete
+         - MyModule:AnotherContract2/3: Server timed out`);
+
+      const statusResult: StatusResult = {
+        started: [],
+        timedOut: [
+          { futureId: "MyModule:MyContract1", networkInteractionId: 1 },
+          { futureId: "MyModule:AnotherContract1", networkInteractionId: 3 },
+        ],
+        held: [
+          {
+            futureId: "MyModule:MyContract2",
+            heldId: 1,
+            reason: "Vote is not complete",
+          },
+          {
+            futureId: "MyModule:AnotherContract2",
+            heldId: 3,
+            reason: "Server timed out",
+          },
+        ],
+        failed: [
+          {
+            futureId: "MyModule:MyContract3",
+            networkInteractionId: 1,
+            error: "Reverted with reason x",
+          },
+          {
+            futureId: "MyModule:AnotherContract3",
+            networkInteractionId: 3,
+            error: "Reverted with reason y",
+          },
+        ],
+        successful: ["MyModule#Token", "MyModule#AnotherToken"],
+        contracts: {
+          "MyModule#Token": {
+            id: "MyModule#Token",
+            address: exampleAddress,
+            contractName: "Token",
+          },
+          "MyModule#AnotherToken": {
+            id: "MyModule#AnotherToken",
+            address: differentAddress,
+            contractName: "AnotherToken",
+          },
+        },
+      };
+
+      const actualText = calculateDeploymentStatusDisplay(
+        "deployment-01",
+        statusResult
+      );
+
+      assert.equal(actualText, expectedText);
+    });
+  });
+
+  describe("deployment with started but unfinished futures (e.g. simulation errors)", () => {
+    it("should render a sucessful deployment", () => {
+      const expectedText = testFormat(`
+
+        [ deployment-01 ] has futures that have started but not finished ⛔
+
+         - MyModule#Token
+         - MyModule#AnotherToken`);
+
+      const statusResult: StatusResult = {
+        ...exampleStatusResult,
+        started: ["MyModule#Token", "MyModule#AnotherToken"],
+      };
+
+      const actualText = calculateDeploymentStatusDisplay(
+        "deployment-01",
+        statusResult
+      );
+
+      assert.equal(actualText, expectedText);
+    });
+  });
+});
+
+function testFormat(expected: string): string {
+  return expected
+    .toString()
+    .substring(1) // Remove the first newline
+    .split("\n")
+    .map((line) => line.substring(8)) // remove prefix whitespace
+    .join("\n");
+}