Repository: NomicFoundation/hardhat-ignition (ts)

Issue:
Not all accounts' nonces are sync before execution
Before starting to execute a module we run [`getNonceSyncMessages`](https://github.com/NomicFoundation/ignition/blob/316ec19b1df431a665e523cefd88ef4db7f0bf31/packages/core/src/new-api/internal/new-execution/nonce-management.ts#L42C23-L42C43) to sync the nonces from the previous with the actual ones, and to prevent us from execution if there are user transactions without enough confirmations.

We only check the accounts that we have already used, but we have to check every transaction that's either a `from` in a future, or the default sender.

Pull request:
fix/additional-nonces-checks (PR 7)
Include the future senders from the module when syncing nonces for checks. 

The senders for all futures without exStates that will be used if the module executes, are now added to the syncing taking into account usage of default sender when `from` is undefined.

Resolves #411 

Patch:
diff --git a/packages/core/src/internal/execution/execution-engine.ts b/packages/core/src/internal/execution/execution-engine.ts
index 9023f199..a14d5f36 100644
--- a/packages/core/src/internal/execution/execution-engine.ts
+++ b/packages/core/src/internal/execution/execution-engine.ts
@@ -20,11 +20,9 @@ import { isBatchFinished } from "../views/is-batch-finished";
 import { applyNewMessage } from "./deployment-state-helpers";
 import { FutureProcessor } from "./future-processor/future-processor";
 import { Block, JsonRpcClient } from "./jsonrpc-client";
-import {
-  JsonRpcNonceManager,
-  getMaxNonceUsedBySender,
-  getNonceSyncMessages,
-} from "./nonce-management";
+import { getMaxNonceUsedBySender } from "./nonce-management/get-max-nonce-used-by-sender";
+import { getNonceSyncMessages } from "./nonce-management/get-nonce-sync-messages";
+import { JsonRpcNonceManager } from "./nonce-management/json-rpc-nonce-manager";
 import { TransactionTrackingTimer } from "./transaction-tracking-timer";
 import { DeploymentState } from "./types/deployment-state";
 import { ExecutionStrategy } from "./types/execution-strategy";
@@ -70,9 +68,15 @@ export class ExecutionEngine {
     deploymentParameters: DeploymentParameters,
     defaultSender: string
   ): Promise<DeploymentState> {
-    deploymentState = await this._syncNonces(deploymentState);
+    deploymentState = await this._syncNonces(
+      deploymentState,
+      module,
+      accounts,
+      defaultSender
+    );
 
     const transactionTrackingTimer = new TransactionTrackingTimer();
+
     const nonceManager = new JsonRpcNonceManager(
       this._jsonRpcClient,
       getMaxNonceUsedBySender(deploymentState)
@@ -195,14 +199,25 @@ export class ExecutionEngine {
    * This method processes dropped and replaced transactions.
    *
    * @param deploymentState The existing deployment state.
+   * @param ignitionModule The module that will be executed.
    * @returns The updated deployment state.
    */
   private async _syncNonces(
-    deploymentState: DeploymentState
+    deploymentState: DeploymentState,
+    ignitionModule: IgnitionModule<
+      string,
+      string,
+      IgnitionModuleResult<string>
+    >,
+    accounts: string[],
+    defaultSender: string
   ): Promise<DeploymentState> {
     const nonceSyncMessages = await getNonceSyncMessages(
       this._jsonRpcClient,
       deploymentState,
+      ignitionModule,
+      accounts,
+      defaultSender,
       this._requiredConfirmations
     );
 
diff --git a/packages/core/src/internal/execution/future-processor/future-processor.ts b/packages/core/src/internal/execution/future-processor/future-processor.ts
index d4aa8b78..40cebccb 100644
--- a/packages/core/src/internal/execution/future-processor/future-processor.ts
+++ b/packages/core/src/internal/execution/future-processor/future-processor.ts
@@ -6,7 +6,7 @@ import { assertIgnitionInvariant } from "../../utils/assertions";
 import { isExecutionStateComplete } from "../../views/is-execution-state-complete";
 import { applyNewMessage } from "../deployment-state-helpers";
 import { JsonRpcClient } from "../jsonrpc-client";
-import { NonceManager } from "../nonce-management";
+import { NonceManager } from "../nonce-management/json-rpc-nonce-manager";
 import { TransactionTrackingTimer } from "../transaction-tracking-timer";
 import { DeploymentState } from "../types/deployment-state";
 import { ExecutionResultType } from "../types/execution-result";
diff --git a/packages/core/src/internal/execution/future-processor/handlers/send-transaction.ts b/packages/core/src/internal/execution/future-processor/handlers/send-transaction.ts
index fd8c9b47..c3f7ceb0 100644
--- a/packages/core/src/internal/execution/future-processor/handlers/send-transaction.ts
+++ b/packages/core/src/internal/execution/future-processor/handlers/send-transaction.ts
@@ -1,6 +1,6 @@
 import { assertIgnitionInvariant } from "../../../utils/assertions";
 import { JsonRpcClient } from "../../jsonrpc-client";
-import { NonceManager } from "../../nonce-management";
+import { NonceManager } from "../../nonce-management/json-rpc-nonce-manager";
 import { TransactionTrackingTimer } from "../../transaction-tracking-timer";
 import { ExecutionResultType } from "../../types/execution-result";
 import {
diff --git a/packages/core/src/internal/execution/nonce-management/get-max-nonce-used-by-sender.ts b/packages/core/src/internal/execution/nonce-management/get-max-nonce-used-by-sender.ts
new file mode 100644
index 00000000..a02cbbb2
--- /dev/null
+++ b/packages/core/src/internal/execution/nonce-management/get-max-nonce-used-by-sender.ts
@@ -0,0 +1,28 @@
+import { getPendingNonceAndSender } from "../../views/execution-state/get-pending-nonce-and-sender";
+import { DeploymentState } from "../types/deployment-state";
+
+export function getMaxNonceUsedBySender(deploymentState: DeploymentState): {
+  [sender: string]: number;
+} {
+  const nonces: {
+    [sender: string]: number;
+  } = {};
+
+  for (const executionState of Object.values(deploymentState.executionStates)) {
+    const pendingNonceAndSender = getPendingNonceAndSender(executionState);
+
+    if (pendingNonceAndSender === undefined) {
+      continue;
+    }
+
+    const { sender, nonce } = pendingNonceAndSender;
+
+    if (nonces[sender] === undefined) {
+      nonces[sender] = nonce;
+    } else {
+      nonces[sender] = Math.max(nonces[sender], nonce);
+    }
+  }
+
+  return nonces;
+}
diff --git a/packages/core/src/internal/execution/nonce-management.ts b/packages/core/src/internal/execution/nonce-management/get-nonce-sync-messages.ts
similarity index 70%
rename from packages/core/src/internal/execution/nonce-management.ts
rename to packages/core/src/internal/execution/nonce-management/get-nonce-sync-messages.ts
index 337fe8bb..1ca3fdce 100644
--- a/packages/core/src/internal/execution/nonce-management.ts
+++ b/packages/core/src/internal/execution/nonce-management/get-nonce-sync-messages.ts
@@ -1,16 +1,28 @@
-import { IgnitionError } from "../../errors";
-import { ERRORS } from "../../errors-list";
-import { getPendingNonceAndSender } from "../views/execution-state/get-pending-nonce-and-sender";
-import { getPendingOnchainInteraction } from "../views/execution-state/get-pending-onchain-interaction";
-
-import { JsonRpcClient } from "./jsonrpc-client";
-import { DeploymentState } from "./types/deployment-state";
-import { ExecutionSateType } from "./types/execution-state";
+import uniq from "lodash/uniq";
+
+import { IgnitionError } from "../../../errors";
+import { ERRORS } from "../../../errors-list";
+import {
+  isArtifactContractAtFuture,
+  isNamedContractAtFuture,
+  isReadEventArgumentFuture,
+} from "../../../type-guards";
+import {
+  Future,
+  IgnitionModule,
+  IgnitionModuleResult,
+} from "../../../types/module";
+import { getFuturesFromModule } from "../../utils/get-futures-from-module";
+import { getPendingOnchainInteraction } from "../../views/execution-state/get-pending-onchain-interaction";
+import { resolveFutureFrom } from "../future-processor/helpers/future-resolvers";
+import { JsonRpcClient } from "../jsonrpc-client";
+import { DeploymentState } from "../types/deployment-state";
+import { ExecutionSateType, ExecutionStatus } from "../types/execution-state";
 import {
   JournalMessageType,
   OnchainInteractionDroppedMessage,
   OnchainInteractionReplacedByUserMessage,
-} from "./types/messages";
+} from "../types/messages";
 
 /**
  * This function is meant to be used to sync the local state's nonces
@@ -43,6 +55,9 @@ import {
 export async function getNonceSyncMessages(
   jsonRpcClient: JsonRpcClient,
   deploymentState: DeploymentState,
+  ignitionModule: IgnitionModule<string, string, IgnitionModuleResult<string>>,
+  accounts: string[],
+  defaultSender: string,
   requiredConfirmations: number
 ): Promise<
   Array<
@@ -54,7 +69,12 @@ export async function getNonceSyncMessages(
   > = [];
 
   const pendingTransactionsPerSender =
-    createMapFromSenderToNonceAndTransactions(deploymentState);
+    createMapFromSenderToNonceAndTransactions(
+      deploymentState,
+      ignitionModule,
+      accounts,
+      defaultSender
+    );
 
   const block = await jsonRpcClient.getLatestBlock();
   const confirmedBlockNumber = block.number - requiredConfirmations + 1;
@@ -145,84 +165,11 @@ export async function getNonceSyncMessages(
   return messages;
 }
 
-/**
- * This interface is meant to be used to fetch new nonces for transactions.
- */
-export interface NonceManager {
-  /**
-   * Returns the next nonce for a given sender, throwing if its not the one
-   * expected by the network.
-   *
-   * If a nonce is returned by this method it must be immediately used to
-   * send a transaction. If it can't be used, Ignition's execution must be
-   * interrupted.
-   */
-  getNextNonce(sender: string): Promise<number>;
-}
-
-/**
- * An implementation of NonceManager that validates the nonces using
- * the _maxUsedNonce params and a JsonRpcClient.
- */
-export class JsonRpcNonceManager implements NonceManager {
-  constructor(
-    private readonly _jsonRpcClient: JsonRpcClient,
-    private readonly _maxUsedNonce: { [sender: string]: number }
-  ) {}
-
-  public async getNextNonce(sender: string): Promise<number> {
-    const pendingCount = await this._jsonRpcClient.getTransactionCount(
-      sender,
-      "pending"
-    );
-
-    const expectedNonce =
-      this._maxUsedNonce[sender] !== undefined
-        ? this._maxUsedNonce[sender] + 1
-        : pendingCount;
-
-    if (expectedNonce !== pendingCount) {
-      throw new IgnitionError(ERRORS.EXECUTION.INVALID_NONCE, {
-        sender,
-        expectedNonce,
-        pendingCount,
-      });
-    }
-
-    // The nonce hasn't been used yet, but we update as it will be immediately used.
-    this._maxUsedNonce[expectedNonce] = expectedNonce;
-
-    return expectedNonce;
-  }
-}
-
-export function getMaxNonceUsedBySender(deploymentState: DeploymentState): {
-  [sender: string]: number;
-} {
-  const nonces: {
-    [sender: string]: number;
-  } = {};
-  for (const executionState of Object.values(deploymentState.executionStates)) {
-    const pendingNonceAndSender = getPendingNonceAndSender(executionState);
-
-    if (pendingNonceAndSender === undefined) {
-      continue;
-    }
-
-    const { sender, nonce } = pendingNonceAndSender;
-
-    if (nonces[sender] === undefined) {
-      nonces[sender] = nonce;
-    } else {
-      nonces[sender] = Math.max(nonces[sender], nonce);
-    }
-  }
-
-  return nonces;
-}
-
 function createMapFromSenderToNonceAndTransactions(
-  deploymentState: DeploymentState
+  deploymentState: DeploymentState,
+  ignitionModule: IgnitionModule<string, string, IgnitionModuleResult<string>>,
+  accounts: string[],
+  defaultSender: string
 ): {
   [sender: string]: Array<{
     nonce: number;
@@ -249,6 +196,10 @@ function createMapFromSenderToNonceAndTransactions(
       continue;
     }
 
+    if (executionState.status === ExecutionStatus.SUCCESS) {
+      continue;
+    }
+
     const onchainInteraction = getPendingOnchainInteraction(executionState);
 
     if (onchainInteraction === undefined) {
@@ -271,6 +222,18 @@ function createMapFromSenderToNonceAndTransactions(
     });
   }
 
+  const exStateIds = Object.keys(deploymentState.executionStates);
+  const futureSenders = _resolveFutureSenders(
+    ignitionModule,
+    accounts,
+    defaultSender,
+    exStateIds
+  );
+
+  for (const futureSender of futureSenders) {
+    pendingTransactionsPerAccount[futureSender] = [];
+  }
+
   for (const pendingTransactions of Object.values(
     pendingTransactionsPerAccount
   )) {
@@ -279,3 +242,43 @@ function createMapFromSenderToNonceAndTransactions(
 
   return pendingTransactionsPerAccount;
 }
+
+/**
+ * Scan the futures for upcoming account usage, add them to the list,
+ * including the default sender if there are any undefined froms
+ */
+function _resolveFutureSenders(
+  ignitionModule: IgnitionModule<string, string, IgnitionModuleResult<string>>,
+  accounts: string[],
+  defaultSender: string,
+  exStateIds: string[]
+): string[] {
+  const futures = getFuturesFromModule(ignitionModule);
+
+  const senders: string[] = futures
+    .filter((f) => !exStateIds.includes(f.id))
+    .map((f) => _pickFrom(f, accounts, defaultSender))
+    .filter((x): x is string => x !== null);
+
+  return uniq(senders);
+}
+
+function _pickFrom(
+  future: Future,
+  accounts: string[],
+  defaultSender: string
+): string | null {
+  if (isNamedContractAtFuture(future)) {
+    return null;
+  }
+
+  if (isArtifactContractAtFuture(future)) {
+    return null;
+  }
+
+  if (isReadEventArgumentFuture(future)) {
+    return null;
+  }
+
+  return resolveFutureFrom(future.from, accounts, defaultSender);
+}
diff --git a/packages/core/src/internal/execution/nonce-management/json-rpc-nonce-manager.ts b/packages/core/src/internal/execution/nonce-management/json-rpc-nonce-manager.ts
new file mode 100644
index 00000000..7db279c2
--- /dev/null
+++ b/packages/core/src/internal/execution/nonce-management/json-rpc-nonce-manager.ts
@@ -0,0 +1,55 @@
+import { IgnitionError } from "../../../errors";
+import { ERRORS } from "../../../errors-list";
+import { JsonRpcClient } from "../jsonrpc-client";
+
+/**
+ * This interface is meant to be used to fetch new nonces for transactions.
+ */
+
+export interface NonceManager {
+  /**
+   * Returns the next nonce for a given sender, throwing if its not the one
+   * expected by the network.
+   *
+   * If a nonce is returned by this method it must be immediately used to
+   * send a transaction. If it can't be used, Ignition's execution must be
+   * interrupted.
+   */
+  getNextNonce(sender: string): Promise<number>;
+}
+
+/**
+ * An implementation of NonceManager that validates the nonces using
+ * the _maxUsedNonce params and a JsonRpcClient.
+ */
+export class JsonRpcNonceManager implements NonceManager {
+  constructor(
+    private readonly _jsonRpcClient: JsonRpcClient,
+    private readonly _maxUsedNonce: { [sender: string]: number }
+  ) {}
+
+  public async getNextNonce(sender: string): Promise<number> {
+    const pendingCount = await this._jsonRpcClient.getTransactionCount(
+      sender,
+      "pending"
+    );
+
+    const expectedNonce =
+      this._maxUsedNonce[sender] !== undefined
+        ? this._maxUsedNonce[sender] + 1
+        : pendingCount;
+
+    if (expectedNonce !== pendingCount) {
+      throw new IgnitionError(ERRORS.EXECUTION.INVALID_NONCE, {
+        sender,
+        expectedNonce,
+        pendingCount,
+      });
+    }
+
+    // The nonce hasn't been used yet, but we update as it will be immediately used.
+    this._maxUsedNonce[expectedNonce] = expectedNonce;
+
+    return expectedNonce;
+  }
+}
diff --git a/packages/core/src/type-guards.ts b/packages/core/src/type-guards.ts
index eec1dc58..e55970fd 100644
--- a/packages/core/src/type-guards.ts
+++ b/packages/core/src/type-guards.ts
@@ -2,23 +2,23 @@ import { Artifact } from "./types/artifact";
 import {
   AccountRuntimeValue,
   AddressResolvableFuture,
+  CallableContractFuture,
   ContractAtFuture,
   ContractDeploymentFuture,
-  LibraryDeploymentFuture,
-  CallableContractFuture,
   ContractFuture,
   DeploymentFuture,
   FunctionCallFuture,
   Future,
   FutureType,
+  LibraryDeploymentFuture,
   ModuleParameterRuntimeValue,
   NamedArtifactContractAtFuture,
   NamedArtifactContractDeploymentFuture,
   NamedArtifactLibraryDeploymentFuture,
-  StaticCallFuture,
   ReadEventArgumentFuture,
   RuntimeValue,
   RuntimeValueType,
+  StaticCallFuture,
 } from "./types/module";
 
 function isValidEnumValue(
@@ -170,7 +170,7 @@ export function isNamedStaticCallFuture(
  */
 export function isReadEventArgumentFuture(
   future: Future
-): future is StaticCallFuture<string, string> {
+): future is ReadEventArgumentFuture {
   return future.type === FutureType.READ_EVENT_ARGUMENT;
 }
 


Test patch:
diff --git a/packages/core/test/execution/future-processor/utils.ts b/packages/core/test/execution/future-processor/utils.ts
index bf536d39..c2fc9ecd 100644
--- a/packages/core/test/execution/future-processor/utils.ts
+++ b/packages/core/test/execution/future-processor/utils.ts
@@ -7,7 +7,7 @@ import {
   JsonRpcClient,
   TransactionParams,
 } from "../../../src/internal/execution/jsonrpc-client";
-import { NonceManager } from "../../../src/internal/execution/nonce-management";
+import { NonceManager } from "../../../src/internal/execution/nonce-management/json-rpc-nonce-manager";
 import { TransactionTrackingTimer } from "../../../src/internal/execution/transaction-tracking-timer";
 import {
   NetworkFees,
diff --git a/packages/core/test/execution/nonce-management/get-nonce-sync-messages.ts b/packages/core/test/execution/nonce-management/get-nonce-sync-messages.ts
new file mode 100644
index 00000000..6979d8ac
--- /dev/null
+++ b/packages/core/test/execution/nonce-management/get-nonce-sync-messages.ts
@@ -0,0 +1,407 @@
+import { assert } from "chai";
+
+import {
+  FutureType,
+  IgnitionModule,
+  IgnitionModuleResult,
+  buildModule,
+} from "../../../src";
+import { JsonRpcClient } from "../../../src/internal/execution/jsonrpc-client";
+import { getNonceSyncMessages } from "../../../src/internal/execution/nonce-management/get-nonce-sync-messages";
+import { deploymentStateReducer } from "../../../src/internal/execution/reducers/deployment-state-reducer";
+import { DeploymentState } from "../../../src/internal/execution/types/deployment-state";
+import {
+  DeploymentExecutionState,
+  ExecutionSateType,
+  ExecutionStatus,
+} from "../../../src/internal/execution/types/execution-state";
+import {
+  JournalMessageType,
+  OnchainInteractionDroppedMessage,
+  OnchainInteractionReplacedByUserMessage,
+} from "../../../src/internal/execution/types/messages";
+import { NetworkInteractionType } from "../../../src/internal/execution/types/network-interaction";
+import { exampleAccounts } from "../../helpers";
+
+const requiredConfirmations = 5;
+const latestBlock = 10;
+
+describe("execution - getNonceSyncMessages", () => {
+  let exampleModule: IgnitionModule<
+    string,
+    string,
+    IgnitionModuleResult<string>
+  >;
+
+  const exampleDeploymentState: DeploymentExecutionState = {
+    id: "Example",
+    type: ExecutionSateType.DEPLOYMENT_EXECUTION_STATE,
+    futureType: FutureType.CONTRACT_DEPLOYMENT,
+    strategy: "basic",
+    status: ExecutionStatus.STARTED,
+    dependencies: new Set<string>(),
+    networkInteractions: [],
+    artifactId: "./artifact.json",
+    contractName: "Contract1",
+    value: BigInt("0"),
+    constructorArgs: [],
+    libraries: {},
+    from: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+  };
+
+  beforeEach(() => {
+    exampleModule = buildModule("Example", (m) => {
+      m.contract("MyContract", [], { from: exampleAccounts[1] });
+
+      return {};
+    });
+  });
+
+  describe("first deployment run", () => {
+    it("should allow if there are no pending transactions for all of the future's senders", async () => {
+      // Set latest block to an arbitrary nonce
+      const latestCount1 = 55;
+      // The safest is the same as latest, as it is not relevant to this test
+      const safestCount1 = latestCount1;
+      // There are no pending transactions
+      const pendingCount1 = latestCount1;
+
+      // Set latest block to an arbitrary nonce
+      const latestCount2 = 12;
+      // The safest is the same as latest, as it is not relevant to this test
+      const safestCount2 = latestCount2;
+      // There are no pending transactions
+      const pendingCount2 = latestCount2;
+
+      const ignitionModule = buildModule("Example", (m) => {
+        m.contract("MyContract", [], { from: exampleAccounts[1] });
+        m.contract("AnotherContract", [], { from: undefined });
+
+        return {};
+      });
+
+      await assertSuccessOnGetNonceSyncResult({
+        ignitionModule,
+        transactionCountEntries: {
+          [exampleAccounts[1]]: {
+            pending: pendingCount1,
+            latest: latestCount1,
+            number: () => safestCount1,
+          },
+          [exampleAccounts[2]]: {
+            pending: pendingCount2,
+            latest: latestCount2,
+            number: () => safestCount2,
+          },
+        },
+      });
+    });
+
+    it("should throw if there are pending transactions for a future's sender", async () => {
+      // Set the latest block to be an arbitrary nonce
+      const latestCount = 30;
+      // Safest is the same as latest as it is not relevant in this test
+      const safestCount = latestCount;
+      // There are pending transactions
+      const pendingCount = latestCount + 1;
+
+      await assertGetNonceSyncThrows(
+        {
+          ignitionModule: exampleModule,
+          transactionCountEntries: {
+            [exampleAccounts[1]]: {
+              pending: pendingCount,
+              latest: latestCount,
+              number: () => safestCount,
+            },
+          },
+        },
+        `IGN403: You have sent transactions from ${exampleAccounts[1]}. Please wait until they get 5 confirmations before running Ignition again.`
+      );
+    });
+  });
+
+  describe("second deployment run", () => {
+    it("should indicate the user replaced the transaction if the transaction's nonce is less than the latest", async () => {
+      // Set latest to be an arbitrary nonce
+      const latestCount = 30;
+      // Set safest to be the same as latest, it is not relevant
+      const safestCount = latestCount;
+      // There are no pending transactions
+      const pendingCount = latestCount;
+      // Set the nonce to be less than latest, indicating it was replaced
+      const nonce = latestCount - 2;
+
+      await assertGetNonceSyncResult(
+        {
+          ignitionModule: exampleModule,
+          deploymentState:
+            setupDeploymentStateBasedOnExampleModuleWithOneTranWith(nonce),
+          transactionCountEntries: {
+            [exampleAccounts[1]]: {
+              pending: pendingCount,
+              latest: latestCount,
+              number: () => safestCount,
+            },
+          },
+        },
+        [
+          {
+            futureId: "Example#MyContract",
+            networkInteractionId: 1,
+            type: JournalMessageType.ONCHAIN_INTERACTION_REPLACED_BY_USER,
+          },
+        ]
+      );
+    });
+
+    it("should error if the user has sent a non-ignition pending transaction that has not confirmed on the account", async () => {
+      // Set latest to an arbitary nonce
+      const latestCount = 30;
+      // Safe is the same as latest
+      const safestCount = latestCount;
+      // Set the nonce to be larger than latest
+      const nonce = latestCount + 1;
+      // Set pending larger than the nonce
+      const pendingCount = nonce + 1;
+
+      await assertGetNonceSyncThrows(
+        {
+          ignitionModule: exampleModule,
+          deploymentState:
+            setupDeploymentStateBasedOnExampleModuleWithOneTranWith(nonce),
+          transactionCountEntries: {
+            [exampleAccounts[1]]: {
+              pending: pendingCount,
+              latest: latestCount,
+              number: () => safestCount,
+            },
+          },
+        },
+        `IGN404: You have sent transactions from ${exampleAccounts[1]} with nonce 31. Please wait until they get 5 confirmations before running Ignition again.`
+      );
+    });
+
+    it("should indicate the transaction was dropped if the nonce is higher than the latest", async () => {
+      // Set an arbitary latest
+      const latestCount = 30;
+      // The safest is exactly the same as latest
+      const safestCount = 40;
+      // Pending isn't relevant so is set to latest
+      const pendingCount = latestCount;
+      // Set the nonce higher than latest, indicating it was dropped
+      const nonce = latestCount + 1;
+
+      await assertGetNonceSyncResult(
+        {
+          ignitionModule: exampleModule,
+          deploymentState:
+            setupDeploymentStateBasedOnExampleModuleWithOneTranWith(nonce),
+          transactionCountEntries: {
+            [exampleAccounts[1]]: {
+              pending: pendingCount,
+              latest: latestCount,
+              number: () => safestCount,
+            },
+          },
+        },
+        [
+          {
+            futureId: "Example#MyContract",
+            networkInteractionId: 1,
+            type: JournalMessageType.ONCHAIN_INTERACTION_DROPPED,
+          },
+        ]
+      );
+    });
+
+    it("should ignore futures that have already been completed", async () => {
+      // Safest count nonce is set arbitarily
+      const latestCount = 40;
+      // Safest is the same as latest
+      const safestCount = latestCount;
+      // There are multiple pending transactions on top of latest
+      const pendingCount = latestCount + 99;
+
+      await assertSuccessOnGetNonceSyncResult({
+        ignitionModule: exampleModule,
+        deploymentState: {
+          ...deploymentStateReducer(),
+          executionStates: {
+            "Example#MyContract": {
+              ...exampleDeploymentState,
+              id: "Example#MyContract",
+              status: ExecutionStatus.SUCCESS,
+            },
+          },
+        },
+        transactionCountEntries: {
+          [exampleAccounts[1]]: {
+            pending: pendingCount,
+            latest: latestCount,
+            number: () => safestCount,
+          },
+        },
+      });
+    });
+  });
+});
+
+async function assertGetNonceSyncThrows(
+  ctx: {
+    ignitionModule: IgnitionModule<
+      string,
+      string,
+      IgnitionModuleResult<string>
+    >;
+    deploymentState?: DeploymentState;
+    transactionCountEntries?: {
+      [key: string]: {
+        pending: number;
+        latest: number;
+        number: (num: number) => number;
+      };
+    };
+  },
+  errorMessage: string
+) {
+  await assert.isRejected(assertGetNonceSyncResult(ctx, []), errorMessage);
+}
+
+async function assertSuccessOnGetNonceSyncResult(ctx: {
+  ignitionModule: IgnitionModule<string, string, IgnitionModuleResult<string>>;
+  deploymentState?: DeploymentState;
+  transactionCountEntries?: {
+    [key: string]: {
+      pending: number;
+      latest: number;
+      number: (num: number) => number;
+    };
+  };
+}) {
+  return assertGetNonceSyncResult(ctx, []);
+}
+
+async function assertGetNonceSyncResult(
+  {
+    ignitionModule,
+    deploymentState = deploymentStateReducer(),
+    transactionCountEntries = {},
+  }: {
+    ignitionModule: IgnitionModule<
+      string,
+      string,
+      IgnitionModuleResult<string>
+    >;
+    deploymentState?: DeploymentState;
+    transactionCountEntries?: {
+      [key: string]: {
+        pending: number;
+        latest: number;
+        number: (num: number) => number;
+      };
+    };
+  },
+  expectedResult: Array<
+    OnchainInteractionReplacedByUserMessage | OnchainInteractionDroppedMessage
+  >
+) {
+  const mockJsonRpcClient = setupJsonRpcClient(
+    latestBlock,
+    transactionCountEntries
+  );
+
+  const result = await getNonceSyncMessages(
+    mockJsonRpcClient,
+    deploymentState,
+    ignitionModule,
+    exampleAccounts,
+    exampleAccounts[2],
+    requiredConfirmations
+  );
+
+  assert.deepStrictEqual(result, expectedResult);
+}
+
+function setupJsonRpcClient(
+  latestBlockNum: number,
+  transactionCountEntries: {
+    [key: string]: {
+      pending: number;
+      latest: number;
+      number: (num: number) => number;
+    };
+  }
+): JsonRpcClient {
+  const mockJsonRpcClient = {
+    getLatestBlock: () => {
+      return { number: latestBlockNum };
+    },
+    getTransactionCount: (
+      address: string,
+      blockTag: "pending" | "latest" | number
+    ) => {
+      const transactionCountEntry = transactionCountEntries[address];
+
+      if (transactionCountEntry === undefined) {
+        throw new Error(
+          `Mock getTransactionCount was not expecting the sender: ${address}`
+        );
+      }
+
+      if (blockTag === "pending") {
+        return transactionCountEntry.pending;
+      } else if (blockTag === "latest") {
+        return transactionCountEntry.latest;
+      } else {
+        return transactionCountEntry.number(blockTag);
+      }
+    },
+  } as any;
+
+  return mockJsonRpcClient;
+}
+
+function setupDeploymentStateBasedOnExampleModuleWithOneTranWith(
+  nonce: number
+): DeploymentState {
+  const exampleDeploymentState: DeploymentExecutionState = {
+    id: "Example",
+    type: ExecutionSateType.DEPLOYMENT_EXECUTION_STATE,
+    futureType: FutureType.CONTRACT_DEPLOYMENT,
+    strategy: "basic",
+    status: ExecutionStatus.STARTED,
+    dependencies: new Set<string>(),
+    networkInteractions: [],
+    artifactId: "./artifact.json",
+    contractName: "Contract1",
+    value: BigInt("0"),
+    constructorArgs: [],
+    libraries: {},
+    from: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+  };
+
+  return {
+    ...deploymentStateReducer(),
+    executionStates: {
+      "Example#MyContract": {
+        ...exampleDeploymentState,
+        id: "Example#MyContract",
+        status: ExecutionStatus.STARTED,
+        from: exampleAccounts[1],
+        networkInteractions: [
+          {
+            id: 1,
+            type: NetworkInteractionType.ONCHAIN_INTERACTION,
+            to: undefined,
+            data: "0x",
+            value: BigInt(0),
+            transactions: [],
+            shouldBeResent: false,
+            nonce,
+          },
+        ],
+      },
+    },
+  };
+}