diff --git a/examples/uniswap/.gitignore b/examples/uniswap/.gitignore index 36077f288..72d98e146 100644 --- a/examples/uniswap/.gitignore +++ b/examples/uniswap/.gitignore @@ -7,3 +7,4 @@ typechain #Hardhat files cache artifacts +ignition/deployments diff --git a/package-lock.json b/package-lock.json index 9f7cf30a2..20ea38e91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5464,6 +5464,15 @@ "integrity": "sha512-kb9/98N6X8gyME9Cf7YaqIMvYGnBSWqEci6tiettE6iJWH1XdJz/PO8LB0GtLCG7x8dU3KWhZT+lA1a35127tA==", "dev": true }, + "node_modules/@types/lodash.flattendeep": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/@types/lodash.flattendeep/-/lodash.flattendeep-4.4.7.tgz", + "integrity": "sha512-1h6GW/AeZw/Wej6uxrqgmdTDZX1yFS39lRsXYkg+3kWvOWWrlGCI6H7lXxlUHOzxDT4QeYGmgPpQ3BX9XevzOg==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz", @@ -14215,8 +14224,7 @@ "node_modules/lodash.flattendeep": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", - "dev": true + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==" }, "node_modules/lodash.get": { "version": "4.4.2", @@ -21739,7 +21747,7 @@ "@types/fs-extra": "^9.0.13", "@types/mocha": "9.1.1", "@types/ndjson": "2.0.1", - "@types/node": "12.20.25", + "@types/node": "^16.0.0", "@typescript-eslint/eslint-plugin": "4.31.2", "@typescript-eslint/parser": "4.31.2", "chai": "^4.3.4", @@ -21772,9 +21780,9 @@ ] }, "packages/core/node_modules/@types/node": { - "version": "12.20.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.25.tgz", - "integrity": "sha512-hcTWqk7DR/HrN9Xe7AlJwuCaL13Vcd9/g/T54YrJz4Q3ESM5mr33YCzW2bOfzSIc3aZMeGBvbLGvgN6mIJ0I5Q==", + "version": "16.18.44", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.44.tgz", + "integrity": "sha512-PZXtT+wqSMHnLPVExTh+tMt1VK+GvjRLsGZMbcQ4Mb/cG63xJig/TUmgrDa9aborl2i22UnpIzHYNu7s97NbBQ==", "dev": true }, "packages/core/node_modules/aes-js": { @@ -21860,6 +21868,7 @@ "fs-extra": "^10.0.0", "ink": "3.2.0", "ink-spinner": "4.0.3", + "lodash.flattendeep": "^4.4.0", "ndjson": "2.0.0", "prompts": "^2.4.2", "react": "18.2.0", @@ -21877,10 +21886,11 @@ "@types/dompurify": "2.4.0", "@types/fs-extra": "^9.0.13", "@types/lodash": "4.14.189", + "@types/lodash.flattendeep": "^4.4.7", "@types/mermaid": "^9.1.0", "@types/mocha": "^9.0.0", "@types/ndjson": "2.0.1", - "@types/node": "12.20.25", + "@types/node": "^16.0.0", "@types/prompts": "^2.4.2", "@types/sinon": "^10.0.13", "@typescript-eslint/eslint-plugin": "4.31.2", @@ -21976,9 +21986,9 @@ } }, "packages/hardhat-plugin/node_modules/@types/node": { - "version": "12.20.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.25.tgz", - "integrity": "sha512-hcTWqk7DR/HrN9Xe7AlJwuCaL13Vcd9/g/T54YrJz4Q3ESM5mr33YCzW2bOfzSIc3aZMeGBvbLGvgN6mIJ0I5Q==", + "version": "16.18.44", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.44.tgz", + "integrity": "sha512-PZXtT+wqSMHnLPVExTh+tMt1VK+GvjRLsGZMbcQ4Mb/cG63xJig/TUmgrDa9aborl2i22UnpIzHYNu7s97NbBQ==", "dev": true }, "packages/hardhat-plugin/node_modules/aes-js": { diff --git a/packages/core/package.json b/packages/core/package.json index fb7588a3d..aa14d0e16 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -53,7 +53,7 @@ "@types/fs-extra": "^9.0.13", "@types/mocha": "9.1.1", "@types/ndjson": "2.0.1", - "@types/node": "12.20.25", + "@types/node": "^16.0.0", "@typescript-eslint/eslint-plugin": "4.31.2", "@typescript-eslint/parser": "4.31.2", "chai": "^4.3.4", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e8e86c922..376b9849e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,14 +1,15 @@ export * from "./errors"; export { buildModule } from "./new-api/build-module"; export { deploy } from "./new-api/deploy"; +export { formatSolidityParameter } from "./new-api/internal/formatters"; export { plan } from "./new-api/plan"; -export { wipe } from "./new-api/wipe"; export { StoredDeploymentSerializer } from "./new-api/stored-deployment-serializer"; export * from "./new-api/type-guards"; export * from "./new-api/types/artifact"; export * from "./new-api/types/deploy"; +export * from "./new-api/types/execution-events"; export * from "./new-api/types/module"; export * from "./new-api/types/module-builder"; export * from "./new-api/types/provider"; export * from "./new-api/types/serialized-deployment"; -export { formatSolidityParameter } from "./new-api/internal/formatters"; +export { wipe } from "./new-api/wipe"; diff --git a/packages/core/src/new-api/deploy.ts b/packages/core/src/new-api/deploy.ts index db81a5957..87204dd29 100644 --- a/packages/core/src/new-api/deploy.ts +++ b/packages/core/src/new-api/deploy.ts @@ -18,6 +18,10 @@ import { DeploymentParameters, DeploymentResult, } from "./types/deploy"; +import { + ExecutionEventListener, + ExecutionEventType, +} from "./types/execution-events"; import { IgnitionModule, IgnitionModuleResult } from "./types/module"; import { EIP1193Provider } from "./types/provider"; @@ -34,16 +38,17 @@ export async function deploy< config = {}, artifactResolver, provider, + executionEventListener, deploymentDir, ignitionModule, deploymentParameters, accounts, - verbose, defaultSender, }: { config?: Partial; artifactResolver: ArtifactResolver; provider: EIP1193Provider; + executionEventListener?: ExecutionEventListener; deploymentDir?: string; ignitionModule: IgnitionModule< ModuleIdT, @@ -52,10 +57,30 @@ export async function deploy< >; deploymentParameters: DeploymentParameters; accounts: string[]; - verbose: boolean; defaultSender?: string; }): Promise> { - await validateStageOne(ignitionModule, artifactResolver); + if (executionEventListener !== undefined) { + executionEventListener.SET_MODULE_ID({ + type: ExecutionEventType.SET_MODULE_ID, + moduleName: ignitionModule.id, + }); + } + + const validationResult = await validateStageOne( + ignitionModule, + artifactResolver + ); + + if (validationResult !== null) { + if (executionEventListener !== undefined) { + executionEventListener.DEPLOYMENT_COMPLETE({ + type: ExecutionEventType.DEPLOYMENT_COMPLETE, + result: validationResult, + }); + } + + return validationResult; + } if (defaultSender !== undefined) { if (!accounts.includes(defaultSender)) { @@ -69,8 +94,8 @@ export async function deploy< const deploymentLoader = deploymentDir === undefined - ? new EphemeralDeploymentLoader(artifactResolver, verbose) - : new FileDeploymentLoader(deploymentDir, verbose); + ? new EphemeralDeploymentLoader(artifactResolver, executionEventListener) + : new FileDeploymentLoader(deploymentDir, executionEventListener); const executionStrategy = new BasicExecutionStrategy((artifactId) => deploymentLoader.loadArtifact(artifactId) @@ -93,7 +118,8 @@ export async function deploy< executionStrategy, jsonRpcClient, artifactResolver, - deploymentLoader + deploymentLoader, + executionEventListener ); return deployer.deploy( diff --git a/packages/core/src/new-api/internal/deployer.ts b/packages/core/src/new-api/internal/deployer.ts index a21d44cf4..397f52673 100644 --- a/packages/core/src/new-api/internal/deployer.ts +++ b/packages/core/src/new-api/internal/deployer.ts @@ -11,6 +11,10 @@ import { ReconciliationErrorDeploymentResult, SuccessfulDeploymentResult, } from "../types/deploy"; +import { + ExecutionEventListener, + ExecutionEventType, +} from "../types/execution-events"; import { Batcher } from "./batcher"; import { DeploymentLoader } from "./deployment-loader/types"; @@ -50,7 +54,8 @@ export class Deployer { private readonly _executionStrategy: ExecutionStrategy, private readonly _jsonRpcClient: JsonRpcClient, private readonly _artifactResolver: ArtifactResolver, - private readonly _deploymentLoader: DeploymentLoader + private readonly _deploymentLoader: DeploymentLoader, + private readonly _executionEventListener?: ExecutionEventListener ) { assertIgnitionInvariant( this._config.requiredConfirmations >= 1, @@ -80,10 +85,14 @@ export class Deployer { ); if (validationResult !== null) { + this._emitDeploymentCompleteEvent(validationResult); + return validationResult; } - let deploymentState = await this._getOrInitializeDeploymentState(); + let deploymentState = await this._getOrInitializeDeploymentState( + ignitionModule.id + ); const contracts = getFuturesFromModule(ignitionModule).filter(isContractFuture); @@ -129,10 +138,14 @@ export class Deployer { errors[futureId].push(failure); } - return { + const reconciliationErrorResult: ReconciliationErrorDeploymentResult = { type: DeploymentResultType.RECONCILIATION_ERROR, errors, }; + + this._emitDeploymentCompleteEvent(reconciliationErrorResult); + + return reconciliationErrorResult; } if (reconciliationResult.missingExecutedFutures.length > 0) { @@ -141,11 +154,14 @@ export class Deployer { const batches = Batcher.batch(ignitionModule, deploymentState); + this._emitDeploymentBatchEvent(batches); + const executionEngine = new ExecutionEngine( this._deploymentLoader, this._artifactResolver, this._executionStrategy, this._jsonRpcClient, + this._executionEventListener, this._config.requiredConfirmations, this._config.timeBeforeBumpingFees, this._config.maxFeeBumps, @@ -161,7 +177,14 @@ export class Deployer { defaultSender ); - return this._getDeploymentResult(deploymentState, ignitionModule); + const result = await this._getDeploymentResult( + deploymentState, + ignitionModule + ); + + this._emitDeploymentCompleteEvent(result); + + return result; } private async _getDeploymentResult< @@ -196,11 +219,15 @@ export class Deployer { }; } - private async _getOrInitializeDeploymentState(): Promise { + private async _getOrInitializeDeploymentState( + moduleId: string + ): Promise { const chainId = await this._jsonRpcClient.getChainId(); const deploymentState = await loadDeploymentState(this._deploymentLoader); if (deploymentState === undefined) { + this._emitDeploymentStartEvent(moduleId); + return initializeDeploymentState(chainId, this._deploymentLoader); } @@ -212,6 +239,41 @@ export class Deployer { return deploymentState; } + private _emitDeploymentStartEvent(moduleId: string): void { + if (this._executionEventListener === undefined) { + return; + } + + this._executionEventListener.DEPLOYMENT_START({ + type: ExecutionEventType.DEPLOYMENT_START, + moduleName: moduleId, + }); + } + + private _emitDeploymentBatchEvent(batches: string[][]): void { + if (this._executionEventListener === undefined) { + return; + } + + this._executionEventListener.BATCH_INITIALIZE({ + type: ExecutionEventType.BATCH_INITIALIZE, + batches, + }); + } + + private _emitDeploymentCompleteEvent( + result: DeploymentResult> + ): void { + if (this._executionEventListener === undefined) { + return; + } + + this._executionEventListener.DEPLOYMENT_COMPLETE({ + type: ExecutionEventType.DEPLOYMENT_COMPLETE, + result, + }); + } + private _isSuccessful(deploymentState: DeploymentState): boolean { return Object.values(deploymentState.executionStates).every( (ex) => ex.status === ExecutionStatus.SUCCESS diff --git a/packages/core/src/new-api/internal/deployment-loader/ephemeral-deployment-loader.ts b/packages/core/src/new-api/internal/deployment-loader/ephemeral-deployment-loader.ts index 4c131103a..3f37cb8a9 100644 --- a/packages/core/src/new-api/internal/deployment-loader/ephemeral-deployment-loader.ts +++ b/packages/core/src/new-api/internal/deployment-loader/ephemeral-deployment-loader.ts @@ -1,4 +1,5 @@ import { Artifact, ArtifactResolver, BuildInfo } from "../../types/artifact"; +import { ExecutionEventListener } from "../../types/execution-events"; import { MemoryJournal } from "../journal/memory-journal"; import { Journal } from "../journal/types"; import { JournalMessage } from "../new-execution/types/messages"; @@ -23,9 +24,9 @@ export class EphemeralDeploymentLoader implements DeploymentLoader { constructor( private _artifactResolver: ArtifactResolver, - private _verbose: boolean + private _executionEventListener?: ExecutionEventListener ) { - this._journal = new MemoryJournal(this._verbose); + this._journal = new MemoryJournal(this._executionEventListener); this._deployedAddresses = {}; this._savedArtifacts = {}; } diff --git a/packages/core/src/new-api/internal/deployment-loader/file-deployment-loader.ts b/packages/core/src/new-api/internal/deployment-loader/file-deployment-loader.ts index c96223399..842f9e819 100644 --- a/packages/core/src/new-api/internal/deployment-loader/file-deployment-loader.ts +++ b/packages/core/src/new-api/internal/deployment-loader/file-deployment-loader.ts @@ -2,6 +2,7 @@ import { ensureDir, pathExists, readFile, writeFile } from "fs-extra"; import path from "path"; import { Artifact, BuildInfo } from "../../types/artifact"; +import { ExecutionEventListener } from "../../types/execution-events"; import { FileJournal } from "../journal/file-journal"; import { Journal } from "../journal/types"; import { JournalMessage } from "../new-execution/types/messages"; @@ -22,7 +23,7 @@ export class FileDeploymentLoader implements DeploymentLoader { constructor( private readonly _deploymentDirPath: string, - private readonly _verbose: boolean + private readonly _executionEventListener?: ExecutionEventListener ) { const artifactsDir = path.join(this._deploymentDirPath, "artifacts"); const buildInfoDir = path.join(this._deploymentDirPath, "build-info"); @@ -32,7 +33,7 @@ export class FileDeploymentLoader implements DeploymentLoader { "deployed_addresses.json" ); - this._journal = new FileJournal(journalPath, this._verbose); + this._journal = new FileJournal(journalPath, this._executionEventListener); this._paths = { deploymentDir: this._deploymentDirPath, diff --git a/packages/core/src/new-api/internal/journal/file-journal.ts b/packages/core/src/new-api/internal/journal/file-journal.ts index fd06bb032..a74fb8c40 100644 --- a/packages/core/src/new-api/internal/journal/file-journal.ts +++ b/packages/core/src/new-api/internal/journal/file-journal.ts @@ -2,11 +2,12 @@ import fs, { closeSync, constants, openSync, writeFileSync } from "fs"; import { parse } from "ndjson"; +import { ExecutionEventListener } from "../../types/execution-events"; import { JournalMessage } from "../new-execution/types/messages"; import { Journal } from "./types"; import { deserializeReplacer } from "./utils/deserialize-replacer"; -import { logJournalableMessage } from "./utils/log"; +import { emitExecutionEvent } from "./utils/emitExecutionEvent"; import { serializeReplacer } from "./utils/serialize-replacer"; /** @@ -15,7 +16,10 @@ import { serializeReplacer } from "./utils/serialize-replacer"; * @beta */ export class FileJournal implements Journal { - constructor(private _filePath: string, private _verbose: boolean = false) {} + constructor( + private _filePath: string, + private _executionEventListener?: ExecutionEventListener + ) {} public record(message: JournalMessage): void { this._log(message); @@ -60,8 +64,8 @@ export class FileJournal implements Journal { } private _log(message: JournalMessage): void { - if (this._verbose) { - return logJournalableMessage(message); + if (this._executionEventListener !== undefined) { + emitExecutionEvent(message, this._executionEventListener); } } } diff --git a/packages/core/src/new-api/internal/journal/memory-journal.ts b/packages/core/src/new-api/internal/journal/memory-journal.ts index 1f22ae094..be20d0074 100644 --- a/packages/core/src/new-api/internal/journal/memory-journal.ts +++ b/packages/core/src/new-api/internal/journal/memory-journal.ts @@ -1,7 +1,8 @@ +import { ExecutionEventListener } from "../../types/execution-events"; import { JournalMessage } from "../new-execution/types/messages"; import { Journal } from "./types"; -import { logJournalableMessage } from "./utils/log"; +import { emitExecutionEvent } from "./utils/emitExecutionEvent"; /** * An in-memory journal. @@ -11,7 +12,7 @@ import { logJournalableMessage } from "./utils/log"; export class MemoryJournal implements Journal { private messages: JournalMessage[] = []; - constructor(private _verbose: boolean = false) {} + constructor(private _executionEventListener?: ExecutionEventListener) {} public record(message: JournalMessage): void { this._log(message); @@ -26,8 +27,8 @@ export class MemoryJournal implements Journal { } private _log(message: JournalMessage): void { - if (this._verbose) { - return logJournalableMessage(message); + if (this._executionEventListener !== undefined) { + emitExecutionEvent(message, this._executionEventListener); } } } diff --git a/packages/core/src/new-api/internal/journal/utils/emitExecutionEvent.ts b/packages/core/src/new-api/internal/journal/utils/emitExecutionEvent.ts new file mode 100644 index 000000000..b50db2053 --- /dev/null +++ b/packages/core/src/new-api/internal/journal/utils/emitExecutionEvent.ts @@ -0,0 +1,277 @@ +import { + ExecutionEventListener, + ExecutionEventType, + ExecutionEventResult, + ExecutionEventResultType, + ExecutionEventNetworkInteractionType, +} from "../../../types/execution-events"; +import { SolidityParameterType } from "../../../types/module"; +import { + CallExecutionResult, + DeploymentExecutionResult, + ExecutionResultType, + SendDataExecutionResult, + StaticCallExecutionResult, +} from "../../new-execution/types/execution-result"; +import { + JournalMessage, + JournalMessageType, +} from "../../new-execution/types/messages"; +import { NetworkInteractionType } from "../../new-execution/types/network-interaction"; + +import { failedEvmExecutionResultToErrorDescription } from "./failedEvmExecutionResultToErrorDescription"; + +export function emitExecutionEvent( + message: JournalMessage, + executionEventListener: ExecutionEventListener +): void { + switch (message.type) { + case JournalMessageType.RUN_START: { + executionEventListener[ExecutionEventType.RUN_START]({ + type: ExecutionEventType.RUN_START, + chainId: message.chainId, + }); + break; + } + case JournalMessageType.WIPE_EXECUTION_STATE: { + executionEventListener[ExecutionEventType.WIPE_EXECUTION_STATE]({ + type: ExecutionEventType.WIPE_EXECUTION_STATE, + futureId: message.futureId, + }); + break; + } + case JournalMessageType.DEPLOYMENT_EXECUTION_STATE_INITIALIZE: { + executionEventListener[ + ExecutionEventType.DEPLOYMENT_EXECUTION_STATE_INITIALIZE + ]({ + type: ExecutionEventType.DEPLOYMENT_EXECUTION_STATE_INITIALIZE, + futureId: message.futureId, + }); + break; + } + case JournalMessageType.DEPLOYMENT_EXECUTION_STATE_COMPLETE: { + executionEventListener[ + ExecutionEventType.DEPLOYMENT_EXECUTION_STATE_COMPLETE + ]({ + type: ExecutionEventType.DEPLOYMENT_EXECUTION_STATE_COMPLETE, + futureId: message.futureId, + result: convertExecutionResultToEventResult(message.result), + }); + break; + } + case JournalMessageType.CALL_EXECUTION_STATE_INITIALIZE: { + executionEventListener[ + ExecutionEventType.CALL_EXECUTION_STATE_INITIALIZE + ]({ + type: ExecutionEventType.CALL_EXECUTION_STATE_INITIALIZE, + futureId: message.futureId, + }); + break; + } + case JournalMessageType.CALL_EXECUTION_STATE_COMPLETE: { + executionEventListener[ExecutionEventType.CALL_EXECUTION_STATE_COMPLETE]({ + type: ExecutionEventType.CALL_EXECUTION_STATE_COMPLETE, + futureId: message.futureId, + result: convertExecutionResultToEventResult(message.result), + }); + break; + } + case JournalMessageType.STATIC_CALL_EXECUTION_STATE_INITIALIZE: { + executionEventListener[ + ExecutionEventType.STATIC_CALL_EXECUTION_STATE_INITIALIZE + ]({ + type: ExecutionEventType.STATIC_CALL_EXECUTION_STATE_INITIALIZE, + futureId: message.futureId, + }); + break; + } + case JournalMessageType.STATIC_CALL_EXECUTION_STATE_COMPLETE: { + executionEventListener[ + ExecutionEventType.STATIC_CALL_EXECUTION_STATE_COMPLETE + ]({ + type: ExecutionEventType.STATIC_CALL_EXECUTION_STATE_COMPLETE, + futureId: message.futureId, + result: convertStaticCallResultToExecutionEventResult(message.result), + }); + break; + } + case JournalMessageType.SEND_DATA_EXECUTION_STATE_INITIALIZE: { + executionEventListener[ + ExecutionEventType.SEND_DATA_EXECUTION_STATE_INITIALIZE + ]({ + type: ExecutionEventType.SEND_DATA_EXECUTION_STATE_INITIALIZE, + futureId: message.futureId, + }); + break; + } + case JournalMessageType.SEND_DATA_EXECUTION_STATE_COMPLETE: { + executionEventListener[ + ExecutionEventType.STATIC_CALL_EXECUTION_STATE_COMPLETE + ]({ + type: ExecutionEventType.STATIC_CALL_EXECUTION_STATE_COMPLETE, + futureId: message.futureId, + result: convertExecutionResultToEventResult(message.result), + }); + break; + } + case JournalMessageType.CONTRACT_AT_EXECUTION_STATE_INITIALIZE: { + executionEventListener[ + ExecutionEventType.CONTRACT_AT_EXECUTION_STATE_INITIALIZE + ]({ + type: ExecutionEventType.CONTRACT_AT_EXECUTION_STATE_INITIALIZE, + futureId: message.futureId, + }); + break; + } + case JournalMessageType.READ_EVENT_ARGUMENT_EXECUTION_STATE_INITIALIZE: { + executionEventListener[ + ExecutionEventType.READ_EVENT_ARGUMENT_EXECUTION_STATE_INITIALIZE + ]({ + type: ExecutionEventType.READ_EVENT_ARGUMENT_EXECUTION_STATE_INITIALIZE, + futureId: message.futureId, + result: { + type: ExecutionEventResultType.SUCCESS, + result: solidityParamToString(message.result), + }, + }); + break; + } + case JournalMessageType.NETWORK_INTERACTION_REQUEST: { + executionEventListener[ExecutionEventType.NETWORK_INTERACTION_REQUEST]({ + type: ExecutionEventType.NETWORK_INTERACTION_REQUEST, + networkInteractionType: + message.networkInteraction.type === + NetworkInteractionType.ONCHAIN_INTERACTION + ? ExecutionEventNetworkInteractionType.ONCHAIN_INTERACTION + : ExecutionEventNetworkInteractionType.STATIC_CALL, + futureId: message.futureId, + }); + break; + } + case JournalMessageType.TRANSACTION_SEND: { + executionEventListener[ExecutionEventType.TRANSACTION_SEND]({ + type: ExecutionEventType.TRANSACTION_SEND, + futureId: message.futureId, + hash: message.transaction.hash, + }); + break; + } + case JournalMessageType.TRANSACTION_CONFIRM: { + executionEventListener[ExecutionEventType.TRANSACTION_CONFIRM]({ + type: ExecutionEventType.TRANSACTION_CONFIRM, + futureId: message.futureId, + hash: message.hash, + }); + break; + } + case JournalMessageType.STATIC_CALL_COMPLETE: { + executionEventListener[ExecutionEventType.STATIC_CALL_COMPLETE]({ + type: ExecutionEventType.STATIC_CALL_COMPLETE, + futureId: message.futureId, + }); + break; + } + case JournalMessageType.ONCHAIN_INTERACTION_BUMP_FEES: { + executionEventListener[ExecutionEventType.ONCHAIN_INTERACTION_BUMP_FEES]({ + type: ExecutionEventType.ONCHAIN_INTERACTION_BUMP_FEES, + futureId: message.futureId, + }); + break; + } + case JournalMessageType.ONCHAIN_INTERACTION_DROPPED: { + executionEventListener[ExecutionEventType.ONCHAIN_INTERACTION_DROPPED]({ + type: ExecutionEventType.ONCHAIN_INTERACTION_DROPPED, + futureId: message.futureId, + }); + break; + } + case JournalMessageType.ONCHAIN_INTERACTION_REPLACED_BY_USER: { + executionEventListener[ + ExecutionEventType.ONCHAIN_INTERACTION_REPLACED_BY_USER + ]({ + type: ExecutionEventType.ONCHAIN_INTERACTION_REPLACED_BY_USER, + futureId: message.futureId, + }); + break; + } + case JournalMessageType.ONCHAIN_INTERACTION_TIMEOUT: { + executionEventListener[ExecutionEventType.ONCHAIN_INTERACTION_TIMEOUT]({ + type: ExecutionEventType.ONCHAIN_INTERACTION_TIMEOUT, + futureId: message.futureId, + }); + break; + } + } +} + +function convertExecutionResultToEventResult( + result: + | DeploymentExecutionResult + | CallExecutionResult + | SendDataExecutionResult +): ExecutionEventResult { + switch (result.type) { + case ExecutionResultType.SUCCESS: { + return { + type: ExecutionEventResultType.SUCCESS, + result: "address" in result ? result.address : undefined, + }; + } + case ExecutionResultType.STATIC_CALL_ERROR: + case ExecutionResultType.SIMULATION_ERROR: { + return { + type: ExecutionEventResultType.ERROR, + error: failedEvmExecutionResultToErrorDescription(result.error), + }; + } + case ExecutionResultType.STRATEGY_ERROR: + case ExecutionResultType.STRATEGY_SIMULATION_ERROR: { + return { + type: ExecutionEventResultType.ERROR, + error: result.error, + }; + } + case ExecutionResultType.REVERTED_TRANSACTION: { + return { + type: ExecutionEventResultType.ERROR, + error: "Transaction reverted", + }; + } + } +} + +function convertStaticCallResultToExecutionEventResult( + result: StaticCallExecutionResult +): ExecutionEventResult { + switch (result.type) { + case ExecutionResultType.SUCCESS: { + return { + type: ExecutionEventResultType.SUCCESS, + }; + } + case ExecutionResultType.STATIC_CALL_ERROR: { + return { + type: ExecutionEventResultType.ERROR, + error: failedEvmExecutionResultToErrorDescription(result.error), + }; + } + case ExecutionResultType.STRATEGY_ERROR: { + return { + type: ExecutionEventResultType.ERROR, + error: result.error, + }; + } + } +} + +function solidityParamToString(param: SolidityParameterType): string { + if (typeof param === "object") { + return JSON.stringify(param); + } + + if (typeof param === "string") { + return param; + } + + return param.toString(); +} diff --git a/packages/core/src/new-api/internal/journal/utils/failedEvmExecutionResultToErrorDescription.ts b/packages/core/src/new-api/internal/journal/utils/failedEvmExecutionResultToErrorDescription.ts new file mode 100644 index 000000000..0c9aa020e --- /dev/null +++ b/packages/core/src/new-api/internal/journal/utils/failedEvmExecutionResultToErrorDescription.ts @@ -0,0 +1,37 @@ +import { + EvmExecutionResultTypes, + FailedEvmExecutionResult, +} from "../../new-execution/types/evm-execution"; + +export function failedEvmExecutionResultToErrorDescription( + result: FailedEvmExecutionResult +): string { + switch (result.type) { + case EvmExecutionResultTypes.INVALID_RESULT_ERROR: { + return `Transaction appears to have succeeded, but has returned invalid data: '${result.data}'`; + } + case EvmExecutionResultTypes.REVERT_WITHOUT_REASON: { + return `Transaction reverted`; + } + case EvmExecutionResultTypes.REVERT_WITH_REASON: { + return `Transaction reverted with reason: '${result.message}'`; + } + case EvmExecutionResultTypes.REVERT_WITH_PANIC_CODE: { + return `Transaction reverted with panic code (${result.panicCode}): '${result.panicName}'`; + } + case EvmExecutionResultTypes.REVERT_WITH_CUSTOM_ERROR: { + return `Transaction reverted with custom error: '${ + result.errorName + }' args: ${JSON.stringify(result.args.positional)}`; + } + case EvmExecutionResultTypes.REVERT_WITH_UNKNOWN_CUSTOM_ERROR: { + return `Transaction reverted with unknown custom error. Error signature: '${result.signature}' data: '${result.data}'`; + } + case EvmExecutionResultTypes.REVERT_WITH_INVALID_DATA: { + return `Transaction reverted with invalid error data: '${result.data}'`; + } + case EvmExecutionResultTypes.REVERT_WITH_INVALID_DATA_OR_UNKNOWN_CUSTOM_ERROR: { + return `Transaction reverted with unknown error. Error signature: '${result.signature}' data: '${result.data}'`; + } + } +} diff --git a/packages/core/src/new-api/internal/new-execution/deployment-state-helpers.ts b/packages/core/src/new-api/internal/new-execution/deployment-state-helpers.ts index 661e22575..243073d3c 100644 --- a/packages/core/src/new-api/internal/new-execution/deployment-state-helpers.ts +++ b/packages/core/src/new-api/internal/new-execution/deployment-state-helpers.ts @@ -18,6 +18,7 @@ export async function loadDeploymentState( deploymentLoader: DeploymentLoader ): Promise { let deploymentState: DeploymentState | undefined; + for await (const message of deploymentLoader.readFromJournal()) { deploymentState = deploymentStateReducer(deploymentState, message); } diff --git a/packages/core/src/new-api/internal/new-execution/execution-engine.ts b/packages/core/src/new-api/internal/new-execution/execution-engine.ts index 6afb29537..06b54d1d2 100644 --- a/packages/core/src/new-api/internal/new-execution/execution-engine.ts +++ b/packages/core/src/new-api/internal/new-execution/execution-engine.ts @@ -1,6 +1,10 @@ import { IgnitionError } from "../../../errors"; import { ArtifactResolver } from "../../types/artifact"; import { DeploymentParameters } from "../../types/deploy"; +import { + ExecutionEventListener, + ExecutionEventType, +} from "../../types/execution-events"; import { Future, IgnitionModule, @@ -33,6 +37,9 @@ export class ExecutionEngine { private readonly _artifactResolver: ArtifactResolver, private readonly _executionStrategy: ExecutionStrategy, private readonly _jsonRpcClient: JsonRpcClient, + private readonly _executionEventListener: + | ExecutionEventListener + | undefined, private readonly _requiredConfirmations: number, private readonly _millisecondBeforeBumpingFees: number, private readonly _maxFeeBumps: number, @@ -87,6 +94,8 @@ export class ExecutionEngine { const futures = getFuturesFromModule(module); for (const batch of batches) { + this._emitBeginNextBatchEvent(); + // TODO: consider changing batcher to return futures rather than ids const executionBatch = batch.map((futureId) => this._lookupFuture(futures, futureId) @@ -256,4 +265,15 @@ export class ExecutionEngine { return sortedBatch.map((f) => f.future); } + + /** + * Emits an execution event signaling that execution of the next batch has begun. + */ + private _emitBeginNextBatchEvent(): void { + if (this._executionEventListener !== undefined) { + this._executionEventListener.BEGIN_NEXT_BATCH({ + type: ExecutionEventType.BEGIN_NEXT_BATCH, + }); + } + } } diff --git a/packages/core/src/new-api/internal/new-execution/types/evm-execution.ts b/packages/core/src/new-api/internal/new-execution/types/evm-execution.ts index 51a54df1a..62f4e27c1 100644 --- a/packages/core/src/new-api/internal/new-execution/types/evm-execution.ts +++ b/packages/core/src/new-api/internal/new-execution/types/evm-execution.ts @@ -78,7 +78,7 @@ export interface SuccessfulEvmExecutionResult { } /** - * The execution was seemgly succseful, but the data returned by it was invalid. + * The execution was seemingly successful, but the data returned by it was invalid. */ export interface InvalidResultError { type: EvmExecutionResultTypes.INVALID_RESULT_ERROR; @@ -137,7 +137,7 @@ export interface RevertWithUnknownCustomError { /** * The execution failed due to some error whose kind we can recognize, but that - * we can't decode becase its data is invalid. This happens when the ABI decoding + * we can't decode because its data is invalid. This happens when the ABI decoding * of the error fails, or when a panic code is invalid. */ export interface RevertWithInvalidData { diff --git a/packages/core/src/new-api/internal/new-execution/types/messages.ts b/packages/core/src/new-api/internal/new-execution/types/messages.ts index b3e061049..d4353fe33 100644 --- a/packages/core/src/new-api/internal/new-execution/types/messages.ts +++ b/packages/core/src/new-api/internal/new-execution/types/messages.ts @@ -39,6 +39,12 @@ export type JournalMessage = | OnchainInteractionReplacedByUserMessage | OnchainInteractionTimeoutMessage; +/** + * NOTE: + * + * when adding/removing/changing any of these + * be sure to update UiEventType accordingly + */ export enum JournalMessageType { RUN_START = "RUN_START", WIPE_EXECUTION_STATE = "WIPE_EXECUTION_STATE", diff --git a/packages/core/src/new-api/internal/validation/validateStageOne.ts b/packages/core/src/new-api/internal/validation/validateStageOne.ts index d291e78fa..0fe750b8d 100644 --- a/packages/core/src/new-api/internal/validation/validateStageOne.ts +++ b/packages/core/src/new-api/internal/validation/validateStageOne.ts @@ -1,5 +1,11 @@ +import { IgnitionValidationError } from "../../../errors"; import { ArtifactResolver } from "../../types/artifact"; -import { FutureType, IgnitionModule } from "../../types/module"; +import { + DeploymentResultType, + ValidationErrorDeploymentResult, +} from "../../types/deploy"; +import { Future, FutureType, IgnitionModule } from "../../types/module"; +import { assertIgnitionInvariant } from "../utils/assertions"; import { getFuturesFromModule } from "../utils/get-futures-from-module"; import { validateArtifactContractAt } from "./stageOne/validateArtifactContractAt"; @@ -16,50 +22,65 @@ import { validateSendData } from "./stageOne/validateSendData"; export async function validateStageOne( module: IgnitionModule, artifactLoader: ArtifactResolver -): Promise { +): Promise { const futures = getFuturesFromModule(module); - // originally, I wrote a getSubmodulesFromModule function similar to the one above - // that recursively retrieved all submodules regardless of how deeply nested they were. - // however, by taking only the top level submodules of the current depth and recursively - // validating each of those, we achieve the same effect. - const submodules = module.submodules; - for (const submodule of submodules) { - await validateStageOne(submodule, artifactLoader); - } - for (const future of futures) { - switch (future.type) { - case FutureType.ARTIFACT_CONTRACT_DEPLOYMENT: - await validateArtifactContractDeployment(future, artifactLoader); - break; - case FutureType.ARTIFACT_LIBRARY_DEPLOYMENT: - await validateArtifactLibraryDeployment(future, artifactLoader); - break; - case FutureType.ARTIFACT_CONTRACT_AT: - await validateArtifactContractAt(future, artifactLoader); - break; - case FutureType.NAMED_CONTRACT_DEPLOYMENT: - await validateNamedContractDeployment(future, artifactLoader); - break; - case FutureType.NAMED_LIBRARY_DEPLOYMENT: - await validateNamedLibraryDeployment(future, artifactLoader); - break; - case FutureType.NAMED_CONTRACT_AT: - await validateNamedContractAt(future, artifactLoader); - break; - case FutureType.NAMED_CONTRACT_CALL: - await validateNamedContractCall(future, artifactLoader); - break; - case FutureType.NAMED_STATIC_CALL: - await validateNamedStaticCall(future, artifactLoader); - break; - case FutureType.READ_EVENT_ARGUMENT: - await validateReadEventArgument(future, artifactLoader); - break; - case FutureType.SEND_DATA: - await validateSendData(future, artifactLoader); - break; + try { + await _validateFuture(future, artifactLoader); + } catch (err) { + assertIgnitionInvariant( + err instanceof IgnitionValidationError, + `Expected an IgnitionValidationError when validating the future ${future.id}` + ); + + return { + type: DeploymentResultType.VALIDATION_ERROR, + errors: { + [future.id]: [err.message], + }, + }; } } + + // No validation errors + return null; +} + +async function _validateFuture( + future: Future, + artifactLoader: ArtifactResolver +): Promise { + switch (future.type) { + case FutureType.ARTIFACT_CONTRACT_DEPLOYMENT: + await validateArtifactContractDeployment(future, artifactLoader); + break; + case FutureType.ARTIFACT_LIBRARY_DEPLOYMENT: + await validateArtifactLibraryDeployment(future, artifactLoader); + break; + case FutureType.ARTIFACT_CONTRACT_AT: + await validateArtifactContractAt(future, artifactLoader); + break; + case FutureType.NAMED_CONTRACT_DEPLOYMENT: + await validateNamedContractDeployment(future, artifactLoader); + break; + case FutureType.NAMED_LIBRARY_DEPLOYMENT: + await validateNamedLibraryDeployment(future, artifactLoader); + break; + case FutureType.NAMED_CONTRACT_AT: + await validateNamedContractAt(future, artifactLoader); + break; + case FutureType.NAMED_CONTRACT_CALL: + await validateNamedContractCall(future, artifactLoader); + break; + case FutureType.NAMED_STATIC_CALL: + await validateNamedStaticCall(future, artifactLoader); + break; + case FutureType.READ_EVENT_ARGUMENT: + await validateReadEventArgument(future, artifactLoader); + break; + case FutureType.SEND_DATA: + await validateSendData(future, artifactLoader); + break; + } } diff --git a/packages/core/src/new-api/types/execution-events.ts b/packages/core/src/new-api/types/execution-events.ts new file mode 100644 index 000000000..f08f62b35 --- /dev/null +++ b/packages/core/src/new-api/types/execution-events.ts @@ -0,0 +1,434 @@ +import { DeploymentResult } from "./deploy"; +import { IgnitionModuleResult } from "./module"; + +/** + * Events emitted by the execution engine to allow tracking + * progress of a deploy. + * + * @beta + */ +export type ExecutionEvent = + | RunStartEvent + | WipeExecutionStateEvent + | DeploymentExecutionStateInitializeEvent + | DeploymentExecutionStateCompleteEvent + | CallExecutionStateInitializeEvent + | CallExecutionStateCompleteEvent + | StaticCallExecutionStateInitializeEvent + | StaticCallExecutionStateCompleteEvent + | SendDataExecutionStateInitializeEvent + | SendDataExecutionStateCompleteEvent + | ContractAtExecutionStateInitializeEvent + | ReadEventArgExecutionStateInitializeEvent + | NetworkInteractionRequestEvent + | TransactionSendEvent + | TransactionConfirmEvent + | StaticCallCompleteEvent + | OnchainInteractionBumpFeesEvent + | OnchainInteractionDroppedEvent + | OnchainInteractionReplacedByUserEvent + | OnchainInteractionTimeoutEvent + | BatchInitializeEvent + | DeploymentStartEvent + | BeginNextBatchEvent + | SetModuleIdEvent; + +/** + * The types of diagnostic events emitted during a deploy. + * + * @beta + */ +export enum ExecutionEventType { + RUN_START = "RUN_START", + WIPE_EXECUTION_STATE = "WIPE_EXECUTION_STATE", + DEPLOYMENT_EXECUTION_STATE_INITIALIZE = "DEPLOYMENT_EXECUTION_STATE_INITIALIZE", + DEPLOYMENT_EXECUTION_STATE_COMPLETE = "DEPLOYMENT_EXECUTION_STATE_COMPLETE", + CALL_EXECUTION_STATE_INITIALIZE = "CALL_EXECUTION_STATE_INITIALIZE", + CALL_EXECUTION_STATE_COMPLETE = "CALL_EXECUTION_STATE_COMPLETE", + STATIC_CALL_EXECUTION_STATE_INITIALIZE = "STATIC_CALL_EXECUTION_STATE_INITIALIZE", + STATIC_CALL_EXECUTION_STATE_COMPLETE = "STATIC_CALL_EXECUTION_STATE_COMPLETE", + SEND_DATA_EXECUTION_STATE_INITIALIZE = "SEND_DATA_EXECUTION_STATE_INITIALIZE", + SEND_DATA_EXECUTION_STATE_COMPLETE = "SEND_DATA_EXECUTION_STATE_COMPLETE", + CONTRACT_AT_EXECUTION_STATE_INITIALIZE = "CONTRACT_AT_EXECUTION_STATE_INITIALIZE", + READ_EVENT_ARGUMENT_EXECUTION_STATE_INITIALIZE = "READ_EVENT_ARGUMENT_EXECUTION_STATE_INITIALIZE", + NETWORK_INTERACTION_REQUEST = "NETWORK_INTERACTION_REQUEST", + TRANSACTION_SEND = "TRANSACTION_SEND", + TRANSACTION_CONFIRM = "TRANSACTION_CONFIRM", + STATIC_CALL_COMPLETE = "STATIC_CALL_COMPLETE", + ONCHAIN_INTERACTION_BUMP_FEES = "ONCHAIN_INTERACTION_BUMP_FEES", + ONCHAIN_INTERACTION_DROPPED = "ONCHAIN_INTERACTION_DROPPED", + ONCHAIN_INTERACTION_REPLACED_BY_USER = "ONCHAIN_INTERACTION_REPLACED_BY_USER", + ONCHAIN_INTERACTION_TIMEOUT = "ONCHAIN_INTERACTION_TIMEOUT", + BATCH_INITIALIZE = "BATCH_INITIALIZE", + DEPLOYMENT_START = "DEPLOYMENT_START", + BEGIN_NEXT_BATCH = "BEGIN_NEXT_BATCH", + DEPLOYMENT_COMPLETE = "DEPLOYMENT_COMPLETE", + SET_MODULE_ID = "SET_MODULE_ID", +} + +/** + * An event indicating that a deployment has started. + * + * @beta + */ +export interface DeploymentStartEvent { + type: ExecutionEventType.DEPLOYMENT_START; + moduleName: string; +} + +/** + * An event indicating a new run has started. + * + * @beta + */ +export interface RunStartEvent { + type: ExecutionEventType.RUN_START; + chainId: number; +} + +/** + * An event indicating that batches have been generated for a deployment run. + * + * @beta + */ +export interface BatchInitializeEvent { + type: ExecutionEventType.BATCH_INITIALIZE; + batches: string[][]; +} + +/** + * An event indicating that the execution engine has moved onto + * the next batch. + * + * @beta + */ +export interface BeginNextBatchEvent { + type: ExecutionEventType.BEGIN_NEXT_BATCH; +} + +/** + * An event indicating that a deployment has started. + * + * @beta + */ +export interface DeploymentCompleteEvent { + type: ExecutionEventType.DEPLOYMENT_COMPLETE; + result: DeploymentResult>; +} + +/** + * An event indicating a future that deploys a contract + * or library has started execution. + * + * @beta + */ +export interface DeploymentExecutionStateInitializeEvent { + type: ExecutionEventType.DEPLOYMENT_EXECUTION_STATE_INITIALIZE; + futureId: string; +} + +/** + * An event indicating that a future that deploys a contract + * or library has completed execution. + * + * @beta + */ +export interface DeploymentExecutionStateCompleteEvent { + type: ExecutionEventType.DEPLOYMENT_EXECUTION_STATE_COMPLETE; + futureId: string; + result: ExecutionEventResult; +} + +/** + * An event indicating a future that calls a function onchain + * via transactions has started execution. + * + * @beta + */ +export interface CallExecutionStateInitializeEvent { + type: ExecutionEventType.CALL_EXECUTION_STATE_INITIALIZE; + futureId: string; +} + +/** + * An event indicating a future that calls a function onchain + * via transactions has completed execution. + * + * @beta + */ +export interface CallExecutionStateCompleteEvent { + type: ExecutionEventType.CALL_EXECUTION_STATE_COMPLETE; + futureId: string; + result: ExecutionEventResult; +} + +/** + * An event indicating that a future that makes a static call + * has started execution. + * + * @beta + */ +export interface StaticCallExecutionStateInitializeEvent { + type: ExecutionEventType.STATIC_CALL_EXECUTION_STATE_INITIALIZE; + futureId: string; +} + +/** + * An event indicating that a future that makes a static call + * has completed execution. + * + * @beta + */ +export interface StaticCallExecutionStateCompleteEvent { + type: ExecutionEventType.STATIC_CALL_EXECUTION_STATE_COMPLETE; + futureId: string; + result: ExecutionEventResult; +} + +/** + * An event indicationing that a future that sends data or eth to a contract + * has started execution. + * + * @beta + */ +export interface SendDataExecutionStateInitializeEvent { + type: ExecutionEventType.SEND_DATA_EXECUTION_STATE_INITIALIZE; + futureId: string; +} + +/** + * An event indicationing that a future that sends data or eth to a contract + * has completed execution. + * + * @beta + */ +export interface SendDataExecutionStateCompleteEvent { + type: ExecutionEventType.SEND_DATA_EXECUTION_STATE_COMPLETE; + futureId: string; + result: ExecutionEventResult; +} + +/** + * An event indicating that a future that represents an existing contract + * has been initialized, there is no complete event as it initializes + * as complete. + * + * @beta + */ +export interface ContractAtExecutionStateInitializeEvent { + type: ExecutionEventType.CONTRACT_AT_EXECUTION_STATE_INITIALIZE; + futureId: string; +} + +/** + * An event indicating that a future that represents resolving an event + * from a previous futures onchain interaction, there is no complete event + * as it initializes as complete. + * + * @beta + */ +export interface ReadEventArgExecutionStateInitializeEvent { + type: ExecutionEventType.READ_EVENT_ARGUMENT_EXECUTION_STATE_INITIALIZE; + futureId: string; + result: ExecutionEventSuccess; +} + +/** + * An event indicating the user has clear the previous execution of a future. + * + * @beta + */ +export interface WipeExecutionStateEvent { + type: ExecutionEventType.WIPE_EXECUTION_STATE; + futureId: string; +} + +/** + * An event indicating that a future has requested a network interaction, + * either a transaction or a static call query. + * + * @beta + */ +export interface NetworkInteractionRequestEvent { + type: ExecutionEventType.NETWORK_INTERACTION_REQUEST; + networkInteractionType: ExecutionEventNetworkInteractionType; + futureId: string; +} + +/** + * An event indicating that a transaction has been sent to the network. + * + * @beta + */ +export interface TransactionSendEvent { + type: ExecutionEventType.TRANSACTION_SEND; + futureId: string; + hash: string; +} + +/** + * An event indicating has been detected as confirmed on-chain. + * + * @beta + */ +export interface TransactionConfirmEvent { + type: ExecutionEventType.TRANSACTION_CONFIRM; + futureId: string; + hash: string; +} + +/** + * An event indicating that a static call has been successfully run + * against the chain. + * + * @beta + */ +export interface StaticCallCompleteEvent { + type: ExecutionEventType.STATIC_CALL_COMPLETE; + futureId: string; +} + +/** + * An event indicating that a future's onchain interaction has had + * its its latest transaction fee bumped. + * + * @beta + */ +export interface OnchainInteractionBumpFeesEvent { + type: ExecutionEventType.ONCHAIN_INTERACTION_BUMP_FEES; + futureId: string; +} + +/** + * An event indicating that a future's onchain interaction has + * had its transaction dropped and will be resent. + * + * @beta + */ +export interface OnchainInteractionDroppedEvent { + type: ExecutionEventType.ONCHAIN_INTERACTION_DROPPED; + futureId: string; +} + +/** + * An event indicating that a future's onchain interaction has + * been replaced by a transaction from the user. + * + * @beta + */ +export interface OnchainInteractionReplacedByUserEvent { + type: ExecutionEventType.ONCHAIN_INTERACTION_REPLACED_BY_USER; + futureId: string; +} + +/** + * An event indicating that a future's onchain interaction has + * timed out. + * + * @beta + */ +export interface OnchainInteractionTimeoutEvent { + type: ExecutionEventType.ONCHAIN_INTERACTION_TIMEOUT; + futureId: string; +} + +/** + * An event indicating the current moduleId being validated. + * + * @beta + */ +export interface SetModuleIdEvent { + type: ExecutionEventType.SET_MODULE_ID; + moduleName: string; +} + +/** + * The types of network interactions that can be requested by a future. + * + * @beta + */ +export enum ExecutionEventNetworkInteractionType { + ONCHAIN_INTERACTION = "ONCHAIN_INTERACTION", + STATIC_CALL = "STATIC_CALL", +} + +/** + * The status of a future's completed execution. + * + * @beta + */ +export enum ExecutionEventResultType { + SUCCESS = "SUCCESS", + ERROR = "ERROR", +} + +/** + * The result of a future's completed execution. + * + * @beta + */ +export type ExecutionEventResult = ExecutionEventSuccess | ExecutionEventError; + +/** + * A successful result of a future's execution. + * + * @beta + */ +export interface ExecutionEventSuccess { + type: ExecutionEventResultType.SUCCESS; + result?: string; +} + +/** + * An errored result of a future's execution. + * + * @beta + */ +export interface ExecutionEventError { + type: ExecutionEventResultType.ERROR; + error: string; +} + +/** + * A mapping of execution event types to their corresponding event. + * + * @beta + */ +export interface ExecutionEventTypeMap { + [ExecutionEventType.RUN_START]: RunStartEvent; + [ExecutionEventType.WIPE_EXECUTION_STATE]: WipeExecutionStateEvent; + [ExecutionEventType.DEPLOYMENT_EXECUTION_STATE_INITIALIZE]: DeploymentExecutionStateInitializeEvent; + [ExecutionEventType.DEPLOYMENT_EXECUTION_STATE_COMPLETE]: DeploymentExecutionStateCompleteEvent; + [ExecutionEventType.CALL_EXECUTION_STATE_INITIALIZE]: CallExecutionStateInitializeEvent; + [ExecutionEventType.CALL_EXECUTION_STATE_COMPLETE]: CallExecutionStateCompleteEvent; + [ExecutionEventType.STATIC_CALL_EXECUTION_STATE_INITIALIZE]: StaticCallExecutionStateInitializeEvent; + [ExecutionEventType.STATIC_CALL_EXECUTION_STATE_COMPLETE]: StaticCallExecutionStateCompleteEvent; + [ExecutionEventType.SEND_DATA_EXECUTION_STATE_INITIALIZE]: SendDataExecutionStateInitializeEvent; + [ExecutionEventType.SEND_DATA_EXECUTION_STATE_COMPLETE]: SendDataExecutionStateCompleteEvent; + [ExecutionEventType.CONTRACT_AT_EXECUTION_STATE_INITIALIZE]: ContractAtExecutionStateInitializeEvent; + [ExecutionEventType.READ_EVENT_ARGUMENT_EXECUTION_STATE_INITIALIZE]: ReadEventArgExecutionStateInitializeEvent; + [ExecutionEventType.NETWORK_INTERACTION_REQUEST]: NetworkInteractionRequestEvent; + [ExecutionEventType.TRANSACTION_SEND]: TransactionSendEvent; + [ExecutionEventType.TRANSACTION_CONFIRM]: TransactionConfirmEvent; + [ExecutionEventType.STATIC_CALL_COMPLETE]: StaticCallCompleteEvent; + [ExecutionEventType.ONCHAIN_INTERACTION_BUMP_FEES]: OnchainInteractionBumpFeesEvent; + [ExecutionEventType.ONCHAIN_INTERACTION_DROPPED]: OnchainInteractionDroppedEvent; + [ExecutionEventType.ONCHAIN_INTERACTION_REPLACED_BY_USER]: OnchainInteractionReplacedByUserEvent; + [ExecutionEventType.ONCHAIN_INTERACTION_TIMEOUT]: OnchainInteractionTimeoutEvent; + [ExecutionEventType.BATCH_INITIALIZE]: BatchInitializeEvent; + [ExecutionEventType.DEPLOYMENT_START]: DeploymentStartEvent; + [ExecutionEventType.BEGIN_NEXT_BATCH]: BeginNextBatchEvent; + [ExecutionEventType.DEPLOYMENT_COMPLETE]: DeploymentCompleteEvent; + [ExecutionEventType.SET_MODULE_ID]: SetModuleIdEvent; +} + +/** + * A listener for execution events. + * + * @beta + */ +export type ExecutionEventListener = { + [eventType in ExecutionEventType]: ( + event: ExecutionEventTypeMap[eventType] + ) => void; +}; diff --git a/packages/core/src/new-api/wipe.ts b/packages/core/src/new-api/wipe.ts index 6d5ec8a9d..b3ae1c6b8 100644 --- a/packages/core/src/new-api/wipe.ts +++ b/packages/core/src/new-api/wipe.ts @@ -18,8 +18,8 @@ export async function wipe( ): Promise { const deploymentLoader = deploymentDir !== undefined - ? new FileDeploymentLoader(deploymentDir, false) - : new EphemeralDeploymentLoader(artifactResolver, false); + ? new FileDeploymentLoader(deploymentDir) + : new EphemeralDeploymentLoader(artifactResolver); const wiper = new Wiper(deploymentLoader); diff --git a/packages/core/test/new-api/reconciliation/helpers.ts b/packages/core/test/new-api/reconciliation/helpers.ts index 4db7366de..6b5025dfa 100644 --- a/packages/core/test/new-api/reconciliation/helpers.ts +++ b/packages/core/test/new-api/reconciliation/helpers.ts @@ -66,6 +66,10 @@ class MockDeploymentLoader implements DeploymentLoader { ): Promise { throw new Error("Method not implemented."); } + + public emitDeploymentBatchEvent(_batches: string[][]): void { + throw new Error("Method not implemented."); + } } class MockArtifactResolver implements ArtifactResolver { diff --git a/packages/core/test/new-api/types/deployment-loader.ts b/packages/core/test/new-api/types/deployment-loader.ts index 4cee095fc..4c80b6ac2 100644 --- a/packages/core/test/new-api/types/deployment-loader.ts +++ b/packages/core/test/new-api/types/deployment-loader.ts @@ -13,7 +13,7 @@ describe("DeploymentLoaderImpls", function () { const _implementation: ExactInterface< DeploymentLoader, FileDeploymentLoader - > = new FileDeploymentLoader("./example", true); + > = new FileDeploymentLoader("./example"); assert.isDefined(_implementation); }); @@ -24,7 +24,7 @@ describe("DeploymentLoaderImpls", function () { const _implementation: ExactInterface< DeploymentLoader, EphemeralDeploymentLoader - > = new EphemeralDeploymentLoader(setupMockArtifactResolver(), true); + > = new EphemeralDeploymentLoader(setupMockArtifactResolver()); assert.isDefined(_implementation); }); diff --git a/packages/core/test/new-api/wipe.ts b/packages/core/test/new-api/wipe.ts index 6861c625a..4d839334a 100644 --- a/packages/core/test/new-api/wipe.ts +++ b/packages/core/test/new-api/wipe.ts @@ -56,8 +56,7 @@ describe("wipe", () => { it("should allow wiping of future", async () => { const deploymentLoader = new EphemeralDeploymentLoader( - mockArtifactResolver, - false + mockArtifactResolver ); let deploymentState = await initializeDeploymentState( @@ -82,8 +81,7 @@ describe("wipe", () => { it("should error if the deployment hasn't been initialized", async () => { const deploymentLoader = new EphemeralDeploymentLoader( - mockArtifactResolver, - false + mockArtifactResolver ); const wiper = new Wiper(deploymentLoader); @@ -95,8 +93,7 @@ describe("wipe", () => { it("should error if the future id doesn't exist", async () => { const deploymentLoader = new EphemeralDeploymentLoader( - mockArtifactResolver, - false + mockArtifactResolver ); await initializeDeploymentState(123, deploymentLoader); @@ -110,8 +107,7 @@ describe("wipe", () => { it("should error if other futures are depenent on the future being wiped", async () => { const deploymentLoader = new EphemeralDeploymentLoader( - mockArtifactResolver, - false + mockArtifactResolver ); let deploymentState = await initializeDeploymentState( diff --git a/packages/hardhat-plugin/package.json b/packages/hardhat-plugin/package.json index 634eaee81..cab7f4221 100644 --- a/packages/hardhat-plugin/package.json +++ b/packages/hardhat-plugin/package.json @@ -42,10 +42,11 @@ "@types/dompurify": "2.4.0", "@types/fs-extra": "^9.0.13", "@types/lodash": "4.14.189", + "@types/lodash.flattendeep": "^4.4.7", "@types/mermaid": "^9.1.0", "@types/mocha": "^9.0.0", "@types/ndjson": "2.0.1", - "@types/node": "12.20.25", + "@types/node": "^16.0.0", "@types/prompts": "^2.4.2", "@types/sinon": "^10.0.13", "@typescript-eslint/eslint-plugin": "4.31.2", @@ -82,6 +83,7 @@ "fs-extra": "^10.0.0", "ink": "3.2.0", "ink-spinner": "4.0.3", + "lodash.flattendeep": "^4.4.0", "ndjson": "2.0.0", "prompts": "^2.4.2", "react": "18.2.0", diff --git a/packages/hardhat-plugin/src/hardhat-artifact-resolver.ts.ts b/packages/hardhat-plugin/src/hardhat-artifact-resolver.ts similarity index 100% rename from packages/hardhat-plugin/src/hardhat-artifact-resolver.ts.ts rename to packages/hardhat-plugin/src/hardhat-artifact-resolver.ts diff --git a/packages/hardhat-plugin/src/ignition-helper.ts b/packages/hardhat-plugin/src/ignition-helper.ts index e4c954e88..a44806da0 100644 --- a/packages/hardhat-plugin/src/ignition-helper.ts +++ b/packages/hardhat-plugin/src/ignition-helper.ts @@ -18,7 +18,7 @@ import { import { HardhatPluginError } from "hardhat/plugins"; import { HardhatRuntimeEnvironment } from "hardhat/types"; -import { HardhatArtifactResolver } from "./hardhat-artifact-resolver.ts"; +import { HardhatArtifactResolver } from "./hardhat-artifact-resolver"; import { errorDeploymentResultToExceptionMessage } from "./utils/error-deployment-result-to-exception-message"; export type DeployedContract = { @@ -84,7 +84,6 @@ export class IgnitionHelper { ignitionModule, deploymentParameters: parameters, accounts, - verbose: false, }); if (result.type !== DeploymentResultType.SUCCESSFUL_DEPLOYMENT) { diff --git a/packages/hardhat-plugin/src/index.ts b/packages/hardhat-plugin/src/index.ts index 387d8785d..345ca73e5 100644 --- a/packages/hardhat-plugin/src/index.ts +++ b/packages/hardhat-plugin/src/index.ts @@ -1,11 +1,7 @@ import { deploy, DeploymentParameters, - DeploymentResult, - DeploymentResultType, - IgnitionModuleResult, plan, - SuccessfulDeploymentResult, wipe, } from "@ignored/ignition-core"; import "@nomicfoundation/hardhat-ethers"; @@ -15,11 +11,12 @@ import { lazyObject } from "hardhat/plugins"; import path from "path"; import Prompt from "prompts"; -import { HardhatArtifactResolver } from "./hardhat-artifact-resolver.ts"; +import { HardhatArtifactResolver } from "./hardhat-artifact-resolver"; import { IgnitionHelper } from "./ignition-helper"; import { loadModule } from "./load-module"; import { writePlan } from "./plan/write-plan"; -import { errorDeploymentResultToExceptionMessage } from "./utils/error-deployment-result-to-exception-message"; +import { UiEventHandler } from "./ui/UiEventHandler"; +import { VerboseEventHandler } from "./ui/VerboseEventHandler"; import { open } from "./utils/open"; import "./type-extensions"; @@ -65,19 +62,19 @@ task("deploy") ) .addOptionalParam("id", "set the deployment id") .addFlag("force", "restart the deployment ignoring previous history") - .addFlag("logs", "output journal logs to the terminal") + .addFlag("useVerbose", "use verbose execution output instead of UI") .setAction( async ( { moduleNameOrPath, parameters: parametersInput, - logs, + useVerbose, id: givenDeploymentId, }: { moduleNameOrPath: string; parameters?: string; force: boolean; - logs: boolean; + useVerbose: boolean; id: string; }, hre @@ -139,18 +136,20 @@ task("deploy") const artifactResolver = new HardhatArtifactResolver(hre); - const result = await deploy({ + const executionEventListener = useVerbose + ? new VerboseEventHandler() + : new UiEventHandler(parameters); + + await deploy({ config: hre.config.ignition, provider: hre.network.provider, + executionEventListener, artifactResolver, deploymentDir, ignitionModule: userModule, deploymentParameters: parameters ?? {}, accounts, - verbose: logs, }); - - displayDeploymentResult(result); } ); @@ -321,29 +320,3 @@ function resolveParametersString(paramString: string): DeploymentParameters { process.exit(0); } } - -function displayDeploymentResult( - result: DeploymentResult> -): void { - switch (result.type) { - case DeploymentResultType.VALIDATION_ERROR: - case DeploymentResultType.RECONCILIATION_ERROR: - case DeploymentResultType.EXECUTION_ERROR: - return console.log(errorDeploymentResultToExceptionMessage(result)); - case DeploymentResultType.SUCCESSFUL_DEPLOYMENT: - return _displaySuccessfulDeployment(result); - } -} - -function _displaySuccessfulDeployment( - result: SuccessfulDeploymentResult> -): void { - console.log("Deployment complete"); - console.log(""); - - for (const [futureId, { contractName, address }] of Object.entries( - result.contracts - )) { - console.log(`${contractName} (${futureId}) - ${address}`); - } -} diff --git a/packages/hardhat-plugin/src/ui/UiEventHandler.tsx b/packages/hardhat-plugin/src/ui/UiEventHandler.tsx new file mode 100644 index 000000000..8fef64161 --- /dev/null +++ b/packages/hardhat-plugin/src/ui/UiEventHandler.tsx @@ -0,0 +1,490 @@ +import { + BatchInitializeEvent, + BeginNextBatchEvent, + CallExecutionStateCompleteEvent, + CallExecutionStateInitializeEvent, + ContractAtExecutionStateInitializeEvent, + DeploymentCompleteEvent, + DeploymentExecutionStateCompleteEvent, + DeploymentExecutionStateInitializeEvent, + DeploymentParameters, + DeploymentResult, + DeploymentResultType, + DeploymentStartEvent, + ExecutionEventListener, + ExecutionEventResult, + ExecutionEventResultType, + ExecutionEventType, + IgnitionError, + IgnitionModuleResult, + NetworkInteractionRequestEvent, + OnchainInteractionBumpFeesEvent, + OnchainInteractionDroppedEvent, + OnchainInteractionReplacedByUserEvent, + OnchainInteractionTimeoutEvent, + ReadEventArgExecutionStateInitializeEvent, + RunStartEvent, + SendDataExecutionStateCompleteEvent, + SendDataExecutionStateInitializeEvent, + SetModuleIdEvent, + StaticCallCompleteEvent, + StaticCallExecutionStateCompleteEvent, + StaticCallExecutionStateInitializeEvent, + TransactionConfirmEvent, + TransactionSendEvent, + WipeExecutionStateEvent, +} from "@ignored/ignition-core"; +import { render } from "ink"; + +import { IgnitionUi } from "./components"; +import { + UiBatches, + UiFuture, + UiFutureErrored, + UiFutureStatusType, + UiFutureSuccess, + UiState, + UiStateDeploymentStatus, +} from "./types"; + +interface RenderState { + rerender: null | ((node: React.ReactNode) => void); + unmount: null | (() => void); + waitUntilExit: null | (() => Promise); + clear: null | (() => void); +} + +export class UiEventHandler implements ExecutionEventListener { + private _renderState: RenderState = { + rerender: null, + unmount: null, + waitUntilExit: null, + clear: null, + }; + + private _uiState: UiState = { + status: UiStateDeploymentStatus.UNSTARTED, + chainId: null, + moduleName: null, + batches: [], + result: null, + }; + + constructor(private _deploymentParams: DeploymentParameters = {}) {} + + public get state(): UiState { + return this._uiState; + } + + public set state(uiState: UiState) { + this._uiState = uiState; + + this._renderToCli(); + } + + public [ExecutionEventType.RUN_START](event: RunStartEvent): void { + this.state = { + ...this.state, + chainId: event.chainId, + }; + } + + public [ExecutionEventType.WIPE_EXECUTION_STATE]( + event: WipeExecutionStateEvent + ): void { + const batches: UiBatches = []; + + for (const batch of this.state.batches) { + const futureBatch: UiFuture[] = []; + + for (const future of batch) { + if (future.futureId === event.futureId) { + continue; + } else { + futureBatch.push(future); + } + } + + batches.push(futureBatch); + } + + this.state = { + ...this.state, + batches, + }; + } + + public [ExecutionEventType.DEPLOYMENT_EXECUTION_STATE_INITIALIZE]( + event: DeploymentExecutionStateInitializeEvent + ): void { + const updatedFuture: UiFuture = { + futureId: event.futureId, + status: { + type: UiFutureStatusType.PENDING, + }, + }; + + this.state = { + ...this.state, + batches: this._applyUpdateToBatchFuture(updatedFuture), + }; + } + + public [ExecutionEventType.DEPLOYMENT_EXECUTION_STATE_COMPLETE]( + event: DeploymentExecutionStateCompleteEvent + ): void { + const updatedFuture: UiFuture = { + futureId: event.futureId, + status: this._getFutureStatusFromEventResult(event.result), + }; + + this.state = { + ...this.state, + batches: this._applyUpdateToBatchFuture(updatedFuture), + }; + } + + public [ExecutionEventType.CALL_EXECUTION_STATE_INITIALIZE]( + event: CallExecutionStateInitializeEvent + ): void { + const updatedFuture: UiFuture = { + futureId: event.futureId, + status: { + type: UiFutureStatusType.PENDING, + }, + }; + + this.state = { + ...this.state, + batches: this._applyUpdateToBatchFuture(updatedFuture), + }; + } + + public [ExecutionEventType.CALL_EXECUTION_STATE_COMPLETE]( + event: CallExecutionStateCompleteEvent + ): void { + const updatedFuture: UiFuture = { + futureId: event.futureId, + status: this._getFutureStatusFromEventResult(event.result), + }; + + this.state = { + ...this.state, + batches: this._applyUpdateToBatchFuture(updatedFuture), + }; + } + + public [ExecutionEventType.STATIC_CALL_EXECUTION_STATE_INITIALIZE]( + event: StaticCallExecutionStateInitializeEvent + ): void { + const updatedFuture: UiFuture = { + futureId: event.futureId, + status: { + type: UiFutureStatusType.PENDING, + }, + }; + + this.state = { + ...this.state, + batches: this._applyUpdateToBatchFuture(updatedFuture), + }; + } + + public [ExecutionEventType.STATIC_CALL_EXECUTION_STATE_COMPLETE]( + event: StaticCallExecutionStateCompleteEvent + ): void { + const updatedFuture: UiFuture = { + futureId: event.futureId, + status: this._getFutureStatusFromEventResult(event.result), + }; + + this.state = { + ...this.state, + batches: this._applyUpdateToBatchFuture(updatedFuture), + }; + } + + public [ExecutionEventType.SEND_DATA_EXECUTION_STATE_INITIALIZE]( + event: SendDataExecutionStateInitializeEvent + ): void { + const updatedFuture: UiFuture = { + futureId: event.futureId, + status: { + type: UiFutureStatusType.PENDING, + }, + }; + + this.state = { + ...this.state, + batches: this._applyUpdateToBatchFuture(updatedFuture), + }; + } + + public [ExecutionEventType.SEND_DATA_EXECUTION_STATE_COMPLETE]( + event: SendDataExecutionStateCompleteEvent + ): void { + const updatedFuture: UiFuture = { + futureId: event.futureId, + status: this._getFutureStatusFromEventResult(event.result), + }; + + this.state = { + ...this.state, + batches: this._applyUpdateToBatchFuture(updatedFuture), + }; + } + + public [ExecutionEventType.CONTRACT_AT_EXECUTION_STATE_INITIALIZE]( + event: ContractAtExecutionStateInitializeEvent + ): void { + const updatedFuture: UiFuture = { + futureId: event.futureId, + status: { + type: UiFutureStatusType.SUCCESS, + }, + }; + + this.state = { + ...this.state, + batches: this._applyUpdateToBatchFuture(updatedFuture), + }; + } + + public [ExecutionEventType.READ_EVENT_ARGUMENT_EXECUTION_STATE_INITIALIZE]( + event: ReadEventArgExecutionStateInitializeEvent + ): void { + const updatedFuture: UiFuture = { + futureId: event.futureId, + status: { + type: UiFutureStatusType.SUCCESS, + }, + }; + + this.state = { + ...this.state, + batches: this._applyUpdateToBatchFuture(updatedFuture), + }; + } + + public [ExecutionEventType.BATCH_INITIALIZE]( + event: BatchInitializeEvent + ): void { + const batches: UiBatches = []; + + for (const batch of event.batches) { + const futureBatch: UiFuture[] = []; + + for (const futureId of batch) { + futureBatch.push({ + futureId, + status: { + type: UiFutureStatusType.UNSTARTED, + }, + }); + } + + batches.push(futureBatch); + } + + this.state = { + ...this.state, + batches, + }; + } + + public [ExecutionEventType.NETWORK_INTERACTION_REQUEST]( + _event: NetworkInteractionRequestEvent + ): void {} + + public [ExecutionEventType.TRANSACTION_SEND]( + _event: TransactionSendEvent + ): void {} + + public [ExecutionEventType.TRANSACTION_CONFIRM]( + _event: TransactionConfirmEvent + ): void {} + + public [ExecutionEventType.STATIC_CALL_COMPLETE]( + _event: StaticCallCompleteEvent + ): void {} + + public [ExecutionEventType.ONCHAIN_INTERACTION_BUMP_FEES]( + _event: OnchainInteractionBumpFeesEvent + ): void {} + + public [ExecutionEventType.ONCHAIN_INTERACTION_DROPPED]( + _event: OnchainInteractionDroppedEvent + ): void {} + + public [ExecutionEventType.ONCHAIN_INTERACTION_REPLACED_BY_USER]( + _event: OnchainInteractionReplacedByUserEvent + ): void {} + + public [ExecutionEventType.ONCHAIN_INTERACTION_TIMEOUT]( + _event: OnchainInteractionTimeoutEvent + ): void {} + + public [ExecutionEventType.DEPLOYMENT_START]( + event: DeploymentStartEvent + ): void { + this.state = { + ...this.state, + status: UiStateDeploymentStatus.DEPLOYING, + moduleName: event.moduleName, + }; + } + + public [ExecutionEventType.BEGIN_NEXT_BATCH]( + _event: BeginNextBatchEvent + ): void {} + + public [ExecutionEventType.DEPLOYMENT_COMPLETE]( + event: DeploymentCompleteEvent + ): void { + this.state = { + ...this.state, + status: UiStateDeploymentStatus.COMPLETE, + result: event.result, + batches: this._applyResultToBatches(this.state.batches, event.result), + }; + } + + public [ExecutionEventType.SET_MODULE_ID](event: SetModuleIdEvent): void { + this.state = { + ...this.state, + moduleName: event.moduleName, + }; + } + + public unmountCli(): Promise { + if ( + this._renderState.unmount === null || + this._renderState.waitUntilExit === null || + this._renderState.clear === null + ) { + throw new IgnitionError("Cannot unmount with no unmount function"); + } + + this._renderState.clear(); + this._renderState.unmount(); + + return this._renderState.waitUntilExit(); + } + + private _renderToCli(): void { + if (this._renderState.rerender === null) { + const { rerender, unmount, waitUntilExit, clear } = render( + , + { patchConsole: false } + ); + + this._renderState.rerender = rerender; + this._renderState.unmount = unmount; + this._renderState.waitUntilExit = waitUntilExit; + this._renderState.clear = clear; + + return; + } + + this._renderState.rerender( + + ); + } + + private _applyUpdateToBatchFuture(updatedFuture: UiFuture): UiBatches { + const batches: UiBatches = []; + + for (const batch of this.state.batches) { + const futureBatch: UiFuture[] = []; + + for (const future of batch) { + if (future.futureId === updatedFuture.futureId) { + futureBatch.push(updatedFuture); + } else { + futureBatch.push(future); + } + } + + batches.push(futureBatch); + } + + return batches; + } + + private _getFutureStatusFromEventResult( + result: ExecutionEventResult + ): UiFutureSuccess | UiFutureErrored { + switch (result.type) { + case ExecutionEventResultType.SUCCESS: { + return { + type: UiFutureStatusType.SUCCESS, + result: result.result, + }; + } + case ExecutionEventResultType.ERROR: { + return { + type: UiFutureStatusType.ERRORED, + message: result.error, + }; + } + } + } + + private _applyResultToBatches( + batches: UiBatches, + result: DeploymentResult> + ): UiBatches { + const newBatches: UiBatches = []; + + for (const oldBatch of batches) { + const newBatch = []; + for (const future of oldBatch) { + const updatedFuture = this._hasUpdatedResult(future.futureId, result); + + newBatch.push(updatedFuture ?? future); + } + + newBatches.push(newBatch); + } + + return newBatches; + } + + private _hasUpdatedResult( + futureId: string, + result: DeploymentResult> + ): UiFuture | null { + if (result.type !== DeploymentResultType.EXECUTION_ERROR) { + return null; + } + + const failed = result.failed.find((f) => f.futureId === futureId); + + if (failed !== undefined) { + const f: UiFuture = { + futureId, + status: { + type: UiFutureStatusType.ERRORED, + message: failed.error, + }, + }; + + return f; + } + + const timedout = result.timedOut.find((f) => f.futureId === futureId); + + if (timedout !== undefined) { + const f: UiFuture = { + futureId, + status: { + type: UiFutureStatusType.PENDING, + }, + }; + + return f; + } + + return null; + } +} diff --git a/packages/hardhat-plugin/src/ui/VerboseEventHandler.ts b/packages/hardhat-plugin/src/ui/VerboseEventHandler.ts new file mode 100644 index 000000000..f6173aa8a --- /dev/null +++ b/packages/hardhat-plugin/src/ui/VerboseEventHandler.ts @@ -0,0 +1,242 @@ +import { + BatchInitializeEvent, + BeginNextBatchEvent, + CallExecutionStateCompleteEvent, + CallExecutionStateInitializeEvent, + ContractAtExecutionStateInitializeEvent, + DeploymentCompleteEvent, + DeploymentExecutionStateCompleteEvent, + DeploymentExecutionStateInitializeEvent, + DeploymentStartEvent, + ExecutionEventListener, + ExecutionEventNetworkInteractionType, + ExecutionEventResultType, + ExecutionEventType, + NetworkInteractionRequestEvent, + OnchainInteractionBumpFeesEvent, + OnchainInteractionDroppedEvent, + OnchainInteractionReplacedByUserEvent, + OnchainInteractionTimeoutEvent, + ReadEventArgExecutionStateInitializeEvent, + RunStartEvent, + SendDataExecutionStateCompleteEvent, + SendDataExecutionStateInitializeEvent, + SetModuleIdEvent, + StaticCallCompleteEvent, + StaticCallExecutionStateCompleteEvent, + StaticCallExecutionStateInitializeEvent, + TransactionConfirmEvent, + TransactionSendEvent, + WipeExecutionStateEvent, +} from "@ignored/ignition-core"; + +export class VerboseEventHandler implements ExecutionEventListener { + public [ExecutionEventType.RUN_START](event: RunStartEvent): void { + console.log(`Deployment started for chainId: ${event.chainId}`); + } + + public [ExecutionEventType.WIPE_EXECUTION_STATE]( + event: WipeExecutionStateEvent + ): void { + console.log(`Removing the execution of future ${event.futureId}`); + } + + public [ExecutionEventType.DEPLOYMENT_EXECUTION_STATE_INITIALIZE]( + event: DeploymentExecutionStateInitializeEvent + ): void { + console.log(`Starting to execute the deployment future ${event.futureId}`); + } + + public [ExecutionEventType.DEPLOYMENT_EXECUTION_STATE_COMPLETE]( + event: DeploymentExecutionStateCompleteEvent + ): void { + if (event.result.type === ExecutionEventResultType.SUCCESS) { + console.log( + `Successfully completed the execution of deployment future ${ + event.futureId + } with address ${event.result.result ?? "undefined"}` + ); + } else { + console.log( + `Execution of future ${event.futureId} failed with reason: ${event.result.error}` + ); + } + } + + public [ExecutionEventType.CALL_EXECUTION_STATE_INITIALIZE]( + event: CallExecutionStateInitializeEvent + ): void { + console.log(`Starting to execute the call future ${event.futureId}`); + } + + public [ExecutionEventType.CALL_EXECUTION_STATE_COMPLETE]( + event: CallExecutionStateCompleteEvent + ): void { + if (event.result.type === ExecutionEventResultType.SUCCESS) { + console.log( + `Successfully completed the execution of call future ${event.futureId}` + ); + } else { + console.log( + `Execution of future ${event.futureId} failed with reason: ${event.result.error}` + ); + } + } + + public [ExecutionEventType.STATIC_CALL_EXECUTION_STATE_INITIALIZE]( + event: StaticCallExecutionStateInitializeEvent + ): void { + console.log(`Starting to execute the static call future ${event.futureId}`); + } + + public [ExecutionEventType.STATIC_CALL_EXECUTION_STATE_COMPLETE]( + event: StaticCallExecutionStateCompleteEvent + ): void { + if (event.result.type === ExecutionEventResultType.SUCCESS) { + console.log( + `Successfully completed the execution of static call future ${ + event.futureId + } with result ${event.result.result ?? "undefined"}` + ); + } else { + console.log( + `Execution of future ${event.futureId} failed with reason: ${event.result.error}` + ); + } + } + + public [ExecutionEventType.SEND_DATA_EXECUTION_STATE_INITIALIZE]( + event: SendDataExecutionStateInitializeEvent + ): void { + console.log(`Started to execute the send data future ${event.futureId}`); + } + + public [ExecutionEventType.SEND_DATA_EXECUTION_STATE_COMPLETE]( + event: SendDataExecutionStateCompleteEvent + ): void { + if (event.result.type === ExecutionEventResultType.SUCCESS) { + console.log( + `Successfully completed the execution of send data future ${ + event.futureId + } in tx ${event.result.result ?? "undefined"}` + ); + } else { + console.log( + `Execution of future ${event.futureId} failed with reason: ${event.result.error}` + ); + } + } + + public [ExecutionEventType.CONTRACT_AT_EXECUTION_STATE_INITIALIZE]( + event: ContractAtExecutionStateInitializeEvent + ): void { + console.log(`Executed contract at future ${event.futureId}`); + } + + public [ExecutionEventType.READ_EVENT_ARGUMENT_EXECUTION_STATE_INITIALIZE]( + event: ReadEventArgExecutionStateInitializeEvent + ): void { + console.log( + `Executed read event argument future ${event.futureId} with result ${ + event.result.result ?? "undefined" + }` + ); + } + + public [ExecutionEventType.NETWORK_INTERACTION_REQUEST]( + event: NetworkInteractionRequestEvent + ): void { + if ( + event.networkInteractionType === + ExecutionEventNetworkInteractionType.ONCHAIN_INTERACTION + ) { + console.log( + `New onchain interaction requested for future ${event.futureId}` + ); + } else { + console.log(`New static call requested for future ${event.futureId}`); + } + } + + public [ExecutionEventType.TRANSACTION_SEND]( + event: TransactionSendEvent + ): void { + console.log( + `Transaction ${event.hash} sent for onchain interaction of future ${event.futureId}` + ); + } + + public [ExecutionEventType.TRANSACTION_CONFIRM]( + event: TransactionConfirmEvent + ): void { + console.log(`Transaction ${event.hash} confirmed`); + } + + public [ExecutionEventType.STATIC_CALL_COMPLETE]( + event: StaticCallCompleteEvent + ): void { + console.log(`Static call completed for future ${event.futureId}`); + } + + public [ExecutionEventType.ONCHAIN_INTERACTION_BUMP_FEES]( + event: OnchainInteractionBumpFeesEvent + ): void { + console.log( + `A transaction with higher fees will be sent for onchain interaction of future ${event.futureId}` + ); + } + + public [ExecutionEventType.ONCHAIN_INTERACTION_DROPPED]( + event: OnchainInteractionDroppedEvent + ): void { + console.log( + `Transactions for onchain interaction of future ${event.futureId} has been dropped and will be resent` + ); + } + + public [ExecutionEventType.ONCHAIN_INTERACTION_REPLACED_BY_USER]( + event: OnchainInteractionReplacedByUserEvent + ): void { + console.log( + `Transactions for onchain interaction of future ${event.futureId} has been replaced by the user and the onchain interaction exection will start again` + ); + } + + public [ExecutionEventType.ONCHAIN_INTERACTION_TIMEOUT]( + event: OnchainInteractionTimeoutEvent + ): void { + console.log( + `Onchain interaction of future ${event.futureId} failed due to being resent too many times and not having confirmed` + ); + } + + public [ExecutionEventType.BATCH_INITIALIZE]( + event: BatchInitializeEvent + ): void { + console.log( + `Starting execution for batches: ${JSON.stringify(event.batches)}` + ); + } + + public [ExecutionEventType.DEPLOYMENT_START]( + _event: DeploymentStartEvent + ): void { + console.log(`Starting execution for new deployment`); + } + + public [ExecutionEventType.BEGIN_NEXT_BATCH]( + _event: BeginNextBatchEvent + ): void { + console.log(`Starting execution for next batch`); + } + + public [ExecutionEventType.DEPLOYMENT_COMPLETE]( + _event: DeploymentCompleteEvent + ): void { + console.log(`Deployment complete`); + } + + public [ExecutionEventType.SET_MODULE_ID](event: SetModuleIdEvent): void { + console.log(`Starting validation for module: ${event.moduleName}`); + } +} diff --git a/packages/hardhat-plugin/src/ui/components/StartingPanel.tsx b/packages/hardhat-plugin/src/ui/components/StartingPanel.tsx new file mode 100644 index 000000000..0a1b24797 --- /dev/null +++ b/packages/hardhat-plugin/src/ui/components/StartingPanel.tsx @@ -0,0 +1,12 @@ +import { Box, Text } from "ink"; +import Spinner from "ink-spinner"; + +export const StartingPanel = () => { + return ( + + + Ignition starting + + + ); +}; diff --git a/packages/hardhat-plugin/src/ui/components/execution/AddressResults.tsx b/packages/hardhat-plugin/src/ui/components/execution/AddressResults.tsx new file mode 100644 index 000000000..8edd425a2 --- /dev/null +++ b/packages/hardhat-plugin/src/ui/components/execution/AddressResults.tsx @@ -0,0 +1,34 @@ +import { + IgnitionModuleResult, + SuccessfulDeploymentResult, +} from "@ignored/ignition-core"; +import { Box, Spacer, Text } from "ink"; + +import { NetworkInfo } from "./NetworkInfo"; + +export const AddressResults = ({ + contracts, + chainId, +}: { + contracts: SuccessfulDeploymentResult< + string, + IgnitionModuleResult + >["contracts"]; + chainId: number; +}) => { + return ( + + + Deployed Addresses + + + + + {Object.values(contracts).map((contract) => ( + + {contract.id} {`->`} {contract.address} + + ))} + + ); +}; diff --git a/packages/hardhat-plugin/src/ui/components/execution/BatchExecution.tsx b/packages/hardhat-plugin/src/ui/components/execution/BatchExecution.tsx new file mode 100644 index 000000000..e89a42529 --- /dev/null +++ b/packages/hardhat-plugin/src/ui/components/execution/BatchExecution.tsx @@ -0,0 +1,151 @@ +import { Box, Text } from "ink"; +import Spinner from "ink-spinner"; + +import { + UiFuture, + UiFutureStatusType, + UiState, + UiStateDeploymentStatus, +} from "../../types"; + +import { Divider } from "./Divider"; + +export const BatchExecution = ({ state }: { state: UiState }) => { + const isComplete = state.status === UiStateDeploymentStatus.COMPLETE; + + return ( + <> + + + + {isComplete ? ( + Execution complete + ) : ( + + Executing + + )} + + + {state.batches + .filter((batch, i) => isBatchDisplayable(batch, i)) + .map((batch, i) => ( + + ))} + + ); +}; + +const Batch = ({ batch, index }: { batch: UiFuture[]; index: number }) => { + const borderColor = resolveBatchBorderColor(batch); + + return ( + + + #{index} + + + {batch.map((future, i) => ( + + ))} + + ); +}; + +const Future = ({ future }: { future: UiFuture }) => { + const { borderColor, borderStyle, textColor } = resolveFutureColors(future); + + return ( + + + {future.futureId} + + ); +}; + +const StatusBadge = ({ future }: { future: UiFuture }) => { + let badge: any = " "; + switch (future.status.type) { + case UiFutureStatusType.UNSTARTED: + badge = ; + break; + case UiFutureStatusType.SUCCESS: + badge = ; + break; + case UiFutureStatusType.PENDING: + badge = 🔶; + break; + case UiFutureStatusType.ERRORED: + badge = ; + break; + } + + return ( + <> + + {badge} + + + ); +}; + +function resolveBatchBorderColor(futures: UiFuture[]) { + if (futures.some((v) => v.status.type === UiFutureStatusType.UNSTARTED)) { + return "lightgray"; + } + + if (futures.some((v) => v.status.type === UiFutureStatusType.ERRORED)) { + return "red"; + } + + if (futures.some((v) => v.status.type === UiFutureStatusType.PENDING)) { + return "yellow"; + } + + if (futures.every((v) => v.status.type === UiFutureStatusType.SUCCESS)) { + return "green"; + } + + return "lightgray"; +} + +function resolveFutureColors(future: UiFuture): { + borderColor: string; + borderStyle: "single" | "classic" | "bold" | "singleDouble"; + textColor: string; +} { + switch (future.status.type) { + case UiFutureStatusType.UNSTARTED: + return { + borderColor: "lightgray", + borderStyle: "singleDouble", + textColor: "white", + }; + case UiFutureStatusType.SUCCESS: + return { + borderColor: "greenBright", + borderStyle: "single", + textColor: "white", + }; + case UiFutureStatusType.PENDING: + return { + borderColor: "yellow", + borderStyle: "bold", + textColor: "white", + }; + case UiFutureStatusType.ERRORED: + return { + borderColor: "redBright", + borderStyle: "bold", + textColor: "white", + }; + } +} + +function isBatchDisplayable(batch: UiFuture[], index: number): boolean { + if (index === 0) { + return true; + } + + return batch.some((v) => v.status.type !== UiFutureStatusType.UNSTARTED); +} diff --git a/packages/hardhat-plugin/src/ui/components/execution/DeployParameters.tsx b/packages/hardhat-plugin/src/ui/components/execution/DeployParameters.tsx new file mode 100644 index 000000000..23c0c8cd6 --- /dev/null +++ b/packages/hardhat-plugin/src/ui/components/execution/DeployParameters.tsx @@ -0,0 +1,60 @@ +import type { + DeploymentParameters, + ModuleParameters, +} from "@ignored/ignition-core"; + +import { Text, Newline } from "ink"; + +export const DeployParameters = ({ + deployParams, +}: { + deployParams?: DeploymentParameters; +}) => { + if (deployParams === undefined) { + return null; + } + + const entries = Object.entries(deployParams); + + if (entries.length === 0) { + return null; + } + + const params = entries.map(([moduleId, moduleParams]) => { + return ( + + + Module: {moduleId} + + + + + + ); + }); + + return ( + + Deployment parameters: + + {...params} + + ); +}; + +const ModuleParams = ({ moduleParams }: { moduleParams: ModuleParameters }) => { + const entries = Object.entries(moduleParams); + + const params = entries.map(([paramId, paramValue]) => { + return ( + + + {paramId}: {JSON.stringify(paramValue)} + + + + ); + }); + + return {...params}; +}; diff --git a/packages/hardhat-plugin/src/ui/components/execution/Divider.tsx b/packages/hardhat-plugin/src/ui/components/execution/Divider.tsx new file mode 100644 index 000000000..b4a26f474 --- /dev/null +++ b/packages/hardhat-plugin/src/ui/components/execution/Divider.tsx @@ -0,0 +1,15 @@ +import { Box, Text } from "ink"; + +export const Divider = () => { + return ( + + + + {Array.from({ length: 400 }) + .map((_i) => "─") + .join("")} + + + + ); +}; diff --git a/packages/hardhat-plugin/src/ui/components/execution/ExecutionPanel.tsx b/packages/hardhat-plugin/src/ui/components/execution/ExecutionPanel.tsx new file mode 100644 index 000000000..5af85d561 --- /dev/null +++ b/packages/hardhat-plugin/src/ui/components/execution/ExecutionPanel.tsx @@ -0,0 +1,34 @@ +import type { DeploymentParameters } from "@ignored/ignition-core"; + +import { Box } from "ink"; + +import { UiState } from "../../types"; + +import { BatchExecution } from "./BatchExecution"; +import { FinalStatus } from "./FinalStatus"; +import { SummarySection } from "./SummarySection"; +import { viewEverythingExecutedAlready } from "./views"; + +export const ExecutionPanel = ({ + state, + deployParams, +}: { + state: UiState; + deployParams?: DeploymentParameters; +}) => { + if (viewEverythingExecutedAlready(state)) { + return ( + + + + ); + } + + return ( + + + + + + ); +}; diff --git a/packages/hardhat-plugin/src/ui/components/execution/FinalStatus.tsx b/packages/hardhat-plugin/src/ui/components/execution/FinalStatus.tsx new file mode 100644 index 000000000..4351daa5a --- /dev/null +++ b/packages/hardhat-plugin/src/ui/components/execution/FinalStatus.tsx @@ -0,0 +1,170 @@ +import { + DeploymentResultType, + ExecutionErrorDeploymentResult, + IgnitionModuleResult, + ReconciliationErrorDeploymentResult, + SuccessfulDeploymentResult, + ValidationErrorDeploymentResult, +} from "@ignored/ignition-core"; +import { Box, Newline, Text } from "ink"; + +import { UiState } from "../../types"; + +import { AddressResults } from "./AddressResults"; +import { Divider } from "./Divider"; + +export const FinalStatus = ({ state }: { state: UiState }) => { + if (state.result === null) { + return null; + } + + switch (state.result.type) { + case DeploymentResultType.VALIDATION_ERROR: { + return ( + + ); + } + case DeploymentResultType.RECONCILIATION_ERROR: { + return ( + + ); + } + case DeploymentResultType.EXECUTION_ERROR: { + return ( + + ); + } + case DeploymentResultType.SUCCESSFUL_DEPLOYMENT: { + return ( + + ); + } + } +}; + +const SuccessfulResult: React.FC<{ + moduleName: string; + chainId: number; + result: SuccessfulDeploymentResult>; +}> = ({ moduleName, chainId, result }) => { + return ( + + + + + 🚀 Deployment Complete for module{" "} + {moduleName} + + + + + + + ); +}; + +const ErrorResult: React.FC<{ + moduleName: string; + chainId: number; + message: string; + result: ReconciliationErrorDeploymentResult | ValidationErrorDeploymentResult; +}> = ({ moduleName, message, result }) => { + return ( + + + + + ⛔ {message} {moduleName} + + + + + + {Object.entries(result.errors).map(([futureId, futureErrors]) => ( + + {futureId} errors: + + {futureErrors.map((error, i) => ( + + {" "} + - {error} + + + ))} + + ))} + + + + + ); +}; + +const ExecutionErrorResult: React.FC<{ + moduleName: string; + chainId: number; + result: ExecutionErrorDeploymentResult; +}> = ({ moduleName, result }) => { + return ( + + + + + ⛔ Execution failed for module {moduleName} + + + + + + {result.timedOut.length > 0 && ( + + + Timed Out: + + + {result.timedOut.map(({ futureId, executionId }) => ( + + - {futureId}/{executionId} + + ))} + + )} + + {result.failed.length > 0 && ( + + + Failed: + + + + {result.failed.map(({ futureId, executionId, error }) => ( + + - {futureId}/{executionId}: {error} + + ))} + + )} + + + + + ); +}; diff --git a/packages/hardhat-plugin/src/ui/components/execution/NetworkInfo.tsx b/packages/hardhat-plugin/src/ui/components/execution/NetworkInfo.tsx new file mode 100644 index 000000000..a7eb0ba35 --- /dev/null +++ b/packages/hardhat-plugin/src/ui/components/execution/NetworkInfo.tsx @@ -0,0 +1,11 @@ +import { Text } from "ink"; + +export const NetworkInfo = ({ + networkInfo: { chainId }, +}: { + networkInfo: { + chainId: number; + }; +}) => { + return Chain ID: {chainId}; +}; diff --git a/packages/hardhat-plugin/src/ui/components/execution/SummarySection.tsx b/packages/hardhat-plugin/src/ui/components/execution/SummarySection.tsx new file mode 100644 index 000000000..043f6b897 --- /dev/null +++ b/packages/hardhat-plugin/src/ui/components/execution/SummarySection.tsx @@ -0,0 +1,28 @@ +import { DeploymentParameters } from "@ignored/ignition-core"; +import { Box, Text, Spacer } from "ink"; + +import { UiState } from "../../types"; + +import { DeployParameters } from "./DeployParameters"; +import { NetworkInfo } from "./NetworkInfo"; + +export const SummarySection = ({ + state: { chainId, moduleName }, + deployParams, +}: { + state: UiState; + deployParams?: DeploymentParameters; +}) => { + return ( + + + + Deploying module {moduleName} + + + + + + + ); +}; diff --git a/packages/hardhat-plugin/src/ui/components/execution/views.ts b/packages/hardhat-plugin/src/ui/components/execution/views.ts new file mode 100644 index 000000000..3dd1ca256 --- /dev/null +++ b/packages/hardhat-plugin/src/ui/components/execution/views.ts @@ -0,0 +1,16 @@ +import { UiState, UiStateDeploymentStatus } from "../../types"; + +/** + * Determine whether any on-chain executions happened in this + * run. An execution that requires on-chain updates in this + * run will have batches, a lack of batches indicates nothing + * to execute or execution happened on a previous run. + * @param state the deploy state + * @returns whether on-chain executions happened in this run + */ +export function viewEverythingExecutedAlready(state: UiState): boolean { + return ( + state.status !== UiStateDeploymentStatus.UNSTARTED && + state.batches.length === 0 + ); +} diff --git a/packages/hardhat-plugin/src/ui/components/index.tsx b/packages/hardhat-plugin/src/ui/components/index.tsx new file mode 100644 index 000000000..c17a043a7 --- /dev/null +++ b/packages/hardhat-plugin/src/ui/components/index.tsx @@ -0,0 +1,20 @@ +import type { DeploymentParameters } from "@ignored/ignition-core"; + +import { UiState, UiStateDeploymentStatus } from "../types"; + +import { StartingPanel } from "./StartingPanel"; +import { ExecutionPanel } from "./execution/ExecutionPanel"; + +export const IgnitionUi = ({ + state, + deployParams, +}: { + state: UiState; + deployParams?: DeploymentParameters; +}) => { + if (state.status === UiStateDeploymentStatus.UNSTARTED) { + return ; + } + + return ; +}; diff --git a/packages/hardhat-plugin/src/ui/types.ts b/packages/hardhat-plugin/src/ui/types.ts new file mode 100644 index 000000000..c99831e88 --- /dev/null +++ b/packages/hardhat-plugin/src/ui/types.ts @@ -0,0 +1,57 @@ +import { DeploymentResult, IgnitionModuleResult } from "@ignored/ignition-core"; + +export enum UiFutureStatusType { + UNSTARTED = "UNSTARTED", + SUCCESS = "SUCCESS", + PENDING = "PENDING", + ERRORED = "ERRORED", +} + +export enum UiStateDeploymentStatus { + UNSTARTED = "UNSTARTED", + DEPLOYING = "DEPLOYING", + COMPLETE = "COMPLETE", +} + +export interface UiFutureUnstarted { + type: UiFutureStatusType.UNSTARTED; +} + +export interface UiFutureSuccess { + type: UiFutureStatusType.SUCCESS; + result?: string; +} + +export interface UiFuturePending { + type: UiFutureStatusType.PENDING; +} + +export interface UiFutureErrored { + type: UiFutureStatusType.ERRORED; + message: string; +} + +export type UiFutureStatus = + | UiFutureUnstarted + | UiFutureSuccess + | UiFuturePending + | UiFutureErrored; + +export interface UiFuture { + status: UiFutureStatus; + futureId: string; +} + +export type UiBatches = UiFuture[][]; + +export interface UiState { + status: UiStateDeploymentStatus; + chainId: number | null; + moduleName: string | null; + batches: UiBatches; + result: DeploymentResult> | null; +} + +export interface AddressMap { + [label: string]: string; +}