Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/weak-apricots-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@nomicfoundation/ignition-core": patch
"@nomicfoundation/hardhat-ignition-ethers": patch
"@nomicfoundation/hardhat-ignition-viem": patch
"@nomicfoundation/hardhat-ignition": patch
---

Expose ignition retry loop variables in user config (Hardhat v3) ([#7303](https://github.com/NomicFoundation/hardhat/issues/7303))
6 changes: 6 additions & 0 deletions v-next/hardhat-ignition-core/src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export async function deploy<
maxPriorityFeePerGas,
gasPrice,
disableFeeBumping,
maxRetries,
retryInterval,
}: {
config?: Partial<DeployConfig>;
artifactResolver: ArtifactResolver;
Expand All @@ -74,6 +76,8 @@ export async function deploy<
maxPriorityFeePerGas?: bigint;
gasPrice?: bigint;
disableFeeBumping?: boolean;
maxRetries?: number;
Copy link
Contributor

@michalbrabec michalbrabec Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we adding the property here, when the config property contains the value as well?
The final value is calculated as follows, which can be done by the deploy function.

    maxRetries:
      hre.config.ignition.maxRetries ??
      hre.config.networks[connection.networkName]?.ignition.maxRetries,

This could also concentrate the logic in one place instead of spreading it through all the ignition-helpers.

retryInterval?: number;
}): Promise<DeploymentResult> {
const executionStrategy: ExecutionStrategy = resolveStrategy(
strategy,
Expand Down Expand Up @@ -131,6 +135,8 @@ export async function deploy<
? DEFAULT_AUTOMINE_REQUIRED_CONFIRMATIONS
: config.requiredConfirmations ?? defaultConfig.requiredConfirmations,
disableFeeBumping: disableFeeBumping ?? defaultConfig.disableFeeBumping,
maxRetries: maxRetries ?? defaultConfig.maxRetries,
retryInterval: retryInterval ?? defaultConfig.retryInterval,
...config,
};

Expand Down
2 changes: 2 additions & 0 deletions v-next/hardhat-ignition-core/src/internal/defaultConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export const defaultConfig: DeployConfig = {
maxFeeBumps: 4,
requiredConfirmations: 5,
disableFeeBumping: false,
maxRetries: 10,
retryInterval: 1_000,
};

/**
Expand Down
2 changes: 2 additions & 0 deletions v-next/hardhat-ignition-core/src/internal/deployer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ export class Deployer {
this._config.maxFeeBumps,
this._config.blockPollingInterval,
this._config.disableFeeBumping,
this._config.maxRetries,
this._config.retryInterval,
);

deploymentState = await executionEngine.executeModule(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export class ExecutionEngine {
private readonly _maxFeeBumps: number,
private readonly _blockPollingInterval: number,
private readonly _disableFeeBumping: boolean,
private readonly _maxRetries: number,
private readonly _retryInterval: number,
) {}

/**
Expand Down Expand Up @@ -107,6 +109,8 @@ export class ExecutionEngine {
deploymentParameters,
defaultSender,
this._disableFeeBumping,
this._maxRetries,
this._retryInterval,
);

const futures = getFuturesFromModule(module);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export class FutureProcessor {
private readonly _deploymentParameters: DeploymentParameters,
private readonly _defaultSender: string,
private readonly _disableFeeBumping: boolean,
private readonly _maxRetries: number,
private readonly _retryInterval: number,
) {}

/**
Expand Down Expand Up @@ -207,16 +209,17 @@ export class FutureProcessor {
`Unexpected transaction request in StaticCallExecutionState ${exState.id}`,
);

return monitorOnchainInteraction(
return monitorOnchainInteraction({
exState,
this._jsonRpcClient,
this._transactionTrackingTimer,
this._requiredConfirmations,
this._millisecondBeforeBumpingFees,
this._maxFeeBumps,
undefined,
this._disableFeeBumping,
);
jsonRpcClient: this._jsonRpcClient,
transactionTrackingTimer: this._transactionTrackingTimer,
requiredConfirmations: this._requiredConfirmations,
millisecondBeforeBumpingFees: this._millisecondBeforeBumpingFees,
maxFeeBumps: this._maxFeeBumps,
disableFeeBumping: this._disableFeeBumping,
maxRetries: this._maxRetries,
retryInterval: this._retryInterval,
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import type {
OnchainInteractionTimeoutMessage,
TransactionConfirmMessage,
} from "../../types/messages.js";
import type { OnchainInteraction } from "../../types/network-interaction.js";
import type {
GetTransactionRetryConfig,
OnchainInteraction,
} from "../../types/network-interaction.js";

import { HardhatError } from "@nomicfoundation/hardhat-errors";
import setupDebug from "debug";
Expand All @@ -22,11 +25,6 @@ import { NetworkInteractionType } from "../../types/network-interaction.js";

const debug = setupDebug("hardhat-ignition:onchain-interaction-monitor");

export interface GetTransactionRetryConfig {
maxRetries: number;
retryInterval: number;
}

/**
* Checks the transactions of the latest network interaction of the execution state,
* and returns a message, or undefined if we need to wait for more confirmations.
Expand All @@ -40,81 +38,82 @@ export interface GetTransactionRetryConfig {
*
* SIDE EFFECTS: This function doesn't have any side effects.
*
* @param exState The execution state that requires the transactions to be checked.
* @param jsonRpcClient The JSON RPC client to use for accessing the network.
* @param transactionTrackingTimer The TransactionTrackingTimer to use for checking the
* @param params.exState The execution state that requires the transactions to be checked.
* @param params.jsonRpcClient The JSON RPC client to use for accessing the network.
* @param params.transactionTrackingTimer The TransactionTrackingTimer to use for checking the
* if a transaction has been pending for too long.
* @param requiredConfirmations The number of confirmations required for a transaction
* @param params.requiredConfirmations The number of confirmations required for a transaction
* to be considered confirmed.
* @param millisecondBeforeBumpingFees The number of milliseconds before bumping the fees
* @param params.millisecondBeforeBumpingFees The number of milliseconds before bumping the fees
* of a transaction.
* @param maxFeeBumps The maximum number of times we can bump the fees of a transaction
* @param params.maxFeeBumps The maximum number of times we can bump the fees of a transaction
* before considering the onchain interaction timed out.
* @param getTransactionRetryConfig This is really only a parameter to help with testing this function
* @param disableFeeBumping Disables fee bumping for all transactions.
* @param params.getTransactionRetryConfig This is really only a parameter to help with testing this function
* @param params.disableFeeBumping Disables fee bumping for all transactions.
* @param params.maxRetries The maximum number of times to retry fetching a transaction from the mempool.
* @param params.retryInterval The number of milliseconds to wait between retries when fetching
* a transaction from the mempool.
* @returns A message indicating the result of checking the transactions of the latest
* network interaction.
*/
export async function monitorOnchainInteraction(
exState:
| DeploymentExecutionState
| CallExecutionState
| SendDataExecutionState,
jsonRpcClient: JsonRpcClient,
transactionTrackingTimer: TransactionTrackingTimer,
requiredConfirmations: number,
millisecondBeforeBumpingFees: number,
maxFeeBumps: number,
givenGetTransactionRetryConfig: GetTransactionRetryConfig | undefined,
disableFeeBumping: boolean,
params: {
exState:
| DeploymentExecutionState
| CallExecutionState
| SendDataExecutionState;
jsonRpcClient: JsonRpcClient;
transactionTrackingTimer: TransactionTrackingTimer;
requiredConfirmations: number;
millisecondBeforeBumpingFees: number;
maxFeeBumps: number;
disableFeeBumping: boolean;
} & GetTransactionRetryConfig,
): Promise<
| TransactionConfirmMessage
| OnchainInteractionBumpFeesMessage
| OnchainInteractionTimeoutMessage
| undefined
> {
const lastNetworkInteraction = exState.networkInteractions.at(-1);
const getTransactionRetryConfig = givenGetTransactionRetryConfig ?? {
maxRetries: 10,
retryInterval: 1000,
};
const lastNetworkInteraction = params.exState.networkInteractions.at(-1);

assertIgnitionInvariant(
lastNetworkInteraction !== undefined,
`No network interaction for ExecutionState ${exState.id} when trying to check its transactions`,
`No network interaction for ExecutionState ${params.exState.id} when trying to check its transactions`,
);

assertIgnitionInvariant(
lastNetworkInteraction.type === NetworkInteractionType.ONCHAIN_INTERACTION,
`StaticCall found as last network interaction of ExecutionState ${exState.id} when trying to check its transactions`,
`StaticCall found as last network interaction of ExecutionState ${params.exState.id} when trying to check its transactions`,
);

assertIgnitionInvariant(
lastNetworkInteraction.transactions.length > 0,
`No transaction found in OnchainInteraction ${exState.id}/${lastNetworkInteraction.id} when trying to check its transactions`,
`No transaction found in OnchainInteraction ${params.exState.id}/${lastNetworkInteraction.id} when trying to check its transactions`,
);

const transaction = await _getTransactionWithRetry(
jsonRpcClient,
lastNetworkInteraction,
getTransactionRetryConfig,
exState.id,
);
const transaction = await _getTransactionWithRetry({
jsonRpcClient: params.jsonRpcClient,
onchainInteraction: lastNetworkInteraction,
futureId: params.exState.id,
maxRetries: params.maxRetries,
retryInterval: params.retryInterval,
});

// We do not try to recover from dropped transactions mid-execution
if (transaction === undefined) {
throw new HardhatError(
HardhatError.ERRORS.IGNITION.EXECUTION.DROPPED_TRANSACTION,
{
futureId: exState.id,
futureId: params.exState.id,
networkInteractionId: lastNetworkInteraction.id,
},
);
}

const [block, receipt] = await Promise.all([
jsonRpcClient.getLatestBlock(),
jsonRpcClient.getTransactionReceipt(transaction.hash),
params.jsonRpcClient.getLatestBlock(),
params.jsonRpcClient.getTransactionReceipt(transaction.hash),
]);

if (receipt !== undefined) {
Expand All @@ -127,10 +126,10 @@ export async function monitorOnchainInteraction(
// values that are high enough to avoid reorgs, we don't do it.
const confirmations = block.number - receipt.blockNumber + 1;

if (confirmations >= requiredConfirmations) {
if (confirmations >= params.requiredConfirmations) {
return {
type: JournalMessageType.TRANSACTION_CONFIRM,
futureId: exState.id,
futureId: params.exState.id,
networkInteractionId: lastNetworkInteraction.id,
hash: transaction.hash,
receipt,
Expand All @@ -140,53 +139,55 @@ export async function monitorOnchainInteraction(
return undefined;
}

const timeTrackingTx = transactionTrackingTimer.getTransactionTrackingTime(
transaction.hash,
);
const timeTrackingTx =
params.transactionTrackingTimer.getTransactionTrackingTime(
transaction.hash,
);

if (timeTrackingTx < millisecondBeforeBumpingFees) {
if (timeTrackingTx < params.millisecondBeforeBumpingFees) {
return undefined;
}

if (
disableFeeBumping ||
lastNetworkInteraction.transactions.length > maxFeeBumps
params.disableFeeBumping ||
lastNetworkInteraction.transactions.length > params.maxFeeBumps
) {
return {
type: JournalMessageType.ONCHAIN_INTERACTION_TIMEOUT,
futureId: exState.id,
futureId: params.exState.id,
networkInteractionId: lastNetworkInteraction.id,
};
}

return {
type: JournalMessageType.ONCHAIN_INTERACTION_BUMP_FEES,
futureId: exState.id,
futureId: params.exState.id,
networkInteractionId: lastNetworkInteraction.id,
};
}

async function _getTransactionWithRetry(
jsonRpcClient: JsonRpcClient,
onchainInteraction: OnchainInteraction,
retryConfig: GetTransactionRetryConfig,
futureId: string,
params: {
jsonRpcClient: JsonRpcClient;
onchainInteraction: OnchainInteraction;
futureId: string;
} & GetTransactionRetryConfig,
): Promise<Transaction | undefined> {
let transaction: Transaction | undefined;

// Small retry loop for up to X seconds to handle blockchain nodes
// that are slow to propagate transactions.
// See https://github.com/NomicFoundation/hardhat-ignition/issues/665
for (let i = 0; i < retryConfig.maxRetries; i++) {
for (let i = 0; i < params.maxRetries; i++) {
debug(
`Retrieving transaction for interaction ${futureId}/${
onchainInteraction.id
} from mempool (attempt ${i + 1}/${retryConfig.maxRetries})`,
`Retrieving transaction for interaction ${params.futureId}/${
params.onchainInteraction.id
} from mempool (attempt ${i + 1}/${params.maxRetries})`,
);

const transactions = await Promise.all(
onchainInteraction.transactions.map((tx) =>
jsonRpcClient.getTransaction(tx.hash),
params.onchainInteraction.transactions.map((tx) =>
params.jsonRpcClient.getTransaction(tx.hash),
),
);

Expand All @@ -197,12 +198,10 @@ async function _getTransactionWithRetry(
}

debug(
`Transaction lookup for ${futureId}/${onchainInteraction.id} not found in mempool, waiting ${retryConfig.retryInterval} seconds before retrying`,
`Transaction lookup for ${params.futureId}/${params.onchainInteraction.id} not found in mempool, waiting ${params.retryInterval} seconds before retrying`,
);

await new Promise((resolve) =>
setTimeout(resolve, retryConfig.retryInterval),
);
await new Promise((resolve) => setTimeout(resolve, params.retryInterval));
}

return transaction;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,11 @@ export interface StaticCall {
from: string;
result?: RawStaticCallResult;
}

/**
* Configuration for the retry loop when trying to fetch a transaction from the node.
*/
export interface GetTransactionRetryConfig {
maxRetries: number;
retryInterval: number;
}
12 changes: 12 additions & 0 deletions v-next/hardhat-ignition-core/src/types/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ export interface DeployConfig {
* Disables fee bumping for all transactions.
*/
disableFeeBumping: boolean;

/**
* The maximum number of times to retry a call to getTransactionReceipt
* when monitoring the status of a transaction.
*/
maxRetries: number;

/**
* The interval, in milliseconds, between retries when calling
* getTransactionReceipt.
*/
retryInterval: number;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ describe("ExecutionEngine", () => {
5,
5,
false,
10,
1000,
);

const deploymentState = await loadDeploymentState(deploymentLoader);
Expand Down
Loading
Loading