diff --git a/.changeset/giant-ants-protect.md b/.changeset/giant-ants-protect.md new file mode 100644 index 0000000000..28ce790001 --- /dev/null +++ b/.changeset/giant-ants-protect.md @@ -0,0 +1,7 @@ +--- +"@nomicfoundation/ignition-core": patch +"@nomicfoundation/ignition-ui": patch +"@nomicfoundation/hardhat-ignition": patch +--- + +Port transaction hash bug to v3 diff --git a/v-next/hardhat-ignition-core/src/index.ts b/v-next/hardhat-ignition-core/src/index.ts index bdd0cd984e..cec3fcde31 100644 --- a/v-next/hardhat-ignition-core/src/index.ts +++ b/v-next/hardhat-ignition-core/src/index.ts @@ -19,5 +19,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/errors-list.ts b/v-next/hardhat-ignition-core/src/internal/errors-list.ts index 11ebb04ffa..c1f378a122 100644 --- a/v-next/hardhat-ignition-core/src/internal/errors-list.ts +++ b/v-next/hardhat-ignition-core/src/internal/errors-list.ts @@ -78,6 +78,11 @@ export const ERROR_RANGES: { max: 1299, title: "List transactions errors", }, + TRACK_TRANSACTION: { + min: 1300, + max: 1399, + title: "Track transaction errors", + }, }; /** @@ -201,6 +206,12 @@ export const ERRORS = { number: 410, message: "Gas estimation failed: %error%", }, + TRANSACTION_LOST: { + number: 411, + message: `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 `, + }, }, RECONCILIATION: { INVALID_EXECUTION_STATUS: { @@ -412,6 +423,36 @@ export const ERRORS = { "Cannot list transactions for nonexistant deployment at %deploymentDir%", }, }, + TRACK_TRANSACTION: { + DEPLOYMENT_DIR_NOT_FOUND: { + number: 1300, + message: "Deployment directory %deploymentDir% not found", + }, + UNINITIALIZED_DEPLOYMENT: { + number: 1301, + message: + "Cannot track transaction for nonexistant deployment at %deploymentDir%", + }, + TRANSACTION_NOT_FOUND: { + number: 1302, + message: `Transaction %txHash% not found. Please double check the transaction hash and try again.`, + }, + MATCHING_NONCE_NOT_FOUND: { + number: 1303, + message: `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.`, + }, + KNOWN_TRANSACTION: { + number: 1304, + message: `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.`, + }, + INSUFFICIENT_CONFIRMATIONS: { + number: 1305, + message: `The transaction you provided doesn't have enough confirmations yet. +Please try again later.`, + }, + }, }; /** 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 3be4d73671..271ccbc98b 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 b857038945..e78cfaa03d 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 @@ -13,9 +13,12 @@ import type { DeploymentLoader } from "../deployment-loader/types.js"; import sortBy from "lodash-es/sortBy.js"; +import { IgnitionError } from "../../errors.js"; import { ExecutionEventType } from "../../types/execution-events.js"; +import { ERRORS } from "../errors-list.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 +29,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 +73,8 @@ export class ExecutionEngine { deploymentParameters: DeploymentParameters, defaultSender: string, ): Promise { + await this._checkForMissingTransactions(deploymentState); + deploymentState = await this._syncNonces( deploymentState, module, @@ -199,6 +205,32 @@ 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 IgnitionError(ERRORS.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 807bcf7ebf..931b7b6f35 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 0a52e371df..d5f8b04bde 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 ff7a5c5385..32ebddffbc 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 @@ -22,8 +22,10 @@ import type { } from "../../types/network-interaction.js"; import { IgnitionError } from "../../../../errors.js"; +import { DeploymentLoader } from "../../../deployment-loader/types.js"; import { ERRORS } from "../../../errors-list.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; } > { @@ -201,6 +205,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 66b93c20db..62e1a99321 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, @@ -487,9 +489,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({ @@ -501,44 +506,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( @@ -546,12 +565,6 @@ export class EIP1193JsonRpcClient implements JsonRpcClient { ), }; } else { - assertResponseType( - method, - response, - "gasPrice" in response && typeof response.gasPrice === "string", - ); - networkFees = { gasPrice: jsonRpcQuantityToBigInt(response.gasPrice), }; @@ -822,6 +835,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 b90bd192a4..37a8ac9352 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 040df7e95c..77ad7a7c7d 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 f157a65237..34c0860e87 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 f66347e60f..f4e33fbb9d 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 ea9ed4ac83..8f21eb5b7f 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 bc99813554..d854530ae1 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 index ae224d15da..1ac61732cc 100644 --- a/v-next/hardhat-ignition-core/src/internal/journal/utils/log.ts +++ b/v-next/hardhat-ignition-core/src/internal/journal/utils/log.ts @@ -113,6 +113,12 @@ export function logJournalableMessage(message: JournalMessage): void { } break; + case JournalMessageType.TRANSACTION_PREPARE_SEND: + console.log( + `Transaction about to be sent for onchain interaction ${message.networkInteractionId} of future ${message.futureId}`, + ); + break; + case JournalMessageType.TRANSACTION_SEND: console.log( `Transaction ${message.transaction.hash} sent for onchain interaction ${message.networkInteractionId} of future ${message.futureId}`, 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 0000000000..33283bd2cd --- /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 0000000000..6aed128201 --- /dev/null +++ b/v-next/hardhat-ignition-core/src/track-transaction.ts @@ -0,0 +1,251 @@ +import { exists } from "@nomicfoundation/hardhat-utils/fs"; + +import { IgnitionError } from "./errors.js"; +import { defaultConfig } from "./internal/defaultConfig.js"; +import { FileDeploymentLoader } from "./internal/deployment-loader/file-deployment-loader.js"; +import { ERRORS } from "./internal/errors-list.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 IgnitionError(ERRORS.TRACK_TRANSACTION.DEPLOYMENT_DIR_NOT_FOUND, { + deploymentDir, + }); + } + const deploymentLoader = new FileDeploymentLoader(deploymentDir); + + const deploymentState = await loadDeploymentState(deploymentLoader); + + if (deploymentState === undefined) { + throw new IgnitionError(ERRORS.TRACK_TRANSACTION.UNINITIALIZED_DEPLOYMENT, { + deploymentDir, + }); + } + + const jsonRpcClient = new EIP1193JsonRpcClient(provider); + + const transaction = await jsonRpcClient.getFullTransaction(txHash); + + if (transaction === undefined) { + throw new IgnitionError(ERRORS.TRACK_TRANSACTION.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 IgnitionError(ERRORS.TRACK_TRANSACTION.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 IgnitionError(ERRORS.TRACK_TRANSACTION.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 IgnitionError( + ERRORS.TRACK_TRANSACTION.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 ad1d7ccc76..3d16495512 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 0000000000..cd933c69fc --- /dev/null +++ b/v-next/hardhat-ignition-core/test/execution/execution-engine.ts @@ -0,0 +1,42 @@ +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 assert.isRejected( + engine.executeModule(deploymentState, {} as any, [], [], {}, "0x"), + /^IGN411:/, + ); + }); + }); +}); 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 f0c491498c..d3af9fa32f 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 @@ -39,7 +39,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, @@ -47,6 +50,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 { @@ -122,6 +126,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 () => { @@ -345,6 +392,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; @@ -366,6 +423,7 @@ describe("Network interactions", () => { const client = new LocalMockJsonRpcClient(); const nonceManager = new MockNonceManager(); + const deploymentLoader = new MockDeploymentLoader(); const onchainInteraction: OnchainInteraction = { to: exampleAccounts[1], @@ -383,6 +441,8 @@ describe("Network interactions", () => { onchainInteraction, nonceManager, async () => undefined, + deploymentLoader, + "test", ); assert.equal(client.storedFees.maxFeePerGas, 100n); @@ -393,6 +453,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], @@ -410,6 +471,8 @@ describe("Network interactions", () => { onchainInteraction, nonceManager, async () => undefined, + deploymentLoader, + "test", ); assert.equal(nonceManager.calls[exampleAccounts[0]], 1); @@ -429,6 +492,7 @@ describe("Network interactions", () => { const client = new LocalMockJsonRpcClient(); const nonceManager = new MockNonceManager(); + const deploymentLoader = new MockDeploymentLoader(); const onchainInteraction: OnchainInteraction = { to: exampleAccounts[1], @@ -447,6 +511,8 @@ describe("Network interactions", () => { onchainInteraction, nonceManager, async () => undefined, + deploymentLoader, + "test", ); assert.equal(nonceManager.calls[exampleAccounts[0]], undefined); @@ -472,6 +538,7 @@ describe("Network interactions", () => { const client = new LocalMockJsonRpcClient(); const nonceManager = new MockNonceManager(); + const deploymentLoader = new MockDeploymentLoader(); const onchainInteraction: OnchainInteraction = { to: exampleAccounts[1], @@ -507,6 +574,8 @@ describe("Network interactions", () => { onchainInteraction, nonceManager, decodeSimulationResult(mockStrategyGenerator, mockExecutionState), + deploymentLoader, + "test", ); // type casting @@ -522,9 +591,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], @@ -542,6 +612,8 @@ describe("Network interactions", () => { onchainInteraction, nonceManager, async () => undefined, + deploymentLoader, + "test", ); // type casting @@ -549,6 +621,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"); }); @@ -586,6 +662,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], @@ -621,6 +698,8 @@ describe("Network interactions", () => { onchainInteraction, nonceManager, decodeSimulationResult(mockStrategyGenerator, mockExecutionState), + deploymentLoader, + "test", ); // type casting @@ -642,6 +721,7 @@ describe("Network interactions", () => { "insufficient funds for transfer", ); const nonceManager = new MockNonceManager(); + const deploymentLoader = new MockDeploymentLoader(); const onchainInteraction: OnchainInteraction = { to: exampleAccounts[1], @@ -660,6 +740,8 @@ describe("Network interactions", () => { onchainInteraction, nonceManager, async () => undefined, + deploymentLoader, + "test", ), /^IGN408/, ); @@ -672,6 +754,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], @@ -690,6 +773,8 @@ describe("Network interactions", () => { onchainInteraction, nonceManager, async () => undefined, + deploymentLoader, + "test", ), /^IGN409/, ); @@ -700,6 +785,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], @@ -718,6 +804,8 @@ describe("Network interactions", () => { onchainInteraction, nonceManager, async () => undefined, + deploymentLoader, + "test", ), /^IGN410/, ); 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 0000000000..6b064d4d63 --- /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 0000000000..35a089d30d --- /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 0000000000..6201f42796 --- /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 0000000000..947be497f3 --- /dev/null +++ b/v-next/hardhat-ignition-core/test/track-transaction.ts @@ -0,0 +1,282 @@ +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"; + +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 assert.isRejected( + trackTransaction( + "non-existent-dir", + "txHash", + new MockEIP1193Provider(), + ), + "Deployment directory non-existent-dir not found", + ); + }); + + it("should throw an error if the deploymentDir leads to an uninitialized deployment", async () => { + const deploymentDir = path.resolve( + __dirname, + "./mocks/trackTransaction/uninitialized", + ); + + await assert.isRejected( + trackTransaction(deploymentDir, "txHash", new MockEIP1193Provider()), + `Cannot track transaction for nonexistant deployment at ${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 assert.isRejected( + trackTransaction(deploymentDir, "txHash", new MockEIP1193Provider()), + `Transaction txHash not found. Please double check the transaction hash and try again.`, + ); + }); + + 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 assert.isRejected( + trackTransaction( + deploymentDir, + mockFullTx.hash, + new MockEIP1193Provider(mockFullTx), + ), + /^IGN1304/, + ); + }); + + 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 assert.isRejected( + trackTransaction( + deploymentDir, + hash, + new MockEIP1193Provider({ ...mockFullTx, hash }, 2), + ), + /^IGN1305/, + ); + }); + + 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 assert.isRejected( + trackTransaction( + deploymentDir, + mockFullTx.hash, + new MockEIP1193Provider({ ...mockFullTx, nonce: "0x4" }), + ), + /^IGN1303/, + ); + }); + + 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 assert.isRejected( + trackTransaction( + deploymentDir, + mockFullTx.hash, + new MockEIP1193Provider({ ...mockFullTx, value: "0x11" }, 2), + ), + /^IGN1305/, + ); + }); + }); +}); diff --git a/v-next/hardhat-ignition-ui/.gitignore b/v-next/hardhat-ignition-ui/.gitignore index 01edb73388..c167f2b267 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 643bf2d6d3..b33ec68298 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 585b447847..d877a13335 100644 --- a/v-next/hardhat-ignition/src/helpers/pretty-event-handler.ts +++ b/v-next/hardhat-ignition/src/helpers/pretty-event-handler.ts @@ -40,6 +40,7 @@ import type { StaticCallExecutionStateCompleteEvent, StaticCallExecutionStateInitializeEvent, TransactionConfirmEvent, + TransactionPrepareSendEvent, TransactionSendEvent, WipeApplyEvent, } from "@nomicfoundation/ignition-core"; @@ -259,6 +260,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 360b30dc21..5ec8f4a51c 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 0000000000..e050869828 --- /dev/null +++ b/v-next/hardhat-ignition/src/internal/tasks/track-tx.ts @@ -0,0 +1,54 @@ +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(); + + let output: string | void; + try { + output = await trackTransaction( + deploymentDir, + txHash, + connection.provider, + hre.config.ignition.requiredConfirmations, + ); + } catch (e) { + // Disabled for the alpha release + // if (e instanceof IgnitionError && shouldBeHardhatPluginError(e)) { + // throw new NomicLabsHardhatPluginError( + // "hardhat-ignition", + // e.message, + // e + // ); + // } + + throw e; + } + + 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 8deb88a83f..7fa3f0df24 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}`, diff --git a/v-next/hardhat-ignition/src/internal/utils/shouldBeHardhatPluginError.ts b/v-next/hardhat-ignition/src/internal/utils/shouldBeHardhatPluginError.ts index 964f3eb406..dd126a6dd1 100644 --- a/v-next/hardhat-ignition/src/internal/utils/shouldBeHardhatPluginError.ts +++ b/v-next/hardhat-ignition/src/internal/utils/shouldBeHardhatPluginError.ts @@ -9,10 +9,10 @@ import type { IgnitionError } from "@nomicfoundation/ignition-core"; * - If there's an exception that doesn't fit in either category, let's discuss it and review the categories. */ const whitelist = [ - 200, 201, 202, 203, 204, 403, 404, 405, 406, 407, 408, 409, 600, 601, 602, - 700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, - 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 800, 900, 1000, - 1001, 1002, 1101, 1102, 1103, + 200, 201, 202, 203, 204, 403, 404, 405, 406, 407, 408, 409, 411, 600, 601, + 602, 700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, + 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 800, 900, + 1000, 1001, 1002, 1101, 1102, 1103, 1300, 1301, 1302, 1303, 1304, 1305, ]; export function shouldBeHardhatPluginError(error: IgnitionError): boolean {