diff --git a/.changeset/giant-ants-protect.md b/.changeset/giant-ants-protect.md new file mode 100644 index 00000000000..7f57d873b80 --- /dev/null +++ b/.changeset/giant-ants-protect.md @@ -0,0 +1,8 @@ +--- +"@nomicfoundation/ignition-core": patch +"@nomicfoundation/ignition-ui": patch +"@nomicfoundation/hardhat-ignition": patch +"hardhat": patch +--- + +Port transaction hash bug fix to v3 ([#6429](https://github.com/NomicFoundation/hardhat/pull/6429)) diff --git a/v-next/hardhat-errors/src/descriptors.ts b/v-next/hardhat-errors/src/descriptors.ts index 51821057119..51e38331438 100644 --- a/v-next/hardhat-errors/src/descriptors.ts +++ b/v-next/hardhat-errors/src/descriptors.ts @@ -189,6 +189,11 @@ export const ERROR_CATEGORIES: { max: 11299, websiteSubTitle: "List transactions errors", }, + TRACK_TRANSACTIONS: { + min: 11300, + max: 11399, + websiteSubTitle: "Track transactions errors", + }, }, }, HARDHAT_ETHERS: { @@ -1506,6 +1511,14 @@ Please review the error message and try again.`, websiteTitle: "Gas estimation failed", websiteDescription: `Gas estimation failed`, }, + TRANSACTION_LOST: { + number: 10410, + messageTemplate: `An error occured while trying to send a transaction for future {futureId}. +Please use a block explorer to find the hash of the transaction with nonce {nonce} sent from account {sender} and use the following command to add it to your deployment: +npx hardhat ignition track-tx --network `, + websiteTitle: "Transaction lost", + websiteDescription: `An error occured while trying to send a transaction`, + }, }, RECONCILIATION: { INVALID_EXECUTION_STATUS: { @@ -1809,6 +1822,48 @@ Please review the error message and try again.`, websiteDescription: `Cannot list transactions for nonexistant deployment`, }, }, + TRACK_TRANSACTIONS: { + DEPLOYMENT_DIR_NOT_FOUND: { + number: 11300, + messageTemplate: "Deployment directory {deploymentDir} not found", + websiteTitle: "Deployment directory not found", + websiteDescription: `The deployment directory was not found`, + }, + UNINITIALIZED_DEPLOYMENT: { + number: 11301, + messageTemplate: + "Cannot track transaction for nonexistant deployment at {deploymentDir}", + websiteTitle: "Uninitialized deployment", + websiteDescription: `Cannot track transaction for nonexistant deployment`, + }, + TRANSACTION_NOT_FOUND: { + number: 11302, + messageTemplate: `Transaction {txHash} not found. Please double check the transaction hash and try again.`, + websiteTitle: "Transaction not found", + websiteDescription: `The transaction hash you provided was not found on the network.`, + }, + MATCHING_NONCE_NOT_FOUND: { + number: 11303, + messageTemplate: `The transaction you provided doesn't seem to belong to your deployment. +Please double check the error you are getting when running Hardhat Ignition, and the instructions it's providing.`, + websiteTitle: "Matching nonce not found", + websiteDescription: `The transaction you provided doesn't seem to belong to your deployment.`, + }, + KNOWN_TRANSACTION: { + number: 11304, + messageTemplate: `The transaction hash that you provided was already present in your deployment. +Please double check the error you are getting when running Hardhat Ignition, and the instructions it's providing.`, + websiteTitle: "Known transaction", + websiteDescription: `The transaction hash that you provided was already present in your deployment.`, + }, + INSUFFICIENT_CONFIRMATIONS: { + number: 11305, + messageTemplate: `The transaction you provided doesn't have enough confirmations yet. +Please try again later.`, + websiteTitle: "Insufficient confirmations", + websiteDescription: `The transaction you provided doesn't have enough confirmations yet.`, + }, + }, }, HARDHAT_ETHERS: { GENERAL: { diff --git a/v-next/hardhat-ignition-core/src/index.ts b/v-next/hardhat-ignition-core/src/index.ts index 94ba64eaadf..14996905991 100644 --- a/v-next/hardhat-ignition-core/src/index.ts +++ b/v-next/hardhat-ignition-core/src/index.ts @@ -17,5 +17,6 @@ export * from "./types/provider.js"; export * from "./types/serialization.js"; export * from "./types/status.js"; export * from "./types/verify.js"; +export { trackTransaction } from "./track-transaction.js"; export { getVerificationInformation } from "./verify.js"; export { wipe } from "./wipe.js"; diff --git a/v-next/hardhat-ignition-core/src/internal/execution/deployment-state-helpers.ts b/v-next/hardhat-ignition-core/src/internal/execution/deployment-state-helpers.ts index 3be4d736711..271ccbc98ba 100644 --- a/v-next/hardhat-ignition-core/src/internal/execution/deployment-state-helpers.ts +++ b/v-next/hardhat-ignition-core/src/internal/execution/deployment-state-helpers.ts @@ -51,9 +51,9 @@ export async function initializeDeploymentState( * This function applies a new message to the deployment state, recording it to the * journal if needed. * - * @param message The message to apply. - * @param deploymentState The original deployment state. - * @param deploymentLoader The deployment loader that will be used to record the message. + * @param message - The message to apply. + * @param deploymentState - The original deployment state. + * @param deploymentLoader - The deployment loader that will be used to record the message. * @returns The new deployment state. */ export async function applyNewMessage( diff --git a/v-next/hardhat-ignition-core/src/internal/execution/execution-engine.ts b/v-next/hardhat-ignition-core/src/internal/execution/execution-engine.ts index b857038945f..8d2b609d4ba 100644 --- a/v-next/hardhat-ignition-core/src/internal/execution/execution-engine.ts +++ b/v-next/hardhat-ignition-core/src/internal/execution/execution-engine.ts @@ -11,11 +11,13 @@ import type { } from "../../types/module.js"; import type { DeploymentLoader } from "../deployment-loader/types.js"; +import { HardhatError } from "@nomicfoundation/hardhat-errors"; import sortBy from "lodash-es/sortBy.js"; import { ExecutionEventType } from "../../types/execution-events.js"; import { assertIgnitionInvariant } from "../utils/assertions.js"; import { getFuturesFromModule } from "../utils/get-futures-from-module.js"; +import { getNetworkExecutionStates } from "../views/execution-state/get-network-execution-states.js"; import { getPendingNonceAndSender } from "../views/execution-state/get-pending-nonce-and-sender.js"; import { hasExecutionSucceeded } from "../views/has-execution-succeeded.js"; import { isBatchFinished } from "../views/is-batch-finished.js"; @@ -26,6 +28,7 @@ import { getMaxNonceUsedBySender } from "./nonce-management/get-max-nonce-used-b import { getNonceSyncMessages } from "./nonce-management/get-nonce-sync-messages.js"; import { JsonRpcNonceManager } from "./nonce-management/json-rpc-nonce-manager.js"; import { TransactionTrackingTimer } from "./transaction-tracking-timer.js"; +import { NetworkInteractionType } from "./types/network-interaction.js"; /** * This class is used to execute a module to completion, returning the new @@ -69,6 +72,8 @@ export class ExecutionEngine { deploymentParameters: DeploymentParameters, defaultSender: string, ): Promise { + await this._checkForMissingTransactions(deploymentState); + deploymentState = await this._syncNonces( deploymentState, module, @@ -199,6 +204,35 @@ export class ExecutionEngine { } } + /** + * Checks the journal for missing transactions, throws if any are found + * and asks the user to track the missing transaction via the `track-tx` command. + */ + private async _checkForMissingTransactions( + deploymentState: DeploymentState, + ): Promise { + const exStates = getNetworkExecutionStates(deploymentState); + + for (const exState of exStates) { + for (const ni of exState.networkInteractions) { + if ( + ni.type === NetworkInteractionType.ONCHAIN_INTERACTION && + ni.nonce !== undefined && + ni.transactions.length === 0 + ) { + throw new HardhatError( + HardhatError.ERRORS.IGNITION.EXECUTION.TRANSACTION_LOST, + { + futureId: exState.id, + nonce: ni.nonce, + sender: exState.from, + }, + ); + } + } + } + } + /** * Syncs the nonces of the deployment state with the blockchain, returning * the new deployment state, and throwing if they can't be synced. diff --git a/v-next/hardhat-ignition-core/src/internal/execution/future-processor/future-processor.ts b/v-next/hardhat-ignition-core/src/internal/execution/future-processor/future-processor.ts index 807bcf7ebf4..931b7b6f35a 100644 --- a/v-next/hardhat-ignition-core/src/internal/execution/future-processor/future-processor.ts +++ b/v-next/hardhat-ignition-core/src/internal/execution/future-processor/future-processor.ts @@ -195,6 +195,7 @@ export class FutureProcessor { this._jsonRpcClient, this._nonceManager, this._transactionTrackingTimer, + this._deploymentLoader, ); case NextAction.QUERY_STATIC_CALL: diff --git a/v-next/hardhat-ignition-core/src/internal/execution/future-processor/handlers/send-transaction.ts b/v-next/hardhat-ignition-core/src/internal/execution/future-processor/handlers/send-transaction.ts index 0a52e371df9..d5f8b04bde6 100644 --- a/v-next/hardhat-ignition-core/src/internal/execution/future-processor/handlers/send-transaction.ts +++ b/v-next/hardhat-ignition-core/src/internal/execution/future-processor/handlers/send-transaction.ts @@ -18,6 +18,7 @@ import type { TransactionSendMessage, } from "../../types/messages.js"; +import { DeploymentLoader } from "../../../deployment-loader/types.js"; import { assertIgnitionInvariant } from "../../../utils/assertions.js"; import { ExecutionResultType } from "../../types/execution-result.js"; import { JournalMessageType } from "../../types/messages.js"; @@ -58,6 +59,7 @@ export async function sendTransaction( jsonRpcClient: JsonRpcClient, nonceManager: NonceManager, transactionTrackingTimer: TransactionTrackingTimer, + deploymentLoader: DeploymentLoader, ): Promise< | TransactionSendMessage | DeploymentExecutionStateCompleteMessage @@ -89,6 +91,8 @@ export async function sendTransaction( lastNetworkInteraction, nonceManager, decodeSimulationResult(strategyGenerator, exState), + deploymentLoader, + exState.id, ); // If the transaction failed during simulation, we need to revert the nonce allocation diff --git a/v-next/hardhat-ignition-core/src/internal/execution/future-processor/helpers/network-interaction-execution.ts b/v-next/hardhat-ignition-core/src/internal/execution/future-processor/helpers/network-interaction-execution.ts index 67df9aca8d1..8c94a9df61a 100644 --- a/v-next/hardhat-ignition-core/src/internal/execution/future-processor/helpers/network-interaction-execution.ts +++ b/v-next/hardhat-ignition-core/src/internal/execution/future-processor/helpers/network-interaction-execution.ts @@ -23,7 +23,9 @@ import type { import { HardhatError } from "@nomicfoundation/hardhat-errors"; +import { DeploymentLoader } from "../../../deployment-loader/types.js"; import { assertIgnitionInvariant } from "../../../utils/assertions.js"; +import { JournalMessageType } from "../../types/messages.js"; /** * Runs a StaticCall NetworkInteraction to completion, returning its raw result. @@ -108,12 +110,14 @@ export async function sendTransactionForOnchainInteraction( | StrategySimulationErrorExecutionResult | undefined >, + deploymentLoader: DeploymentLoader, + futureId: string, ): Promise< | SimulationErrorExecutionResult | StrategySimulationErrorExecutionResult | { type: typeof TRANSACTION_SENT_TYPE; - transaction: Transaction; + transaction: Pick; nonce: number; } > { @@ -207,6 +211,13 @@ export async function sendTransactionForOnchainInteraction( return decodedSimulationResult; } + await deploymentLoader.recordToJournal({ + type: JournalMessageType.TRANSACTION_PREPARE_SEND, + futureId, + networkInteractionId: onchainInteraction.id, + nonce: transactionParams.nonce, + }); + const txHash = await client.sendTransaction(transactionParams); return { diff --git a/v-next/hardhat-ignition-core/src/internal/execution/jsonrpc-client.ts b/v-next/hardhat-ignition-core/src/internal/execution/jsonrpc-client.ts index 190c95889b9..2067b212cf3 100644 --- a/v-next/hardhat-ignition-core/src/internal/execution/jsonrpc-client.ts +++ b/v-next/hardhat-ignition-core/src/internal/execution/jsonrpc-client.ts @@ -1,5 +1,7 @@ import type { + FullTransaction, NetworkFees, + NetworkTransaction, RawStaticCallResult, Transaction, TransactionLog, @@ -488,9 +490,12 @@ export class EIP1193JsonRpcClient implements JsonRpcClient { return jsonRpcQuantityToNumber(response); } - public async getTransaction( + /** + * Like `getTransaction`, but returns the full transaction object. + */ + public async getFullTransaction( txHash: string, - ): Promise | undefined> { + ): Promise { const method = "eth_getTransactionByHash"; const response = await this._provider.request({ @@ -502,44 +507,58 @@ export class EIP1193JsonRpcClient implements JsonRpcClient { return undefined; } - assertResponseType(method, response, typeof response === "object"); + assertResponseIsNetworkTransactionType(response); - assertResponseType( - method, - response, - "hash" in response && typeof response.hash === "string", - ); + return { + hash: response.hash, + data: response.input, + from: response.from, + to: response.to ?? undefined, + chainId: jsonRpcQuantityToNumber(response.chainId), + value: jsonRpcQuantityToBigInt(response.value), + nonce: jsonRpcQuantityToNumber(response.nonce), + blockHash: response.blockHash, + blockNumber: + response.blockNumber !== null + ? jsonRpcQuantityToBigInt(response.blockNumber) + : null, + maxFeePerGas: + "maxFeePerGas" in response + ? jsonRpcQuantityToBigInt(response.maxFeePerGas) + : undefined, + maxPriorityFeePerGas: + "maxPriorityFeePerGas" in response + ? jsonRpcQuantityToBigInt(response.maxPriorityFeePerGas) + : undefined, + gasPrice: + "gasPrice" in response + ? jsonRpcQuantityToBigInt(response.gasPrice) + : undefined, + gasLimit: + "gas" in response && response.gas !== undefined + ? jsonRpcQuantityToBigInt(response.gas) + : undefined, + }; + } - assertResponseType( - method, - response, - "blockNumber" in response && - (typeof response.blockNumber === "string" || - response.blockNumber === null), - ); + public async getTransaction( + txHash: string, + ): Promise | undefined> { + const method = "eth_getTransactionByHash"; - assertResponseType( + const response = await this._provider.request({ method, - response, - "blockHash" in response && - (typeof response.blockHash === "string" || response.blockHash === null), - ); + params: [txHash], + }); - let networkFees: NetworkFees; - if ("maxFeePerGas" in response) { - assertResponseType( - method, - response, - "maxFeePerGas" in response && typeof response.maxFeePerGas === "string", - ); + if (response === null) { + return undefined; + } - assertResponseType( - method, - response, - "maxPriorityFeePerGas" in response && - typeof response.maxPriorityFeePerGas === "string", - ); + assertResponseIsNetworkTransactionType(response); + let networkFees: NetworkFees; + if ("maxFeePerGas" in response) { networkFees = { maxFeePerGas: jsonRpcQuantityToBigInt(response.maxFeePerGas), maxPriorityFeePerGas: jsonRpcQuantityToBigInt( @@ -547,12 +566,6 @@ export class EIP1193JsonRpcClient implements JsonRpcClient { ), }; } else { - assertResponseType( - method, - response, - "gasPrice" in response && typeof response.gasPrice === "string", - ); - networkFees = { gasPrice: jsonRpcQuantityToBigInt(response.gasPrice), }; @@ -826,6 +839,72 @@ function assertResponseType( } } +function assertResponseIsNetworkTransactionType( + response: unknown, +): asserts response is NetworkTransaction { + const method = "eth_getTransactionByHash"; + + assertResponseType( + method, + response, + typeof response === "object" && response !== null, + ); + + assertResponseType( + method, + response, + "hash" in response && typeof response.hash === "string", + ); + + assertResponseType( + method, + response, + "blockNumber" in response && + (typeof response.blockNumber === "string" || + response.blockNumber === null), + ); + + assertResponseType( + method, + response, + "blockHash" in response && + (typeof response.blockHash === "string" || response.blockHash === null), + ); + + assertResponseType( + method, + response, + "input" in response && typeof response.input === "string", + ); + + assertResponseType( + method, + response, + "nonce" in response && typeof response.input === "string", + ); + + if ("maxFeePerGas" in response) { + assertResponseType( + method, + response, + "maxFeePerGas" in response && typeof response.maxFeePerGas === "string", + ); + + assertResponseType( + method, + response, + "maxPriorityFeePerGas" in response && + typeof response.maxPriorityFeePerGas === "string", + ); + } else { + assertResponseType( + method, + response, + "gasPrice" in response && typeof response.gasPrice === "string", + ); + } +} + function formatReceiptLogs(method: string, response: object): TransactionLog[] { const formattedLogs: TransactionLog[] = []; diff --git a/v-next/hardhat-ignition-core/src/internal/execution/reducers/execution-state-reducer.ts b/v-next/hardhat-ignition-core/src/internal/execution/reducers/execution-state-reducer.ts index b90bd192a40..37a8ac9352e 100644 --- a/v-next/hardhat-ignition-core/src/internal/execution/reducers/execution-state-reducer.ts +++ b/v-next/hardhat-ignition-core/src/internal/execution/reducers/execution-state-reducer.ts @@ -20,6 +20,7 @@ import type { StaticCallExecutionStateCompleteMessage, StaticCallExecutionStateInitializeMessage, TransactionConfirmMessage, + TransactionPrepareSendMessage, TransactionSendMessage, } from "../types/messages.js"; @@ -40,6 +41,7 @@ import { import { appendNetworkInteraction, appendTransactionToOnchainInteraction, + applyNonceToOnchainInteraction, bumpOnchainInteractionFees, completeStaticCall, confirmTransaction, @@ -83,6 +85,7 @@ export function executionStateReducer( | ReadEventArgExecutionStateInitializeMessage | EncodeFunctionCallExecutionStateInitializeMessage | NetworkInteractionRequestMessage + | TransactionPrepareSendMessage | TransactionSendMessage | TransactionConfirmMessage | StaticCallCompleteMessage @@ -148,6 +151,13 @@ export function executionStateReducer( exStateTypesThatSupportOnchainInteractionsAndStaticCalls, completeStaticCall, ); + case JournalMessageType.TRANSACTION_PREPARE_SEND: + return _ensureStateThen( + state, + action, + exStateTypesThatSupportOnchainInteractions, + applyNonceToOnchainInteraction, + ); case JournalMessageType.TRANSACTION_SEND: return _ensureStateThen( state, diff --git a/v-next/hardhat-ignition-core/src/internal/execution/reducers/helpers/network-interaction-helpers.ts b/v-next/hardhat-ignition-core/src/internal/execution/reducers/helpers/network-interaction-helpers.ts index 040df7e95c5..77ad7a7c7d4 100644 --- a/v-next/hardhat-ignition-core/src/internal/execution/reducers/helpers/network-interaction-helpers.ts +++ b/v-next/hardhat-ignition-core/src/internal/execution/reducers/helpers/network-interaction-helpers.ts @@ -12,6 +12,7 @@ import type { OnchainInteractionTimeoutMessage, StaticCallCompleteMessage, TransactionConfirmMessage, + TransactionPrepareSendMessage, TransactionSendMessage, } from "../../types/messages.js"; @@ -107,6 +108,37 @@ export function appendTransactionToOnchainInteraction< }); } +/** + * Sets the nonce of the onchain interaction within an execution state. + * + * @param state - the execution state that will be added to + * @param action - the request message that contains the transaction prepare message + * @returns a copy of the execution state with the nonce set + */ +export function applyNonceToOnchainInteraction< + ExState extends + | DeploymentExecutionState + | CallExecutionState + | StaticCallExecutionState + | SendDataExecutionState, +>(state: ExState, action: TransactionPrepareSendMessage): ExState { + return produce(state, (draft: ExState): void => { + const onchainInteraction = findOnchainInteractionBy( + draft, + action.networkInteractionId, + ); + + if (onchainInteraction.nonce === undefined) { + onchainInteraction.nonce = action.nonce; + } else { + assertIgnitionInvariant( + onchainInteraction.nonce === action.nonce, + `New transaction sent for ${state.id}/${onchainInteraction.id} with nonce ${action.nonce} but expected ${onchainInteraction.nonce}`, + ); + } + }); +} + /** * Confirm a transaction for an onchain interaction within an execution state. * diff --git a/v-next/hardhat-ignition-core/src/internal/execution/types/jsonrpc.ts b/v-next/hardhat-ignition-core/src/internal/execution/types/jsonrpc.ts index f157a652378..34c0860e876 100644 --- a/v-next/hardhat-ignition-core/src/internal/execution/types/jsonrpc.ts +++ b/v-next/hardhat-ignition-core/src/internal/execution/types/jsonrpc.ts @@ -80,3 +80,50 @@ export interface Transaction { // Only available after the transaction has confirmed, with enough confirmations. receipt?: TransactionReceipt; } + +/** + * This interface represents a transaction with all of its available fields. + */ +export interface FullTransaction { + hash: string; + blockNumber: bigint | null; + blockHash: string | null; + nonce: number; + chainId: number; + from: string; + to: string | undefined; + value: bigint; + data: string; + gasLimit?: bigint; + gasPrice?: bigint; + maxPriorityFeePerGas?: bigint; + maxFeePerGas?: bigint; +} + +interface BaseNetworkTransaction { + hash: string; + blockNumber: string | null; + blockHash: string | null; + nonce: string; + chainId: string; + from: string; + to: string | null; + value: string; + input: string; + gas?: string; +} + +type LegacyNetworkTransaction = BaseNetworkTransaction & { + [P in keyof LegacyNetworkFees]: string; +}; + +type EIP1559NetworkTransaction = BaseNetworkTransaction & { + [P in keyof EIP1559NetworkFees]: string; +}; + +/** + * This type represents a transaction that was retrieved from the network. + */ +export type NetworkTransaction = + | LegacyNetworkTransaction + | EIP1559NetworkTransaction; diff --git a/v-next/hardhat-ignition-core/src/internal/execution/types/messages.ts b/v-next/hardhat-ignition-core/src/internal/execution/types/messages.ts index f66347e60fb..f4e33fbb9d8 100644 --- a/v-next/hardhat-ignition-core/src/internal/execution/types/messages.ts +++ b/v-next/hardhat-ignition-core/src/internal/execution/types/messages.ts @@ -37,6 +37,7 @@ export type JournalMessage = | ReadEventArgExecutionStateInitializeMessage | EncodeFunctionCallExecutionStateInitializeMessage | NetworkInteractionRequestMessage + | TransactionPrepareSendMessage | TransactionSendMessage | TransactionConfirmMessage | StaticCallCompleteMessage @@ -67,6 +68,7 @@ export enum JournalMessageType { READ_EVENT_ARGUMENT_EXECUTION_STATE_INITIALIZE = "READ_EVENT_ARGUMENT_EXECUTION_STATE_INITIALIZE", ENCODE_FUNCTION_CALL_EXECUTION_STATE_INITIALIZE = "ENCODE_FUNCTION_CALL_EXECUTION_STATE_INITIALIZE", NETWORK_INTERACTION_REQUEST = "NETWORK_INTERACTION_REQUEST", + TRANSACTION_PREPARE_SEND = "TRANSACTION_PREPARE_SEND", TRANSACTION_SEND = "TRANSACTION_SEND", TRANSACTION_CONFIRM = "TRANSACTION_CONFIRM", STATIC_CALL_COMPLETE = "STATIC_CALL_COMPLETE", @@ -207,6 +209,13 @@ export interface NetworkInteractionRequestMessage { | Omit, "result">; } +export interface TransactionPrepareSendMessage { + type: JournalMessageType.TRANSACTION_PREPARE_SEND; + futureId: string; + networkInteractionId: number; + nonce: number; +} + export interface TransactionSendMessage { type: JournalMessageType.TRANSACTION_SEND; futureId: string; diff --git a/v-next/hardhat-ignition-core/src/internal/execution/types/network-interaction.ts b/v-next/hardhat-ignition-core/src/internal/execution/types/network-interaction.ts index ea9ed4ac834..8f21eb5b7fc 100644 --- a/v-next/hardhat-ignition-core/src/internal/execution/types/network-interaction.ts +++ b/v-next/hardhat-ignition-core/src/internal/execution/types/network-interaction.ts @@ -28,6 +28,11 @@ export enum NetworkInteractionType { * All the transactions of an OnchainInteraction are sent with the same nonce, so that * only one of them can be confirmed. * + * The `nonce` field is only available if we have tried to send at least one transaction. + * + * Ideally, we should have sent it, and be tracking its progress. In practice, Ignition + * can fail when trying to send it, so we can have the nonce but no transaction. + * * The `nonce` field is only available if we have sent at least one transaction, and we * are tracking its progress. * diff --git a/v-next/hardhat-ignition-core/src/internal/journal/utils/emitExecutionEvent.ts b/v-next/hardhat-ignition-core/src/internal/journal/utils/emitExecutionEvent.ts index bc99813554c..d854530ae1a 100644 --- a/v-next/hardhat-ignition-core/src/internal/journal/utils/emitExecutionEvent.ts +++ b/v-next/hardhat-ignition-core/src/internal/journal/utils/emitExecutionEvent.ts @@ -142,6 +142,13 @@ export function emitExecutionEvent( }); break; } + case JournalMessageType.TRANSACTION_PREPARE_SEND: { + executionEventListener.transactionPrepareSend({ + type: ExecutionEventType.TRANSACTION_PREPARE_SEND, + futureId: message.futureId, + }); + break; + } case JournalMessageType.TRANSACTION_SEND: { executionEventListener.transactionSend({ type: ExecutionEventType.TRANSACTION_SEND, diff --git a/v-next/hardhat-ignition-core/src/internal/journal/utils/log.ts b/v-next/hardhat-ignition-core/src/internal/journal/utils/log.ts deleted file mode 100644 index ae224d15daa..00000000000 --- a/v-next/hardhat-ignition-core/src/internal/journal/utils/log.ts +++ /dev/null @@ -1,156 +0,0 @@ -import type { JournalMessage } from "../../execution/types/messages.js"; - -import { ExecutionResultType } from "../../execution/types/execution-result.js"; -import { JournalMessageType } from "../../execution/types/messages.js"; -import { NetworkInteractionType } from "../../execution/types/network-interaction.js"; -import { formatSolidityParameter } from "../../formatters.js"; - -export function logJournalableMessage(message: JournalMessage): void { - switch (message.type) { - case JournalMessageType.DEPLOYMENT_INITIALIZE: - console.log(`Deployment started`); - break; - - case JournalMessageType.WIPE_APPLY: { - console.log( - `Removing the execution of future ${message.futureId} from the journal`, - ); - } - case JournalMessageType.DEPLOYMENT_EXECUTION_STATE_INITIALIZE: - console.log( - `Starting to execute the deployment future ${message.futureId}`, - ); - break; - - case JournalMessageType.CALL_EXECUTION_STATE_INITIALIZE: - console.log(`Starting to execute the call future ${message.futureId}`); - break; - - case JournalMessageType.STATIC_CALL_EXECUTION_STATE_INITIALIZE: - console.log( - `Starting to execute the static call future ${message.futureId}`, - ); - break; - - case JournalMessageType.SEND_DATA_EXECUTION_STATE_INITIALIZE: - console.log( - `Started to execute the send data future ${message.futureId}`, - ); - break; - - case JournalMessageType.STATIC_CALL_EXECUTION_STATE_COMPLETE: - if (message.result.type === ExecutionResultType.SUCCESS) { - console.log( - `Successfully completed the execution of static call future ${ - message.futureId - } with result ${formatSolidityParameter(message.result.value)}`, - ); - } else { - console.log(`Execution of future ${message.futureId} failed`); - } - break; - - case JournalMessageType.DEPLOYMENT_EXECUTION_STATE_COMPLETE: - if (message.result.type === ExecutionResultType.SUCCESS) { - console.log( - `Successfully completed the execution of deployment future ${message.futureId} with result ${message.result.address}`, - ); - } else { - console.log(`Execution of future ${message.futureId} failed`); - } - break; - - case JournalMessageType.CALL_EXECUTION_STATE_COMPLETE: - if (message.result.type === ExecutionResultType.SUCCESS) { - console.log( - `Successfully completed the execution of call future ${message.futureId}`, - ); - } else { - console.log(`Execution of future ${message.futureId} failed`); - } - break; - - case JournalMessageType.SEND_DATA_EXECUTION_STATE_COMPLETE: - if (message.result.type === ExecutionResultType.SUCCESS) { - console.log( - `Successfully completed the execution of send data future ${message.futureId}`, - ); - } else { - console.log(`Execution of future ${message.futureId} failed`); - } - break; - - case JournalMessageType.CONTRACT_AT_EXECUTION_STATE_INITIALIZE: - console.log(`Executed contract at future ${message.futureId}`); - break; - - case JournalMessageType.READ_EVENT_ARGUMENT_EXECUTION_STATE_INITIALIZE: - console.log( - `Executed read event argument future ${ - message.futureId - } with result ${formatSolidityParameter(message.result)}`, - ); - break; - - case JournalMessageType.ENCODE_FUNCTION_CALL_EXECUTION_STATE_INITIALIZE: - console.log( - `Executed encode function call future ${message.futureId} with result ${message.result}`, - ); - break; - - case JournalMessageType.NETWORK_INTERACTION_REQUEST: - if ( - message.networkInteraction.type === - NetworkInteractionType.ONCHAIN_INTERACTION - ) { - console.log( - `New onchain interaction ${message.networkInteraction.id} requested for future ${message.futureId}`, - ); - } else { - console.log( - `New static call ${message.networkInteraction.id} requested for future ${message.futureId}`, - ); - } - break; - - case JournalMessageType.TRANSACTION_SEND: - console.log( - `Transaction ${message.transaction.hash} sent for onchain interaction ${message.networkInteractionId} of future ${message.futureId}`, - ); - break; - - case JournalMessageType.TRANSACTION_CONFIRM: - console.log(`Transaction ${message.hash} confirmed`); - break; - - case JournalMessageType.STATIC_CALL_COMPLETE: - console.log( - `Static call ${message.networkInteractionId} completed for future ${message.futureId}`, - ); - break; - - case JournalMessageType.ONCHAIN_INTERACTION_BUMP_FEES: - console.log( - `A transaction with higher fees will be sent for onchain interaction ${message.networkInteractionId} of future ${message.futureId}`, - ); - break; - - case JournalMessageType.ONCHAIN_INTERACTION_DROPPED: - console.log( - `Transactions for onchain interaction ${message.networkInteractionId} of future ${message.futureId} has been dropped and will be resent`, - ); - break; - - case JournalMessageType.ONCHAIN_INTERACTION_REPLACED_BY_USER: - console.log( - `Transactions for onchain interaction ${message.networkInteractionId} of future ${message.futureId} has been replaced by the user and the onchain interaction exection will start again`, - ); - break; - - case JournalMessageType.ONCHAIN_INTERACTION_TIMEOUT: - console.log( - `Onchain interaction ${message.networkInteractionId} of future ${message.futureId} failed due to being resent too many times and not having confirmed`, - ); - break; - } -} diff --git a/v-next/hardhat-ignition-core/src/internal/views/execution-state/get-network-execution-states.ts b/v-next/hardhat-ignition-core/src/internal/views/execution-state/get-network-execution-states.ts new file mode 100644 index 00000000000..33283bd2cdb --- /dev/null +++ b/v-next/hardhat-ignition-core/src/internal/views/execution-state/get-network-execution-states.ts @@ -0,0 +1,37 @@ +import { DeploymentState } from "../../execution/types/deployment-state.js"; +import { + CallExecutionState, + DeploymentExecutionState, + ExecutionSateType, + SendDataExecutionState, + StaticCallExecutionState, +} from "../../execution/types/execution-state.js"; + +export function getNetworkExecutionStates( + deploymentState: DeploymentState, +): Array< + | DeploymentExecutionState + | CallExecutionState + | SendDataExecutionState + | StaticCallExecutionState +> { + const exStates: Array< + | DeploymentExecutionState + | CallExecutionState + | SendDataExecutionState + | StaticCallExecutionState + > = []; + + for (const exState of Object.values(deploymentState.executionStates)) { + if ( + exState.type === ExecutionSateType.DEPLOYMENT_EXECUTION_STATE || + exState.type === ExecutionSateType.CALL_EXECUTION_STATE || + exState.type === ExecutionSateType.SEND_DATA_EXECUTION_STATE || + exState.type === ExecutionSateType.STATIC_CALL_EXECUTION_STATE + ) { + exStates.push(exState); + } + } + + return exStates; +} diff --git a/v-next/hardhat-ignition-core/src/track-transaction.ts b/v-next/hardhat-ignition-core/src/track-transaction.ts new file mode 100644 index 00000000000..93b5f1e33cb --- /dev/null +++ b/v-next/hardhat-ignition-core/src/track-transaction.ts @@ -0,0 +1,263 @@ +import { HardhatError } from "@nomicfoundation/hardhat-errors"; +import { exists } from "@nomicfoundation/hardhat-utils/fs"; + +import { defaultConfig } from "./internal/defaultConfig.js"; +import { FileDeploymentLoader } from "./internal/deployment-loader/file-deployment-loader.js"; +import { + applyNewMessage, + loadDeploymentState, +} from "./internal/execution/deployment-state-helpers.js"; +import { EIP1193JsonRpcClient } from "./internal/execution/jsonrpc-client.js"; +import { DeploymentState } from "./internal/execution/types/deployment-state.js"; +import { + CallExecutionState, + DeploymentExecutionState, + SendDataExecutionState, + StaticCallExecutionState, +} from "./internal/execution/types/execution-state.js"; +import { + FullTransaction, + NetworkFees, +} from "./internal/execution/types/jsonrpc.js"; +import { + JournalMessageType, + OnchainInteractionReplacedByUserMessage, + TransactionSendMessage, +} from "./internal/execution/types/messages.js"; +import { + NetworkInteractionType, + OnchainInteraction, +} from "./internal/execution/types/network-interaction.js"; +import { assertIgnitionInvariant } from "./internal/utils/assertions.js"; +import { getNetworkExecutionStates } from "./internal/views/execution-state/get-network-execution-states.js"; +import { EIP1193Provider } from "./types/provider.js"; + +/** + * Tracks a transaction associated with a given deployment. + * + * @param deploymentDir - the directory of the deployment the transaction belongs to + * @param txHash - the hash of the transaction to track + * @param provider - a JSON RPC provider to retrieve transaction information from + * @param requiredConfirmations - the number of confirmations required for the transaction to be considered confirmed + * @param applyNewMessageFn - only used for ease of testing this function and should not be used otherwise + * + * @beta + */ +export async function trackTransaction( + deploymentDir: string, + txHash: string, + provider: EIP1193Provider, + requiredConfirmations: number = defaultConfig.requiredConfirmations, + applyNewMessageFn: ( + message: any, + _a: any, + _b: any, + ) => Promise = applyNewMessage, +): Promise { + if (!(await exists(deploymentDir))) { + throw new HardhatError( + HardhatError.ERRORS.IGNITION.TRACK_TRANSACTIONS.DEPLOYMENT_DIR_NOT_FOUND, + { + deploymentDir, + }, + ); + } + const deploymentLoader = new FileDeploymentLoader(deploymentDir); + + const deploymentState = await loadDeploymentState(deploymentLoader); + + if (deploymentState === undefined) { + throw new HardhatError( + HardhatError.ERRORS.IGNITION.TRACK_TRANSACTIONS.UNINITIALIZED_DEPLOYMENT, + { + deploymentDir, + }, + ); + } + + const jsonRpcClient = new EIP1193JsonRpcClient(provider); + + const transaction = await jsonRpcClient.getFullTransaction(txHash); + + if (transaction === undefined) { + throw new HardhatError( + HardhatError.ERRORS.IGNITION.TRACK_TRANSACTIONS.TRANSACTION_NOT_FOUND, + { + txHash, + }, + ); + } + + const exStates = getNetworkExecutionStates(deploymentState); + + /** + * Cases to consider: + * 1. (happy case) given txhash matches a nonce we prepared but didn't record sending + * 2. (user replaced with different tx) given txhash matches a nonce we prepared but didn't record sending, + * but the tx details are different + * 3. (user sent known txhash) given txhash matches a nonce we recorded sending with the same txhash + * 4. (user sent unknown txhash) given txhash matches a nonce we recorded sending but with a different txhash + * 5. (user sent unrelated txhash) given txhash doesn't match any nonce we've allocated + */ + for (const exState of exStates) { + for (const networkInteraction of exState.networkInteractions) { + if ( + networkInteraction.type === + NetworkInteractionType.ONCHAIN_INTERACTION && + exState.from.toLowerCase() === transaction.from.toLowerCase() && + networkInteraction.nonce === transaction.nonce + ) { + if (networkInteraction.transactions.length === 0) { + // case 1: the txHash matches a transaction we appear to have sent + if ( + networkInteraction.to?.toLowerCase() === + transaction.to?.toLowerCase() && + networkInteraction.data === transaction.data && + networkInteraction.value === transaction.value + ) { + let fees: NetworkFees; + if ( + "maxFeePerGas" in transaction && + "maxPriorityFeePerGas" in transaction && + transaction.maxFeePerGas !== undefined && + transaction.maxPriorityFeePerGas !== undefined + ) { + fees = { + maxFeePerGas: transaction.maxFeePerGas, + maxPriorityFeePerGas: transaction.maxPriorityFeePerGas, + }; + } else { + assertIgnitionInvariant( + "gasPrice" in transaction && transaction.gasPrice !== undefined, + "Transaction fees are missing", + ); + + fees = { + gasPrice: transaction.gasPrice, + }; + } + + const transactionSendMessage: TransactionSendMessage = { + futureId: exState.id, + networkInteractionId: networkInteraction.id, + nonce: networkInteraction.nonce, + type: JournalMessageType.TRANSACTION_SEND, + transaction: { + hash: transaction.hash, + fees, + }, + }; + + await applyNewMessageFn( + transactionSendMessage, + deploymentState, + deploymentLoader, + ); + + return; + } + // case 2: the user sent a different transaction that replaced ours + // so we check their transaction for the required number of confirmations + else { + return checkConfirmations( + exState, + networkInteraction, + transaction, + requiredConfirmations, + jsonRpcClient, + deploymentState, + deploymentLoader, + applyNewMessageFn, + ); + } + } + // case: the user gave us a transaction that matches a nonce we've already recorded sending from + else { + // case 3: the txHash matches the one we have saved in the journal for the same nonce + if (networkInteraction.transactions[0].hash === transaction.hash) { + throw new HardhatError( + HardhatError.ERRORS.IGNITION.TRACK_TRANSACTIONS.KNOWN_TRANSACTION, + ); + } + + // case 4: the user sent a different transaction that replaced ours + // so we check their transaction for the required number of confirmations + return checkConfirmations( + exState, + networkInteraction, + transaction, + requiredConfirmations, + jsonRpcClient, + deploymentState, + deploymentLoader, + applyNewMessageFn, + ); + } + } + } + } + + // case 5: the txHash doesn't match any nonce we've allocated + throw new HardhatError( + HardhatError.ERRORS.IGNITION.TRACK_TRANSACTIONS.MATCHING_NONCE_NOT_FOUND, + ); +} + +async function checkConfirmations( + exState: + | DeploymentExecutionState + | CallExecutionState + | StaticCallExecutionState + | SendDataExecutionState, + networkInteraction: OnchainInteraction, + transaction: FullTransaction, + requiredConfirmations: number, + jsonRpcClient: EIP1193JsonRpcClient, + deploymentState: DeploymentState, + deploymentLoader: FileDeploymentLoader, + applyNewMessageFn: (message: any, _a: any, _b: any) => Promise, +) { + const [block, receipt] = await Promise.all([ + jsonRpcClient.getLatestBlock(), + jsonRpcClient.getTransactionReceipt(transaction.hash), + ]); + + assertIgnitionInvariant( + receipt !== undefined, + "Unable to retrieve transaction receipt", + ); + + const confirmations = block.number - receipt.blockNumber + 1; + + if (confirmations >= requiredConfirmations) { + const transactionReplacedMessage: OnchainInteractionReplacedByUserMessage = + { + futureId: exState.id, + networkInteractionId: networkInteraction.id, + type: JournalMessageType.ONCHAIN_INTERACTION_REPLACED_BY_USER, + }; + + await applyNewMessageFn( + transactionReplacedMessage, + deploymentState, + deploymentLoader, + ); + + /** + * We tell the user specifically what future will be executed upon re-running the deployment + * in case the replacement transaction sent by the user was the same transaction that we were going to send. + * + * i.e., if the broken transaction was for a future sending 100 ETH to an address, and the user decided to just send it + * themselves after the deployment failed, we tell them that the future sending 100 ETH will be executed upon re-running + * the deployment. It is not obvious to the user that that is the case, and it could result in a double send if they assume + * the opposite. + */ + return `Your deployment has been fixed and will continue with the execution of the "${exState.id}" future. + +If this is not the expected behavior, please edit your Hardhat Ignition module accordingly before re-running your deployment.`; + } else { + throw new HardhatError( + HardhatError.ERRORS.IGNITION.TRACK_TRANSACTIONS.INSUFFICIENT_CONFIRMATIONS, + ); + } +} diff --git a/v-next/hardhat-ignition-core/src/types/execution-events.ts b/v-next/hardhat-ignition-core/src/types/execution-events.ts index ad1d7ccc764..3d16495512d 100644 --- a/v-next/hardhat-ignition-core/src/types/execution-events.ts +++ b/v-next/hardhat-ignition-core/src/types/execution-events.ts @@ -21,6 +21,7 @@ export type ExecutionEvent = | ReadEventArgExecutionStateInitializeEvent | EncodeFunctionCallExecutionStateInitializeEvent | NetworkInteractionRequestEvent + | TransactionPrepareSendEvent | TransactionSendEvent | TransactionConfirmEvent | StaticCallCompleteEvent @@ -54,6 +55,7 @@ export enum ExecutionEventType { READ_EVENT_ARGUMENT_EXECUTION_STATE_INITIALIZE = "READ_EVENT_ARGUMENT_EXECUTION_STATE_INITIALIZE", ENCODE_FUNCTION_CALL_EXECUTION_STATE_INITIALIZE = "ENCODE_FUNCTION_CALL_EXECUTION_STATE_INITIALIZE", NETWORK_INTERACTION_REQUEST = "NETWORK_INTERACTION_REQUEST", + TRANSACTION_PREPARE_SEND = "TRANSACTION_PREPARE_SEND", TRANSACTION_SEND = "TRANSACTION_SEND", TRANSACTION_CONFIRM = "TRANSACTION_CONFIRM", STATIC_CALL_COMPLETE = "STATIC_CALL_COMPLETE", @@ -297,6 +299,16 @@ export interface NetworkInteractionRequestEvent { futureId: string; } +/** + * An event indicating that a transaction is about to be sent to the network. + * + * @beta + */ +export interface TransactionPrepareSendEvent { + type: ExecutionEventType.TRANSACTION_PREPARE_SEND; + futureId: string; +} + /** * An event indicating that a transaction has been sent to the network. * @@ -475,6 +487,7 @@ export interface ExecutionEventTypeMap { [ExecutionEventType.READ_EVENT_ARGUMENT_EXECUTION_STATE_INITIALIZE]: ReadEventArgExecutionStateInitializeEvent; [ExecutionEventType.ENCODE_FUNCTION_CALL_EXECUTION_STATE_INITIALIZE]: EncodeFunctionCallExecutionStateInitializeEvent; [ExecutionEventType.NETWORK_INTERACTION_REQUEST]: NetworkInteractionRequestEvent; + [ExecutionEventType.TRANSACTION_PREPARE_SEND]: TransactionPrepareSendEvent; [ExecutionEventType.TRANSACTION_SEND]: TransactionSendEvent; [ExecutionEventType.TRANSACTION_CONFIRM]: TransactionConfirmEvent; [ExecutionEventType.STATIC_CALL_COMPLETE]: StaticCallCompleteEvent; diff --git a/v-next/hardhat-ignition-core/test/execution/execution-engine.ts b/v-next/hardhat-ignition-core/test/execution/execution-engine.ts new file mode 100644 index 00000000000..0135b49ae0a --- /dev/null +++ b/v-next/hardhat-ignition-core/test/execution/execution-engine.ts @@ -0,0 +1,49 @@ +import { HardhatError } from "@nomicfoundation/hardhat-errors"; +import { assertRejectsWithHardhatError } from "@nomicfoundation/hardhat-test-utils"; +import { assert } from "chai"; +import path from "path"; + +import { ExecutionEngine } from "../../src/internal/execution/execution-engine.js"; +import { FileDeploymentLoader } from "../../src/internal/deployment-loader/file-deployment-loader.js"; +import { loadDeploymentState } from "../../src/internal/execution/deployment-state-helpers.js"; +import { fileURLToPath } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +describe("ExecutionEngine", () => { + describe("_checkForMissingTransactions", () => { + it("should throw if there are PREPARE_SEND_TRANSACTION messages without a corresponding SEND_TRANSACTION message", async () => { + const deploymentLoader = new FileDeploymentLoader( + path.resolve(__dirname, "../mocks/trackTransaction/success"), + ); + + // the only thing the function we are testing requires is a deploymentLoader + const engine = new ExecutionEngine( + deploymentLoader, + {} as any, + {} as any, + {} as any, + {} as any, + 5, + 5, + 5, + 5, + false, + ); + + const deploymentState = await loadDeploymentState(deploymentLoader); + + assert(deploymentState !== undefined, "deploymentState is undefined"); + + await assertRejectsWithHardhatError( + engine.executeModule(deploymentState, {} as any, [], [], {}, "0x"), + HardhatError.ERRORS.IGNITION.EXECUTION.TRANSACTION_LOST, + { + futureId: "LockModule#Lock", + nonce: 1, + sender: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + }, + ); + }); + }); +}); diff --git a/v-next/hardhat-ignition-core/test/execution/future-processor/helpers/network-interaction-execution.ts b/v-next/hardhat-ignition-core/test/execution/future-processor/helpers/network-interaction-execution.ts index 67c610ccc33..9265f248214 100644 --- a/v-next/hardhat-ignition-core/test/execution/future-processor/helpers/network-interaction-execution.ts +++ b/v-next/hardhat-ignition-core/test/execution/future-processor/helpers/network-interaction-execution.ts @@ -41,7 +41,10 @@ import { TransactionReceipt, TransactionReceiptStatus, } from "../../../../src/internal/execution/types/jsonrpc.js"; -import { JournalMessageType } from "../../../../src/internal/execution/types/messages.js"; +import { + JournalMessage, + JournalMessageType, +} from "../../../../src/internal/execution/types/messages.js"; import { NetworkInteractionType, OnchainInteraction, @@ -49,6 +52,7 @@ import { } from "../../../../src/internal/execution/types/network-interaction.js"; import { FutureType } from "../../../../src/types/module.js"; import { exampleAccounts } from "../../../helpers.js"; +import { DeploymentLoader } from "../../../../src/internal/deployment-loader/types.js"; class StubJsonRpcClient implements JsonRpcClient { public async getChainId(): Promise { @@ -124,6 +128,49 @@ class StubJsonRpcClient implements JsonRpcClient { } } +class StubDeploymentLoader implements DeploymentLoader { + public async recordToJournal(_message: JournalMessage): Promise { + throw new Error("Method not implemented."); + } + + public async *readFromJournal(): AsyncGenerator { + throw new Error("Method not implemented."); + } + + public async loadArtifact(_artifactId: string): Promise { + throw new Error("Method not implemented."); + } + + public async storeUserProvidedArtifact( + _futureId: string, + _artifact: any, + ): Promise { + throw new Error("Method not implemented."); + } + + public async storeNamedArtifact( + _futureId: string, + _contractName: string, + _artifact: any, + ): Promise { + throw new Error("Method not implemented."); + } + + public async storeBuildInfo( + _futureId: string, + _buildInfo: any, + ): Promise { + throw new Error("Method not implemented."); + } + + public async recordDeployedAddress( + _futureId: string, + _contractAddress: string, + ): Promise { + throw new Error("Method not implemented."); + } +} + describe("Network interactions", () => { describe("runStaticCall", () => { it("Should run the static call as latest and return the result", async () => { @@ -351,6 +398,16 @@ describe("Network interactions", () => { } } + class MockDeploymentLoader extends StubDeploymentLoader { + public message: JournalMessage | undefined; + + public override async recordToJournal( + _message: JournalMessage, + ): Promise { + this.message = _message; + } + } + it("Should use the recommended network fees", async () => { class LocalMockJsonRpcClient extends MockJsonRpcClient { public storedFees: EIP1559NetworkFees = {} as EIP1559NetworkFees; @@ -372,6 +429,7 @@ describe("Network interactions", () => { const client = new LocalMockJsonRpcClient(); const nonceManager = new MockNonceManager(); + const deploymentLoader = new MockDeploymentLoader(); const onchainInteraction: OnchainInteraction = { to: exampleAccounts[1], @@ -389,6 +447,8 @@ describe("Network interactions", () => { onchainInteraction, nonceManager, async () => undefined, + deploymentLoader, + "test", ); assert.equal(client.storedFees.maxFeePerGas, 100n); @@ -399,6 +459,7 @@ describe("Network interactions", () => { it("Should allocate a nonce when the onchainInteraction doesn't have one", async () => { const client = new MockJsonRpcClient(); const nonceManager = new MockNonceManager(); + const deploymentLoader = new MockDeploymentLoader(); const onchainInteraction: OnchainInteraction = { to: exampleAccounts[1], @@ -416,6 +477,8 @@ describe("Network interactions", () => { onchainInteraction, nonceManager, async () => undefined, + deploymentLoader, + "test", ); assert.equal(nonceManager.calls[exampleAccounts[0]], 1); @@ -435,6 +498,7 @@ describe("Network interactions", () => { const client = new LocalMockJsonRpcClient(); const nonceManager = new MockNonceManager(); + const deploymentLoader = new MockDeploymentLoader(); const onchainInteraction: OnchainInteraction = { to: exampleAccounts[1], @@ -453,6 +517,8 @@ describe("Network interactions", () => { onchainInteraction, nonceManager, async () => undefined, + deploymentLoader, + "test", ); assert.equal(nonceManager.calls[exampleAccounts[0]], undefined); @@ -478,6 +544,7 @@ describe("Network interactions", () => { const client = new LocalMockJsonRpcClient(); const nonceManager = new MockNonceManager(); + const deploymentLoader = new MockDeploymentLoader(); const onchainInteraction: OnchainInteraction = { to: exampleAccounts[1], @@ -513,6 +580,8 @@ describe("Network interactions", () => { onchainInteraction, nonceManager, decodeSimulationResult(mockStrategyGenerator, mockExecutionState), + deploymentLoader, + "test", ); // type casting @@ -528,9 +597,10 @@ describe("Network interactions", () => { }); describe("When the simulation succeeds", () => { - it("Should send the transaction and return its hash and nonce", async () => { + it("Should write a TRANSACTION_PREPARE_SEND message to the journal, then send the transaction and return its hash and nonce", async () => { const client = new MockJsonRpcClient(); const nonceManager = new MockNonceManager(); + const deploymentLoader = new MockDeploymentLoader(); const onchainInteraction: OnchainInteraction = { to: exampleAccounts[1], @@ -548,6 +618,8 @@ describe("Network interactions", () => { onchainInteraction, nonceManager, async () => undefined, + deploymentLoader, + "test", ); // type casting @@ -555,6 +627,10 @@ describe("Network interactions", () => { return assert.fail("Unexpected result type"); } + assert.equal( + deploymentLoader.message?.type, + JournalMessageType.TRANSACTION_PREPARE_SEND, + ); assert.equal(result.nonce, 0); assert.equal(result.transaction.hash, "0x1234"); }); @@ -592,6 +668,7 @@ describe("Network interactions", () => { it("Should return the decoded simulation error", async () => { const client = new LocalMockJsonRpcClient(); const nonceManager = new MockNonceManager(); + const deploymentLoader = new MockDeploymentLoader(); const onchainInteraction: OnchainInteraction = { to: exampleAccounts[1], @@ -627,6 +704,8 @@ describe("Network interactions", () => { onchainInteraction, nonceManager, decodeSimulationResult(mockStrategyGenerator, mockExecutionState), + deploymentLoader, + "test", ); // type casting @@ -648,6 +727,7 @@ describe("Network interactions", () => { "insufficient funds for transfer", ); const nonceManager = new MockNonceManager(); + const deploymentLoader = new MockDeploymentLoader(); const onchainInteraction: OnchainInteraction = { to: exampleAccounts[1], @@ -666,6 +746,8 @@ describe("Network interactions", () => { onchainInteraction, nonceManager, async () => undefined, + deploymentLoader, + "test", ), HardhatError.ERRORS.IGNITION.EXECUTION .INSUFFICIENT_FUNDS_FOR_TRANSFER, @@ -683,6 +765,7 @@ describe("Network interactions", () => { "contract creation code storage out of gas", ); const nonceManager = new MockNonceManager(); + const deploymentLoader = new MockDeploymentLoader(); const onchainInteraction: OnchainInteraction = { to: exampleAccounts[1], @@ -701,6 +784,8 @@ describe("Network interactions", () => { onchainInteraction, nonceManager, async () => undefined, + deploymentLoader, + "test", ), HardhatError.ERRORS.IGNITION.EXECUTION .INSUFFICIENT_FUNDS_FOR_DEPLOY, @@ -715,6 +800,7 @@ describe("Network interactions", () => { it("Should throw an error", async () => { const client = new LocalMockJsonRpcClient("unknown error"); const nonceManager = new MockNonceManager(); + const deploymentLoader = new MockDeploymentLoader(); const onchainInteraction: OnchainInteraction = { to: exampleAccounts[1], @@ -733,6 +819,8 @@ describe("Network interactions", () => { onchainInteraction, nonceManager, async () => undefined, + deploymentLoader, + "test", ), HardhatError.ERRORS.IGNITION.EXECUTION.GAS_ESTIMATION_FAILED, { diff --git a/v-next/hardhat-ignition-core/test/mocks/trackTransaction/known-tx/journal.jsonl b/v-next/hardhat-ignition-core/test/mocks/trackTransaction/known-tx/journal.jsonl new file mode 100644 index 00000000000..6b064d4d635 --- /dev/null +++ b/v-next/hardhat-ignition-core/test/mocks/trackTransaction/known-tx/journal.jsonl @@ -0,0 +1,6 @@ + +{"chainId":31337,"type":"DEPLOYMENT_INITIALIZE"} +{"artifactId":"LockModule#Lock","constructorArgs":[1987909200],"contractName":"Lock","dependencies":[],"from":"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266","futureId":"LockModule#Lock","futureType":"NAMED_ARTIFACT_CONTRACT_DEPLOYMENT","libraries":{},"strategy":"basic","strategyConfig":{},"type":"DEPLOYMENT_EXECUTION_STATE_INITIALIZE","value":{"_kind":"bigint","value":"1000000000"}} +{"futureId":"LockModule#Lock","networkInteraction":{"data":"0x60806040526040516105d83803806105d8833981810160405281019061002591906100f0565b804210610067576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161005e906101a0565b60405180910390fd5b8060008190555033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550506101c0565b600080fd5b6000819050919050565b6100cd816100ba565b81146100d857600080fd5b50565b6000815190506100ea816100c4565b92915050565b600060208284031215610106576101056100b5565b5b6000610114848285016100db565b91505092915050565b600082825260208201905092915050565b7f556e6c6f636b2074696d652073686f756c6420626520696e207468652066757460008201527f7572650000000000000000000000000000000000000000000000000000000000602082015250565b600061018a60238361011d565b91506101958261012e565b604082019050919050565b600060208201905081810360008301526101b98161017d565b9050919050565b610409806101cf6000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c8063251c1aa3146100465780633ccfd60b146100645780638da5cb5b1461006e575b600080fd5b61004e61008c565b60405161005b919061024a565b60405180910390f35b61006c610092565b005b61007661020b565b60405161008391906102a6565b60405180910390f35b60005481565b6000544210156100d7576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016100ce9061031e565b60405180910390fd5b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610167576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161015e9061038a565b60405180910390fd5b7fbf2ed60bd5b5965d685680c01195c9514e4382e28e3a5a2d2d5244bf59411b9347426040516101989291906103aa565b60405180910390a1600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166108fc479081150290604051600060405180830381858888f19350505050158015610208573d6000803e3d6000fd5b50565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000819050919050565b61024481610231565b82525050565b600060208201905061025f600083018461023b565b92915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061029082610265565b9050919050565b6102a081610285565b82525050565b60006020820190506102bb6000830184610297565b92915050565b600082825260208201905092915050565b7f596f752063616e27742077697468647261772079657400000000000000000000600082015250565b60006103086016836102c1565b9150610313826102d2565b602082019050919050565b60006020820190508181036000830152610337816102fb565b9050919050565b7f596f75206172656e277420746865206f776e6572000000000000000000000000600082015250565b60006103746014836102c1565b915061037f8261033e565b602082019050919050565b600060208201905081810360008301526103a381610367565b9050919050565b60006040820190506103bf600083018561023b565b6103cc602083018461023b565b939250505056fea2646970667358221220f92f73d2a3284a3c1cca55a0fe6ec1a91b13bec884aecdbcf644cebf2774f32f64736f6c6343000813003300000000000000000000000000000000000000000000000000000000767d1650","id":1,"type":"ONCHAIN_INTERACTION","value":{"_kind":"bigint","value":"1000000000"}},"type":"NETWORK_INTERACTION_REQUEST"} +{"futureId":"LockModule#Lock","networkInteractionId":1,"nonce":1,"type":"TRANSACTION_PREPARE_SEND"} +{"futureId":"LockModule#Lock","networkInteractionId":1,"nonce":1,"transaction":{"fees":{"maxFeePerGas":{"_kind":"bigint","value":"2750000000"},"maxPriorityFeePerGas":{"_kind":"bigint","value":"1000000000"}},"hash":"0x1a3eb512e21fc849f8e8733b250ce49b61178c9c4a670063f969db59eda4a59f"},"type":"TRANSACTION_SEND"} \ No newline at end of file diff --git a/v-next/hardhat-ignition-core/test/mocks/trackTransaction/success/journal.jsonl b/v-next/hardhat-ignition-core/test/mocks/trackTransaction/success/journal.jsonl new file mode 100644 index 00000000000..35a089d30d4 --- /dev/null +++ b/v-next/hardhat-ignition-core/test/mocks/trackTransaction/success/journal.jsonl @@ -0,0 +1,5 @@ + +{"chainId":31337,"type":"DEPLOYMENT_INITIALIZE"} +{"artifactId":"LockModule#Lock","constructorArgs":[1987909200],"contractName":"Lock","dependencies":[],"from":"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266","futureId":"LockModule#Lock","futureType":"NAMED_ARTIFACT_CONTRACT_DEPLOYMENT","libraries":{},"strategy":"basic","strategyConfig":{},"type":"DEPLOYMENT_EXECUTION_STATE_INITIALIZE","value":{"_kind":"bigint","value":"1000000000"}} +{"futureId":"LockModule#Lock","networkInteraction":{"data":"0x60806040526040516105d83803806105d8833981810160405281019061002591906100f0565b804210610067576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161005e906101a0565b60405180910390fd5b8060008190555033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550506101c0565b600080fd5b6000819050919050565b6100cd816100ba565b81146100d857600080fd5b50565b6000815190506100ea816100c4565b92915050565b600060208284031215610106576101056100b5565b5b6000610114848285016100db565b91505092915050565b600082825260208201905092915050565b7f556e6c6f636b2074696d652073686f756c6420626520696e207468652066757460008201527f7572650000000000000000000000000000000000000000000000000000000000602082015250565b600061018a60238361011d565b91506101958261012e565b604082019050919050565b600060208201905081810360008301526101b98161017d565b9050919050565b610409806101cf6000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c8063251c1aa3146100465780633ccfd60b146100645780638da5cb5b1461006e575b600080fd5b61004e61008c565b60405161005b919061024a565b60405180910390f35b61006c610092565b005b61007661020b565b60405161008391906102a6565b60405180910390f35b60005481565b6000544210156100d7576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016100ce9061031e565b60405180910390fd5b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610167576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161015e9061038a565b60405180910390fd5b7fbf2ed60bd5b5965d685680c01195c9514e4382e28e3a5a2d2d5244bf59411b9347426040516101989291906103aa565b60405180910390a1600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166108fc479081150290604051600060405180830381858888f19350505050158015610208573d6000803e3d6000fd5b50565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000819050919050565b61024481610231565b82525050565b600060208201905061025f600083018461023b565b92915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061029082610265565b9050919050565b6102a081610285565b82525050565b60006020820190506102bb6000830184610297565b92915050565b600082825260208201905092915050565b7f596f752063616e27742077697468647261772079657400000000000000000000600082015250565b60006103086016836102c1565b9150610313826102d2565b602082019050919050565b60006020820190508181036000830152610337816102fb565b9050919050565b7f596f75206172656e277420746865206f776e6572000000000000000000000000600082015250565b60006103746014836102c1565b915061037f8261033e565b602082019050919050565b600060208201905081810360008301526103a381610367565b9050919050565b60006040820190506103bf600083018561023b565b6103cc602083018461023b565b939250505056fea2646970667358221220f92f73d2a3284a3c1cca55a0fe6ec1a91b13bec884aecdbcf644cebf2774f32f64736f6c6343000813003300000000000000000000000000000000000000000000000000000000767d1650","id":1,"type":"ONCHAIN_INTERACTION","value":{"_kind":"bigint","value":"1000000000"}},"type":"NETWORK_INTERACTION_REQUEST"} +{"futureId":"LockModule#Lock","networkInteractionId":1,"nonce":1,"type":"TRANSACTION_PREPARE_SEND"} \ No newline at end of file diff --git a/v-next/hardhat-ignition-core/test/mocks/trackTransaction/uninitialized/blank b/v-next/hardhat-ignition-core/test/mocks/trackTransaction/uninitialized/blank new file mode 100644 index 00000000000..6201f42796f --- /dev/null +++ b/v-next/hardhat-ignition-core/test/mocks/trackTransaction/uninitialized/blank @@ -0,0 +1 @@ +just so github commits the folder \ No newline at end of file diff --git a/v-next/hardhat-ignition-core/test/track-transaction.ts b/v-next/hardhat-ignition-core/test/track-transaction.ts new file mode 100644 index 00000000000..468234c1e74 --- /dev/null +++ b/v-next/hardhat-ignition-core/test/track-transaction.ts @@ -0,0 +1,302 @@ +import { assert } from "chai"; +import path from "path"; + +import { + EIP1193Provider, + RequestArguments, + trackTransaction, +} from "../src/index.js"; +import { NetworkTransaction } from "../src/internal/execution/types/jsonrpc.js"; +import { JournalMessageType } from "../src/internal/execution/types/messages.js"; +import { fileURLToPath } from "url"; +import { assertRejectsWithHardhatError } from "@nomicfoundation/hardhat-test-utils"; +import { HardhatError } from "@nomicfoundation/hardhat-errors"; + +const mockFullTx = { + hash: "0x1a3eb512e21fc849f8e8733b250ce49b61178c9c4a670063f969db59eda4a59f", + input: + "0x60806040526040516105d83803806105d8833981810160405281019061002591906100f0565b804210610067576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161005e906101a0565b60405180910390fd5b8060008190555033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550506101c0565b600080fd5b6000819050919050565b6100cd816100ba565b81146100d857600080fd5b50565b6000815190506100ea816100c4565b92915050565b600060208284031215610106576101056100b5565b5b6000610114848285016100db565b91505092915050565b600082825260208201905092915050565b7f556e6c6f636b2074696d652073686f756c6420626520696e207468652066757460008201527f7572650000000000000000000000000000000000000000000000000000000000602082015250565b600061018a60238361011d565b91506101958261012e565b604082019050919050565b600060208201905081810360008301526101b98161017d565b9050919050565b610409806101cf6000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c8063251c1aa3146100465780633ccfd60b146100645780638da5cb5b1461006e575b600080fd5b61004e61008c565b60405161005b919061024a565b60405180910390f35b61006c610092565b005b61007661020b565b60405161008391906102a6565b60405180910390f35b60005481565b6000544210156100d7576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016100ce9061031e565b60405180910390fd5b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610167576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161015e9061038a565b60405180910390fd5b7fbf2ed60bd5b5965d685680c01195c9514e4382e28e3a5a2d2d5244bf59411b9347426040516101989291906103aa565b60405180910390a1600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166108fc479081150290604051600060405180830381858888f19350505050158015610208573d6000803e3d6000fd5b50565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000819050919050565b61024481610231565b82525050565b600060208201905061025f600083018461023b565b92915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061029082610265565b9050919050565b6102a081610285565b82525050565b60006020820190506102bb6000830184610297565b92915050565b600082825260208201905092915050565b7f596f752063616e27742077697468647261772079657400000000000000000000600082015250565b60006103086016836102c1565b9150610313826102d2565b602082019050919050565b60006020820190508181036000830152610337816102fb565b9050919050565b7f596f75206172656e277420746865206f776e6572000000000000000000000000600082015250565b60006103746014836102c1565b915061037f8261033e565b602082019050919050565b600060208201905081810360008301526103a381610367565b9050919050565b60006040820190506103bf600083018561023b565b6103cc602083018461023b565b939250505056fea2646970667358221220f92f73d2a3284a3c1cca55a0fe6ec1a91b13bec884aecdbcf644cebf2774f32f64736f6c6343000813003300000000000000000000000000000000000000000000000000000000767d1650", + from: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + to: null, + chainId: "0x7A69", + value: "1000000000", + nonce: "0x1", + blockHash: + "0xc03bdad45bf3997457e33261cfc85e2ee45380706685468006c5b37e273a52f0", + blockNumber: "0x2", + maxFeePerGas: "0xA3E9AB80", + maxPriorityFeePerGas: "0x3B9ACA00", + gas: "0x4F9E0", +}; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +class MockEIP1193Provider implements EIP1193Provider { + constructor( + public fullTx: NetworkTransaction | null = null, + public confirmations: number = 6, + ) {} + + public async request(args: RequestArguments): Promise { + if (args.method === "eth_getTransactionByHash") { + return this.fullTx; + } + + if (args.method === "eth_getBlockByNumber") { + return { + number: `0x${this.confirmations.toString(16)}`, + hash: "0x1234", + }; + } + + if (args.method === "eth_getTransactionReceipt") { + return { + blockNumber: "0x1", + blockHash: "0x1234", + status: "0x1", + contractAddress: null, + logs: [], + }; + } + + throw new Error("Method not implemented"); + } +} + +describe("trackTransaction", () => { + it("(with TX_SEND in journal) should apply an ONCHAIN_INTERACTION_REPLACED_BY_USER message to the journal if the user replaced our transaction and their transaction has enough confirmations", async () => { + const deploymentDir = path.resolve( + __dirname, + "./mocks/trackTransaction/known-tx", + ); + + const hash = "0x1234"; + + let message: any; + + async function applyMessageFn( + msg: any, + deploymentState: any, + _deploymentLoader: any, + ) { + message = msg; + return deploymentState; + } + + const result = await trackTransaction( + deploymentDir, + hash, + new MockEIP1193Provider({ ...mockFullTx, hash }, 8), + undefined, + applyMessageFn, + ); + + assert.deepEqual(message, { + type: JournalMessageType.ONCHAIN_INTERACTION_REPLACED_BY_USER, + futureId: "LockModule#Lock", + networkInteractionId: 1, + }); + assert.equal( + result, + `Your deployment has been fixed and will continue with the execution of the "LockModule#Lock" future. + +If this is not the expected behavior, please edit your Hardhat Ignition module accordingly before re-running your deployment.`, + ); + }); + + it("(without TX_SEND in journal) should apply an ONCHAIN_INTERACTION_REPLACED_BY_USER message to the journal if the user replaced our transaction and their transaction has enough confirmations", async () => { + const deploymentDir = path.resolve( + __dirname, + "./mocks/trackTransaction/success", + ); + + let message: any; + + async function applyMessageFn( + msg: any, + deploymentState: any, + _deploymentLoader: any, + ) { + message = msg; + return deploymentState; + } + + const result = await trackTransaction( + deploymentDir, + mockFullTx.hash, + new MockEIP1193Provider({ ...mockFullTx, value: "0x1111" }, 8), + undefined, + applyMessageFn, + ); + + assert.deepEqual(message, { + type: JournalMessageType.ONCHAIN_INTERACTION_REPLACED_BY_USER, + futureId: "LockModule#Lock", + networkInteractionId: 1, + }); + assert.equal( + result, + `Your deployment has been fixed and will continue with the execution of the "LockModule#Lock" future. + +If this is not the expected behavior, please edit your Hardhat Ignition module accordingly before re-running your deployment.`, + ); + }); + + it("should apply a TRANSACTION_SEND message to the journal if the user's txHash matches our prepareSendMessage perfectly", async () => { + const deploymentDir = path.resolve( + __dirname, + "./mocks/trackTransaction/success", + ); + + let message: any; + + async function applyMessageFn( + msg: any, + deploymentState: any, + _deploymentLoader: any, + ) { + message = msg; + return deploymentState; + } + + await trackTransaction( + deploymentDir, + mockFullTx.hash, + new MockEIP1193Provider(mockFullTx), + undefined, + applyMessageFn, + ); + + assert.deepEqual(message, { + type: JournalMessageType.TRANSACTION_SEND, + futureId: "LockModule#Lock", + networkInteractionId: 1, + nonce: 1, + transaction: { + hash: "0x1a3eb512e21fc849f8e8733b250ce49b61178c9c4a670063f969db59eda4a59f", + fees: { maxFeePerGas: 2750000000n, maxPriorityFeePerGas: 1000000000n }, + }, + }); + }); + + describe("errors", () => { + it("should throw an error if the deploymentDir does not exist", async () => { + await assertRejectsWithHardhatError( + trackTransaction( + "non-existent-dir", + "txHash", + new MockEIP1193Provider(), + ), + HardhatError.ERRORS.IGNITION.TRACK_TRANSACTIONS + .DEPLOYMENT_DIR_NOT_FOUND, + { + deploymentDir: "non-existent-dir", + }, + ); + }); + + it("should throw an error if the deploymentDir leads to an uninitialized deployment", async () => { + const deploymentDir = path.resolve( + __dirname, + "./mocks/trackTransaction/uninitialized", + ); + + await assertRejectsWithHardhatError( + trackTransaction(deploymentDir, "txHash", new MockEIP1193Provider()), + HardhatError.ERRORS.IGNITION.TRACK_TRANSACTIONS + .UNINITIALIZED_DEPLOYMENT, + { + deploymentDir, + }, + ); + }); + + it("should throw an error if the transaction cannot be retrived from the provider", async () => { + const deploymentDir = path.resolve( + __dirname, + "./mocks/trackTransaction/success", + ); + + await assertRejectsWithHardhatError( + trackTransaction(deploymentDir, "txHash", new MockEIP1193Provider()), + HardhatError.ERRORS.IGNITION.TRACK_TRANSACTIONS.TRANSACTION_NOT_FOUND, + { + txHash: "txHash", + }, + ); + }); + + it("should throw an error if the user tries to track a transaction we already know about", async () => { + const deploymentDir = path.resolve( + __dirname, + "./mocks/trackTransaction/known-tx", + ); + + await assertRejectsWithHardhatError( + trackTransaction( + deploymentDir, + mockFullTx.hash, + new MockEIP1193Provider(mockFullTx), + ), + HardhatError.ERRORS.IGNITION.TRACK_TRANSACTIONS.KNOWN_TRANSACTION, + {}, + ); + }); + + it("(with TX_SEND in journal) should throw an error if the user replaced our transaction and their transaction does not have enough confirmations yet", async () => { + const deploymentDir = path.resolve( + __dirname, + "./mocks/trackTransaction/known-tx", + ); + + const hash = "0x1234"; + + await assertRejectsWithHardhatError( + trackTransaction( + deploymentDir, + hash, + new MockEIP1193Provider({ ...mockFullTx, hash }, 2), + ), + HardhatError.ERRORS.IGNITION.TRACK_TRANSACTIONS + .INSUFFICIENT_CONFIRMATIONS, + {}, + ); + }); + + it("should throw an error if we were unable to find a prepareSendMessage that matches the nonce of the given txHash", async () => { + const deploymentDir = path.resolve( + __dirname, + "./mocks/trackTransaction/success", + ); + + await assertRejectsWithHardhatError( + trackTransaction( + deploymentDir, + mockFullTx.hash, + new MockEIP1193Provider({ ...mockFullTx, nonce: "0x4" }), + ), + HardhatError.ERRORS.IGNITION.TRACK_TRANSACTIONS + .MATCHING_NONCE_NOT_FOUND, + {}, + ); + }); + + it("(without TX_SEND in journal) should throw an error if the user replaced our transaction and their transaction does not have enough confirmations yet", async () => { + const deploymentDir = path.resolve( + __dirname, + "./mocks/trackTransaction/success", + ); + + await assertRejectsWithHardhatError( + trackTransaction( + deploymentDir, + mockFullTx.hash, + new MockEIP1193Provider({ ...mockFullTx, value: "0x11" }, 2), + ), + HardhatError.ERRORS.IGNITION.TRACK_TRANSACTIONS + .INSUFFICIENT_CONFIRMATIONS, + {}, + ); + }); + }); +}); diff --git a/v-next/hardhat-ignition-ui/.gitignore b/v-next/hardhat-ignition-ui/.gitignore index 01edb73388e..c167f2b267a 100644 --- a/v-next/hardhat-ignition-ui/.gitignore +++ b/v-next/hardhat-ignition-ui/.gitignore @@ -24,3 +24,6 @@ dist-ssr *.sw? public/deployment.json + +vite.config.d.ts +vite.config.js \ No newline at end of file diff --git a/v-next/hardhat-ignition/package.json b/v-next/hardhat-ignition/package.json index 73e6a81b1e9..a67aae22219 100644 --- a/v-next/hardhat-ignition/package.json +++ b/v-next/hardhat-ignition/package.json @@ -37,7 +37,7 @@ "scripts": { "lint": "pnpm prettier --check && pnpm eslint", "lint:fix": "pnpm prettier --write && pnpm eslint --fix", - "eslint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"", + "eslint": "eslint \"src/**/*.ts\" \"test/**/*.ts\" --ignore-pattern \"**/*.d.ts\"", "prettier": "prettier \"**/*.{ts,js,md,json}\"", "pretest": "pnpm run --dir ../hardhat-ignition-ui build", "test": "cross-env TS_NODE_COMPILER_OPTIONS=\"{\\\"isolatedDeclarations\\\":false}\" mocha --recursive \"test/**/*.ts\"", diff --git a/v-next/hardhat-ignition/src/helpers/pretty-event-handler.ts b/v-next/hardhat-ignition/src/helpers/pretty-event-handler.ts index a3b398fac40..1878cba25aa 100644 --- a/v-next/hardhat-ignition/src/helpers/pretty-event-handler.ts +++ b/v-next/hardhat-ignition/src/helpers/pretty-event-handler.ts @@ -39,6 +39,7 @@ import type { StaticCallExecutionStateCompleteEvent, StaticCallExecutionStateInitializeEvent, TransactionConfirmEvent, + TransactionPrepareSendEvent, TransactionSendEvent, WipeApplyEvent, } from "@nomicfoundation/ignition-core"; @@ -258,6 +259,8 @@ export class PrettyEventHandler implements ExecutionEventListener { _event: NetworkInteractionRequestEvent, ): void {} + public transactionPrepareSend(_event: TransactionPrepareSendEvent): void {} + public transactionSend(_event: TransactionSendEvent): void {} public transactionConfirm(_event: TransactionConfirmEvent): void {} diff --git a/v-next/hardhat-ignition/src/index.ts b/v-next/hardhat-ignition/src/index.ts index 360b30dc217..5ec8f4a51c7 100644 --- a/v-next/hardhat-ignition/src/index.ts +++ b/v-next/hardhat-ignition/src/index.ts @@ -124,6 +124,22 @@ const hardhatIgnitionPlugin: HardhatPlugin = { }) .setAction(import.meta.resolve("./internal/tasks/verify.js")) .build(), + task( + ["ignition", "track-tx"], + "Track a transaction that is missing from a given deployment. Only use if a Hardhat Ignition error message suggests to do so.", + ) + .addPositionalArgument({ + name: "txHash", + type: ArgumentType.STRING, + description: "The hash of the transaction to track", + }) + .addPositionalArgument({ + name: "deploymentId", + type: ArgumentType.STRING, + description: "The id of the deployment to add the tx to", + }) + .setAction(import.meta.resolve("./internal/tasks/track-tx.js")) + .build(), ], }; diff --git a/v-next/hardhat-ignition/src/internal/tasks/track-tx.ts b/v-next/hardhat-ignition/src/internal/tasks/track-tx.ts new file mode 100644 index 00000000000..df1115763c9 --- /dev/null +++ b/v-next/hardhat-ignition/src/internal/tasks/track-tx.ts @@ -0,0 +1,40 @@ +import type { HardhatRuntimeEnvironment } from "hardhat/types/hre"; +import type { NewTaskActionFunction } from "hardhat/types/tasks"; + +import path from "node:path"; + +import { trackTransaction } from "@nomicfoundation/ignition-core"; + +interface TrackTxArguments { + txHash: string; + deploymentId: string; +} + +const taskTransactions: NewTaskActionFunction = async ( + { txHash, deploymentId }, + hre: HardhatRuntimeEnvironment, +) => { + const deploymentDir = path.join( + hre.config.paths.ignition, + "deployments", + deploymentId, + ); + + const connection = await hre.network.connect(); + + const output = await trackTransaction( + deploymentDir, + txHash, + connection.provider, + hre.config.ignition.requiredConfirmations, + ); + + console.log( + output ?? + `Thanks for providing the transaction hash, your deployment has been fixed. + +Now you can re-run Hardhat Ignition to continue with your deployment.`, + ); +}; + +export default taskTransactions; diff --git a/v-next/hardhat-ignition/src/internal/ui/verbose-event-handler.ts b/v-next/hardhat-ignition/src/internal/ui/verbose-event-handler.ts index 8deb88a83f7..7fa3f0df24d 100644 --- a/v-next/hardhat-ignition/src/internal/ui/verbose-event-handler.ts +++ b/v-next/hardhat-ignition/src/internal/ui/verbose-event-handler.ts @@ -27,6 +27,7 @@ import type { StaticCallExecutionStateCompleteEvent, StaticCallExecutionStateInitializeEvent, TransactionConfirmEvent, + TransactionPrepareSendEvent, TransactionSendEvent, WipeApplyEvent, } from "@nomicfoundation/ignition-core"; @@ -204,6 +205,12 @@ export class VerboseEventHandler implements ExecutionEventListener { } } + public transactionPrepareSend(event: TransactionPrepareSendEvent): void { + console.log( + `Transaction about to be sent for onchain interaction of future ${event.futureId}`, + ); + } + public transactionSend(event: TransactionSendEvent): void { console.log( `Transaction ${event.hash} sent for onchain interaction of future ${event.futureId}`,