Skip to content
Open
Show file tree
Hide file tree
Changes from all 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/rich-seals-wonder.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 v2) ([#7303](https://github.com/NomicFoundation/hardhat/issues/7303))
6 changes: 6 additions & 0 deletions packages/hardhat-ignition-core/src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ export async function deploy<
maxPriorityFeePerGas,
gasPrice,
disableFeeBumping,
maxRetries,
retryInterval,
}: {
config?: Partial<DeployConfig>;
artifactResolver: ArtifactResolver;
Expand All @@ -77,6 +79,8 @@ export async function deploy<
maxPriorityFeePerGas?: bigint;
gasPrice?: bigint;
disableFeeBumping?: boolean;
maxRetries?: number;
retryInterval?: number;
}): Promise<DeploymentResult> {
const executionStrategy: ExecutionStrategy = resolveStrategy(
strategy,
Expand Down Expand Up @@ -135,6 +139,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 packages/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
4 changes: 3 additions & 1 deletion packages/hardhat-ignition-core/src/internal/deployer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,9 @@ export class Deployer {
this._config.timeBeforeBumpingFees,
this._config.maxFeeBumps,
this._config.blockPollingInterval,
this._config.disableFeeBumping
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 @@ -47,7 +47,9 @@ export class ExecutionEngine {
private readonly _millisecondBeforeBumpingFees: number,
private readonly _maxFeeBumps: number,
private readonly _blockPollingInterval: number,
private readonly _disableFeeBumping: boolean
private readonly _disableFeeBumping: boolean,
private readonly _maxRetries: number,
private readonly _retryInterval: number
) {}

/**
Expand Down Expand Up @@ -106,7 +108,9 @@ export class ExecutionEngine {
accounts,
deploymentParameters,
defaultSender,
this._disableFeeBumping
this._disableFeeBumping,
this._maxRetries,
this._retryInterval
);

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

/**
Expand Down Expand Up @@ -205,16 +207,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 @@ -18,17 +18,13 @@ import {
TransactionConfirmMessage,
} from "../../types/messages";
import {
GetTransactionRetryConfig,
NetworkInteractionType,
OnchainInteraction,
} from "../../types/network-interaction";

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 @@ -42,77 +38,78 @@ 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.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,
getTransactionRetryConfig: GetTransactionRetryConfig = {
maxRetries: 10,
retryInterval: 1000,
},
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 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 IgnitionError(ERRORS.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 @@ -125,10 +122,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 @@ -138,53 +135,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 @@ -195,12 +194,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 @@ -65,3 +65,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 packages/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 @@ -23,7 +23,9 @@ describe("ExecutionEngine", () => {
5,
5,
5,
false
false,
10,
1000
);

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