gate-evm-tools 1.0.26 → 1.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/bin/workflow.cjs +19 -0
- package/dist/core/BytecodeChecker.d.ts.map +1 -1
- package/dist/core/BytecodeChecker.js +16 -1
- package/dist/core/BytecodeChecker.js.map +1 -1
- package/dist/core/legacyProxyArtifact.d.ts +6 -0
- package/dist/core/legacyProxyArtifact.d.ts.map +1 -0
- package/dist/core/legacyProxyArtifact.js +36 -0
- package/dist/core/legacyProxyArtifact.js.map +1 -0
- package/dist-runtime/config/hardhatAdapters.d.ts +7 -0
- package/dist-runtime/config/hardhatAdapters.js +61 -0
- package/dist-runtime/config/hardhatAdapters.js.map +1 -0
- package/dist-runtime/config/networks.d.ts +32 -0
- package/dist-runtime/config/networks.js +275 -0
- package/dist-runtime/config/networks.js.map +1 -0
- package/dist-runtime/config/openzeppelin.d.ts +28 -0
- package/dist-runtime/config/openzeppelin.js +144 -0
- package/dist-runtime/config/openzeppelin.js.map +1 -0
- package/dist-runtime/config/rpc.d.ts +7 -0
- package/dist-runtime/config/rpc.js +19 -0
- package/dist-runtime/config/rpc.js.map +1 -0
- package/dist-runtime/config/zksyncSolcPath.d.ts +1 -0
- package/dist-runtime/config/zksyncSolcPath.js +61 -0
- package/dist-runtime/config/zksyncSolcPath.js.map +1 -0
- package/dist-runtime/migrate.d.ts +13 -0
- package/dist-runtime/migrate.js +208 -0
- package/dist-runtime/migrate.js.map +1 -0
- package/dist-runtime/package.json +1 -0
- package/dist-runtime/projectHardhat.d.ts +4 -0
- package/dist-runtime/projectHardhat.js +16 -0
- package/dist-runtime/projectHardhat.js.map +1 -0
- package/dist-runtime/runtime-identity.json +4 -0
- package/dist-runtime/template-migration.json +293 -0
- package/dist-runtime/workflow/checkCode.d.ts +1 -0
- package/dist-runtime/workflow/checkCode.js +16 -0
- package/dist-runtime/workflow/checkCode.js.map +1 -0
- package/dist-runtime/workflow/checkCodeFinalize.d.ts +1 -0
- package/dist-runtime/workflow/checkCodeFinalize.js +11 -0
- package/dist-runtime/workflow/checkCodeFinalize.js.map +1 -0
- package/dist-runtime/workflow/checkCodePlan.d.ts +1 -0
- package/dist-runtime/workflow/checkCodePlan.js +11 -0
- package/dist-runtime/workflow/checkCodePlan.js.map +1 -0
- package/dist-runtime/workflow/cli.d.ts +1 -0
- package/dist-runtime/workflow/cli.js +302 -0
- package/dist-runtime/workflow/cli.js.map +1 -0
- package/dist-runtime/workflow/commitContractInfo.d.ts +1 -0
- package/dist-runtime/workflow/commitContractInfo.js +64 -0
- package/dist-runtime/workflow/commitContractInfo.js.map +1 -0
- package/dist-runtime/workflow/create2Hashes.d.ts +1 -0
- package/dist-runtime/workflow/create2Hashes.js +76 -0
- package/dist-runtime/workflow/create2Hashes.js.map +1 -0
- package/dist-runtime/workflow/forkSourceProxy.d.ts +1 -0
- package/dist-runtime/workflow/forkSourceProxy.js +151 -0
- package/dist-runtime/workflow/forkSourceProxy.js.map +1 -0
- package/dist-runtime/workflow/importState.d.ts +1 -0
- package/dist-runtime/workflow/importState.js +193 -0
- package/dist-runtime/workflow/importState.js.map +1 -0
- package/dist-runtime/workflow/lib/authority.d.ts +28 -0
- package/dist-runtime/workflow/lib/authority.js +122 -0
- package/dist-runtime/workflow/lib/authority.js.map +1 -0
- package/dist-runtime/workflow/lib/calldata.d.ts +2 -0
- package/dist-runtime/workflow/lib/calldata.js +8 -0
- package/dist-runtime/workflow/lib/calldata.js.map +1 -0
- package/dist-runtime/workflow/lib/calls.d.ts +23 -0
- package/dist-runtime/workflow/lib/calls.js +316 -0
- package/dist-runtime/workflow/lib/calls.js.map +1 -0
- package/dist-runtime/workflow/lib/checkCode/args.d.ts +2 -0
- package/dist-runtime/workflow/lib/checkCode/args.js +68 -0
- package/dist-runtime/workflow/lib/checkCode/args.js.map +1 -0
- package/dist-runtime/workflow/lib/checkCode/command.d.ts +5 -0
- package/dist-runtime/workflow/lib/checkCode/command.js +35 -0
- package/dist-runtime/workflow/lib/checkCode/command.js.map +1 -0
- package/dist-runtime/workflow/lib/checkCode/dependencies.d.ts +6 -0
- package/dist-runtime/workflow/lib/checkCode/dependencies.js +125 -0
- package/dist-runtime/workflow/lib/checkCode/dependencies.js.map +1 -0
- package/dist-runtime/workflow/lib/checkCode/finalize.d.ts +17 -0
- package/dist-runtime/workflow/lib/checkCode/finalize.js +174 -0
- package/dist-runtime/workflow/lib/checkCode/finalize.js.map +1 -0
- package/dist-runtime/workflow/lib/checkCode/index.d.ts +3 -0
- package/dist-runtime/workflow/lib/checkCode/index.js +20 -0
- package/dist-runtime/workflow/lib/checkCode/index.js.map +1 -0
- package/dist-runtime/workflow/lib/checkCode/plan.d.ts +22 -0
- package/dist-runtime/workflow/lib/checkCode/plan.js +145 -0
- package/dist-runtime/workflow/lib/checkCode/plan.js.map +1 -0
- package/dist-runtime/workflow/lib/checkCodeReport.d.ts +25 -0
- package/dist-runtime/workflow/lib/checkCodeReport.js +102 -0
- package/dist-runtime/workflow/lib/checkCodeReport.js.map +1 -0
- package/dist-runtime/workflow/lib/checkCodeRunner.d.ts +87 -0
- package/dist-runtime/workflow/lib/checkCodeRunner.js +964 -0
- package/dist-runtime/workflow/lib/checkCodeRunner.js.map +1 -0
- package/dist-runtime/workflow/lib/cliTool.d.ts +10 -0
- package/dist-runtime/workflow/lib/cliTool.js +25 -0
- package/dist-runtime/workflow/lib/cliTool.js.map +1 -0
- package/dist-runtime/workflow/lib/config.d.ts +33 -0
- package/dist-runtime/workflow/lib/config.js +100 -0
- package/dist-runtime/workflow/lib/config.js.map +1 -0
- package/dist-runtime/workflow/lib/contractInfoCandidates.d.ts +82 -0
- package/dist-runtime/workflow/lib/contractInfoCandidates.js +441 -0
- package/dist-runtime/workflow/lib/contractInfoCandidates.js.map +1 -0
- package/dist-runtime/workflow/lib/controlledMulticallAbi.d.ts +2 -0
- package/dist-runtime/workflow/lib/controlledMulticallAbi.js +10 -0
- package/dist-runtime/workflow/lib/controlledMulticallAbi.js.map +1 -0
- package/dist-runtime/workflow/lib/create2.d.ts +49 -0
- package/dist-runtime/workflow/lib/create2.js +194 -0
- package/dist-runtime/workflow/lib/create2.js.map +1 -0
- package/dist-runtime/workflow/lib/create2Hashes.d.ts +22 -0
- package/dist-runtime/workflow/lib/create2Hashes.js +57 -0
- package/dist-runtime/workflow/lib/create2Hashes.js.map +1 -0
- package/dist-runtime/workflow/lib/deploymentInfo.d.ts +40 -0
- package/dist-runtime/workflow/lib/deploymentInfo.js +240 -0
- package/dist-runtime/workflow/lib/deploymentInfo.js.map +1 -0
- package/dist-runtime/workflow/lib/deploymentRecords.d.ts +137 -0
- package/dist-runtime/workflow/lib/deploymentRecords.js +374 -0
- package/dist-runtime/workflow/lib/deploymentRecords.js.map +1 -0
- package/dist-runtime/workflow/lib/deployments.d.ts +21 -0
- package/dist-runtime/workflow/lib/deployments.js +240 -0
- package/dist-runtime/workflow/lib/deployments.js.map +1 -0
- package/dist-runtime/workflow/lib/erc1967.d.ts +5 -0
- package/dist-runtime/workflow/lib/erc1967.js +29 -0
- package/dist-runtime/workflow/lib/erc1967.js.map +1 -0
- package/dist-runtime/workflow/lib/errors.d.ts +6 -0
- package/dist-runtime/workflow/lib/errors.js +77 -0
- package/dist-runtime/workflow/lib/errors.js.map +1 -0
- package/dist-runtime/workflow/lib/executionId.d.ts +7 -0
- package/dist-runtime/workflow/lib/executionId.js +26 -0
- package/dist-runtime/workflow/lib/executionId.js.map +1 -0
- package/dist-runtime/workflow/lib/executionPlan.d.ts +85 -0
- package/dist-runtime/workflow/lib/executionPlan.js +450 -0
- package/dist-runtime/workflow/lib/executionPlan.js.map +1 -0
- package/dist-runtime/workflow/lib/executionProgress.d.ts +5 -0
- package/dist-runtime/workflow/lib/executionProgress.js +100 -0
- package/dist-runtime/workflow/lib/executionProgress.js.map +1 -0
- package/dist-runtime/workflow/lib/external.d.ts +62 -0
- package/dist-runtime/workflow/lib/external.js +282 -0
- package/dist-runtime/workflow/lib/external.js.map +1 -0
- package/dist-runtime/workflow/lib/externalExecutionScanner.d.ts +64 -0
- package/dist-runtime/workflow/lib/externalExecutionScanner.js +638 -0
- package/dist-runtime/workflow/lib/externalExecutionScanner.js.map +1 -0
- package/dist-runtime/workflow/lib/forkSnapshot.d.ts +34 -0
- package/dist-runtime/workflow/lib/forkSnapshot.js +167 -0
- package/dist-runtime/workflow/lib/forkSnapshot.js.map +1 -0
- package/dist-runtime/workflow/lib/gateTool.d.ts +16 -0
- package/dist-runtime/workflow/lib/gateTool.js +177 -0
- package/dist-runtime/workflow/lib/gateTool.js.map +1 -0
- package/dist-runtime/workflow/lib/gateToolSync.d.ts +22 -0
- package/dist-runtime/workflow/lib/gateToolSync.js +253 -0
- package/dist-runtime/workflow/lib/gateToolSync.js.map +1 -0
- package/dist-runtime/workflow/lib/lock.d.ts +52 -0
- package/dist-runtime/workflow/lib/lock.js +269 -0
- package/dist-runtime/workflow/lib/lock.js.map +1 -0
- package/dist-runtime/workflow/lib/migrationState.d.ts +103 -0
- package/dist-runtime/workflow/lib/migrationState.js +920 -0
- package/dist-runtime/workflow/lib/migrationState.js.map +1 -0
- package/dist-runtime/workflow/lib/operatorExecution.d.ts +28 -0
- package/dist-runtime/workflow/lib/operatorExecution.js +134 -0
- package/dist-runtime/workflow/lib/operatorExecution.js.map +1 -0
- package/dist-runtime/workflow/lib/parameters.d.ts +58 -0
- package/dist-runtime/workflow/lib/parameters.js +323 -0
- package/dist-runtime/workflow/lib/parameters.js.map +1 -0
- package/dist-runtime/workflow/lib/pendingDeploymentReuse.d.ts +29 -0
- package/dist-runtime/workflow/lib/pendingDeploymentReuse.js +277 -0
- package/dist-runtime/workflow/lib/pendingDeploymentReuse.js.map +1 -0
- package/dist-runtime/workflow/lib/proxy.d.ts +18 -0
- package/dist-runtime/workflow/lib/proxy.js +19 -0
- package/dist-runtime/workflow/lib/proxy.js.map +1 -0
- package/dist-runtime/workflow/lib/registryCommit.d.ts +33 -0
- package/dist-runtime/workflow/lib/registryCommit.js +319 -0
- package/dist-runtime/workflow/lib/registryCommit.js.map +1 -0
- package/dist-runtime/workflow/lib/release.d.ts +57 -0
- package/dist-runtime/workflow/lib/release.js +256 -0
- package/dist-runtime/workflow/lib/release.js.map +1 -0
- package/dist-runtime/workflow/lib/releaseFinalize.d.ts +14 -0
- package/dist-runtime/workflow/lib/releaseFinalize.js +571 -0
- package/dist-runtime/workflow/lib/releaseFinalize.js.map +1 -0
- package/dist-runtime/workflow/lib/releaseParameters.d.ts +43 -0
- package/dist-runtime/workflow/lib/releaseParameters.js +184 -0
- package/dist-runtime/workflow/lib/releaseParameters.js.map +1 -0
- package/dist-runtime/workflow/lib/reporter.d.ts +39 -0
- package/dist-runtime/workflow/lib/reporter.js +452 -0
- package/dist-runtime/workflow/lib/reporter.js.map +1 -0
- package/dist-runtime/workflow/lib/resultPaths.d.ts +17 -0
- package/dist-runtime/workflow/lib/resultPaths.js +29 -0
- package/dist-runtime/workflow/lib/resultPaths.js.map +1 -0
- package/dist-runtime/workflow/lib/runtimeVersion.d.ts +10 -0
- package/dist-runtime/workflow/lib/runtimeVersion.js +46 -0
- package/dist-runtime/workflow/lib/runtimeVersion.js.map +1 -0
- package/dist-runtime/workflow/lib/signer.d.ts +10 -0
- package/dist-runtime/workflow/lib/signer.js +105 -0
- package/dist-runtime/workflow/lib/signer.js.map +1 -0
- package/dist-runtime/workflow/lib/signerIdentity.d.ts +1 -0
- package/dist-runtime/workflow/lib/signerIdentity.js +23 -0
- package/dist-runtime/workflow/lib/signerIdentity.js.map +1 -0
- package/dist-runtime/workflow/lib/simulation.d.ts +35 -0
- package/dist-runtime/workflow/lib/simulation.js +282 -0
- package/dist-runtime/workflow/lib/simulation.js.map +1 -0
- package/dist-runtime/workflow/lib/simulationCallers.d.ts +44 -0
- package/dist-runtime/workflow/lib/simulationCallers.js +260 -0
- package/dist-runtime/workflow/lib/simulationCallers.js.map +1 -0
- package/dist-runtime/workflow/lib/sourceRpcPolicy.d.ts +1 -0
- package/dist-runtime/workflow/lib/sourceRpcPolicy.js +43 -0
- package/dist-runtime/workflow/lib/sourceRpcPolicy.js.map +1 -0
- package/dist-runtime/workflow/lib/state.d.ts +6 -0
- package/dist-runtime/workflow/lib/state.js +25 -0
- package/dist-runtime/workflow/lib/state.js.map +1 -0
- package/dist-runtime/workflow/lib/staticValidation.d.ts +22 -0
- package/dist-runtime/workflow/lib/staticValidation.js +124 -0
- package/dist-runtime/workflow/lib/staticValidation.js.map +1 -0
- package/dist-runtime/workflow/lib/zksyncForkCapability.d.ts +27 -0
- package/dist-runtime/workflow/lib/zksyncForkCapability.js +52 -0
- package/dist-runtime/workflow/lib/zksyncForkCapability.js.map +1 -0
- package/dist-runtime/workflow/lib/zksyncForkRunner.d.ts +57 -0
- package/dist-runtime/workflow/lib/zksyncForkRunner.js +344 -0
- package/dist-runtime/workflow/lib/zksyncForkRunner.js.map +1 -0
- package/dist-runtime/workflow/reconcileExternal.d.ts +1 -0
- package/dist-runtime/workflow/reconcileExternal.js +87 -0
- package/dist-runtime/workflow/reconcileExternal.js.map +1 -0
- package/dist-runtime/workflow/reconcileExternalRun.d.ts +1 -0
- package/dist-runtime/workflow/reconcileExternalRun.js +77 -0
- package/dist-runtime/workflow/reconcileExternalRun.js.map +1 -0
- package/dist-runtime/workflow/releaseFinalize.d.ts +1 -0
- package/dist-runtime/workflow/releaseFinalize.js +11 -0
- package/dist-runtime/workflow/releaseFinalize.js.map +1 -0
- package/dist-runtime/workflow/run.d.ts +40 -0
- package/dist-runtime/workflow/run.js +858 -0
- package/dist-runtime/workflow/run.js.map +1 -0
- package/dist-runtime/workflow/staticValidate.d.ts +1 -0
- package/dist-runtime/workflow/staticValidate.js +26 -0
- package/dist-runtime/workflow/staticValidate.js.map +1 -0
- package/dist-runtime/workflow/steps/call.d.ts +3 -0
- package/dist-runtime/workflow/steps/call.js +9 -0
- package/dist-runtime/workflow/steps/call.js.map +1 -0
- package/dist-runtime/workflow/steps/check.d.ts +3 -0
- package/dist-runtime/workflow/steps/check.js +9 -0
- package/dist-runtime/workflow/steps/check.js.map +1 -0
- package/dist-runtime/workflow/steps/custom.d.ts +3 -0
- package/dist-runtime/workflow/steps/custom.js +33 -0
- package/dist-runtime/workflow/steps/custom.js.map +1 -0
- package/dist-runtime/workflow/steps/deploy.d.ts +3 -0
- package/dist-runtime/workflow/steps/deploy.js +596 -0
- package/dist-runtime/workflow/steps/deploy.js.map +1 -0
- package/dist-runtime/workflow/steps/upgrade.d.ts +3 -0
- package/dist-runtime/workflow/steps/upgrade.js +466 -0
- package/dist-runtime/workflow/steps/upgrade.js.map +1 -0
- package/dist-runtime/workflow/steps/view.d.ts +3 -0
- package/dist-runtime/workflow/steps/view.js +9 -0
- package/dist-runtime/workflow/steps/view.js.map +1 -0
- package/docs/direct-package-acceptance.md +20 -0
- package/docs/external-infrastructure-acceptance.md +33 -0
- package/docs/network-runtime-acceptance.md +18 -0
- package/docs/plans/2026-09-09-workflow-extraction.md +16 -0
- package/docs/superpowers/plans/2026-07-22-manifest-bootstrap-transaction.md +50 -0
- package/docs/superpowers/plans/2026-07-22-manifest-review-remediation.md +49 -0
- package/docs/superpowers/plans/2026-08-17-custody-hardhat-signers.md +227 -0
- package/docs/superpowers/plans/2026-09-09-remove-template-infrastructure-contracts.md +11 -0
- package/docs/workflow-reference.md +1181 -0
- package/docs/workflow-runtime-acceptance.md +17 -0
- package/docs/workflow-runtime.md +56 -0
- package/package.json +26 -5
- package/runtime/config/hardhatAdapters.ts +58 -0
- package/runtime/config/networks.ts +305 -0
- package/runtime/config/openzeppelin.ts +173 -0
- package/runtime/config/rpc.ts +27 -0
- package/runtime/config/zksyncSolcPath.ts +59 -0
- package/runtime/hardhat-types.d.ts +2 -0
- package/runtime/migrate.ts +154 -0
- package/runtime/projectHardhat.ts +10 -0
- package/runtime/template-migration.json +293 -0
- package/runtime/workflow/checkCode.ts +13 -0
- package/runtime/workflow/checkCodeFinalize.ts +8 -0
- package/runtime/workflow/checkCodePlan.ts +8 -0
- package/runtime/workflow/cli.ts +356 -0
- package/runtime/workflow/commitContractInfo.ts +63 -0
- package/runtime/workflow/create2Hashes.ts +79 -0
- package/runtime/workflow/forkSourceProxy.ts +193 -0
- package/runtime/workflow/importState.ts +190 -0
- package/runtime/workflow/lib/authority.ts +170 -0
- package/runtime/workflow/lib/calldata.ts +9 -0
- package/runtime/workflow/lib/calls.ts +407 -0
- package/runtime/workflow/lib/checkCode/args.ts +76 -0
- package/runtime/workflow/lib/checkCode/command.ts +36 -0
- package/runtime/workflow/lib/checkCode/dependencies.ts +112 -0
- package/runtime/workflow/lib/checkCode/finalize.ts +198 -0
- package/runtime/workflow/lib/checkCode/index.ts +18 -0
- package/runtime/workflow/lib/checkCode/plan.ts +163 -0
- package/runtime/workflow/lib/checkCodeReport.ts +133 -0
- package/runtime/workflow/lib/checkCodeRunner.ts +1225 -0
- package/runtime/workflow/lib/cliTool.ts +31 -0
- package/runtime/workflow/lib/config.ts +130 -0
- package/runtime/workflow/lib/contractInfoCandidates.ts +690 -0
- package/runtime/workflow/lib/controlledMulticallAbi.ts +6 -0
- package/runtime/workflow/lib/create2.ts +278 -0
- package/runtime/workflow/lib/create2Hashes.ts +83 -0
- package/runtime/workflow/lib/deploymentInfo.ts +286 -0
- package/runtime/workflow/lib/deploymentRecords.ts +576 -0
- package/runtime/workflow/lib/deployments.ts +276 -0
- package/runtime/workflow/lib/erc1967.ts +57 -0
- package/runtime/workflow/lib/errors.ts +95 -0
- package/runtime/workflow/lib/executionId.ts +35 -0
- package/runtime/workflow/lib/executionPlan.ts +550 -0
- package/runtime/workflow/lib/executionProgress.ts +127 -0
- package/runtime/workflow/lib/external.ts +372 -0
- package/runtime/workflow/lib/externalExecutionScanner.ts +892 -0
- package/runtime/workflow/lib/forkSnapshot.ts +231 -0
- package/runtime/workflow/lib/gateTool.ts +261 -0
- package/runtime/workflow/lib/gateToolSync.ts +398 -0
- package/runtime/workflow/lib/lock.ts +388 -0
- package/runtime/workflow/lib/migrationState.ts +1246 -0
- package/runtime/workflow/lib/operatorExecution.ts +174 -0
- package/runtime/workflow/lib/parameters.ts +438 -0
- package/runtime/workflow/lib/pendingDeploymentReuse.ts +428 -0
- package/runtime/workflow/lib/proxy.ts +45 -0
- package/runtime/workflow/lib/registryCommit.ts +455 -0
- package/runtime/workflow/lib/release.ts +329 -0
- package/runtime/workflow/lib/releaseFinalize.ts +711 -0
- package/runtime/workflow/lib/releaseParameters.ts +248 -0
- package/runtime/workflow/lib/reporter.ts +502 -0
- package/runtime/workflow/lib/resultPaths.ts +49 -0
- package/runtime/workflow/lib/runtimeVersion.ts +28 -0
- package/runtime/workflow/lib/signer.ts +124 -0
- package/runtime/workflow/lib/signerIdentity.ts +22 -0
- package/runtime/workflow/lib/simulation.ts +415 -0
- package/runtime/workflow/lib/simulationCallers.ts +335 -0
- package/runtime/workflow/lib/sourceRpcPolicy.ts +41 -0
- package/runtime/workflow/lib/state.ts +25 -0
- package/runtime/workflow/lib/staticValidation.ts +142 -0
- package/runtime/workflow/lib/zksyncForkCapability.ts +80 -0
- package/runtime/workflow/lib/zksyncForkRunner.ts +505 -0
- package/runtime/workflow/reconcileExternal.ts +86 -0
- package/runtime/workflow/reconcileExternalRun.ts +88 -0
- package/runtime/workflow/releaseFinalize.ts +8 -0
- package/runtime/workflow/run.ts +1072 -0
- package/runtime/workflow/staticValidate.ts +23 -0
- package/runtime/workflow/steps/call.ts +10 -0
- package/runtime/workflow/steps/check.ts +10 -0
- package/runtime/workflow/steps/custom.ts +43 -0
- package/runtime/workflow/steps/deploy.ts +772 -0
- package/runtime/workflow/steps/upgrade.ts +599 -0
- package/runtime/workflow/steps/view.ts +10 -0
|
@@ -0,0 +1,1246 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { getAddress, isAddress } from "ethers";
|
|
5
|
+
import {
|
|
6
|
+
type ContractInfo,
|
|
7
|
+
validateContractInfo,
|
|
8
|
+
withContractInfoLock,
|
|
9
|
+
} from "./deployments";
|
|
10
|
+
import {
|
|
11
|
+
type DeploymentInfo,
|
|
12
|
+
validateDeploymentInfo,
|
|
13
|
+
} from "./deploymentInfo";
|
|
14
|
+
|
|
15
|
+
export type MigrationReleaseStage = "development" | "production";
|
|
16
|
+
export type MigrationProxyKind = "none" | "uups" | "transparent";
|
|
17
|
+
|
|
18
|
+
export interface MigrationContractPlan {
|
|
19
|
+
instanceId: string;
|
|
20
|
+
contractInfoKey: string;
|
|
21
|
+
address: string;
|
|
22
|
+
actualContract: string;
|
|
23
|
+
proxyKind: MigrationProxyKind;
|
|
24
|
+
implementationAddress?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface MigrationNetworkPlan {
|
|
28
|
+
sourceNetwork: string;
|
|
29
|
+
releaseStage: MigrationReleaseStage;
|
|
30
|
+
chainId: number;
|
|
31
|
+
manifestDir: string;
|
|
32
|
+
contracts: MigrationContractPlan[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface MigrationStatePlan {
|
|
36
|
+
version: 1;
|
|
37
|
+
migrationId: string;
|
|
38
|
+
sourceProject: string;
|
|
39
|
+
networks: Record<string, MigrationNetworkPlan>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface MigrationInspection {
|
|
43
|
+
chainId: number;
|
|
44
|
+
hasCode: boolean;
|
|
45
|
+
artifactExists: boolean;
|
|
46
|
+
proxyKind: MigrationProxyKind;
|
|
47
|
+
implementationAddress?: string;
|
|
48
|
+
address?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface MigrationImportEvidence {
|
|
52
|
+
version: 1;
|
|
53
|
+
status: "imported";
|
|
54
|
+
integrityHash: string;
|
|
55
|
+
migrationId: string;
|
|
56
|
+
sourceProject: string;
|
|
57
|
+
planHash: string;
|
|
58
|
+
contractInfoHash: string;
|
|
59
|
+
deploymentInfoHash: string;
|
|
60
|
+
networks: Record<string, {
|
|
61
|
+
sourceNetwork: string;
|
|
62
|
+
releaseStage: MigrationReleaseStage;
|
|
63
|
+
chainId: number;
|
|
64
|
+
manifestDir: string;
|
|
65
|
+
contracts: number;
|
|
66
|
+
}>;
|
|
67
|
+
confirmations: string[];
|
|
68
|
+
bytecodeCheck: {
|
|
69
|
+
status: "passed";
|
|
70
|
+
reportPath: string;
|
|
71
|
+
reportHash: string;
|
|
72
|
+
};
|
|
73
|
+
before: {
|
|
74
|
+
contractInfoHash: string;
|
|
75
|
+
deploymentInfoHash: string;
|
|
76
|
+
};
|
|
77
|
+
importedAt: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type MigrationImportEvidencePayload = Omit<MigrationImportEvidence, "integrityHash">;
|
|
81
|
+
|
|
82
|
+
export interface MigrationTargetConfig {
|
|
83
|
+
environment: string;
|
|
84
|
+
openzeppelin?: { manifestDir?: unknown };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface MigrationStateIo {
|
|
88
|
+
writeJson?(
|
|
89
|
+
filePath: string,
|
|
90
|
+
value: unknown,
|
|
91
|
+
options?: { exclusive?: boolean },
|
|
92
|
+
): void;
|
|
93
|
+
rename?(sourcePath: string, targetPath: string): void;
|
|
94
|
+
afterRename?(sourcePath: string, targetPath: string, index: number): void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ImportMigrationStateOptions {
|
|
98
|
+
root: string;
|
|
99
|
+
plan: unknown;
|
|
100
|
+
targets: Record<string, MigrationTargetConfig>;
|
|
101
|
+
confirmations: string[];
|
|
102
|
+
inspect: (
|
|
103
|
+
network: string,
|
|
104
|
+
contract: MigrationContractPlan,
|
|
105
|
+
) => Promise<MigrationInspection> | MigrationInspection;
|
|
106
|
+
verifyCandidate: (
|
|
107
|
+
contractInfo: ContractInfo,
|
|
108
|
+
) => Promise<{ status: "passed" | "failed"; report: unknown }>;
|
|
109
|
+
now?: () => string;
|
|
110
|
+
io?: MigrationStateIo;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,199}$/;
|
|
114
|
+
const SAFE_MANIFEST_DIR = /^\.openzeppelin(?:\.[a-z0-9][a-z0-9-]*)?$/;
|
|
115
|
+
|
|
116
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => (
|
|
117
|
+
Boolean(value) && typeof value === "object" && !Array.isArray(value)
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const fsErrorCode = (error: unknown): string | undefined => (
|
|
121
|
+
error && typeof error === "object" && "code" in error
|
|
122
|
+
? String((error as { code?: unknown }).code)
|
|
123
|
+
: undefined
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const lstatIfPresent = (filePath: string): fs.Stats | undefined => {
|
|
127
|
+
try {
|
|
128
|
+
return fs.lstatSync(filePath);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (fsErrorCode(error) === "ENOENT") return undefined;
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const exactKeys = (
|
|
136
|
+
value: Record<string, unknown>,
|
|
137
|
+
required: string[],
|
|
138
|
+
optional: string[],
|
|
139
|
+
label: string,
|
|
140
|
+
): void => {
|
|
141
|
+
const keys = Object.keys(value);
|
|
142
|
+
if (
|
|
143
|
+
required.some((key) => !keys.includes(key))
|
|
144
|
+
|| keys.some((key) => !required.includes(key) && !optional.includes(key))
|
|
145
|
+
) {
|
|
146
|
+
throw new Error(`${label} is not canonical: unsupported or missing fields`);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const safeId = (value: unknown, label: string): string => {
|
|
151
|
+
if (typeof value !== "string" || !SAFE_ID.test(value)) {
|
|
152
|
+
throw new Error(`${label} must be a safe identifier`);
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const address = (value: unknown, label: string): string => {
|
|
158
|
+
if (typeof value !== "string" || !isAddress(value)) {
|
|
159
|
+
throw new Error(`${label} must be a valid EVM address`);
|
|
160
|
+
}
|
|
161
|
+
return getAddress(value);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const normalizeForHash = (value: unknown): unknown => {
|
|
165
|
+
if (Array.isArray(value)) return value.map(normalizeForHash);
|
|
166
|
+
if (isRecord(value)) {
|
|
167
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [
|
|
168
|
+
key,
|
|
169
|
+
normalizeForHash(value[key]),
|
|
170
|
+
]));
|
|
171
|
+
}
|
|
172
|
+
return value;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const hashValue = (value: unknown): string => crypto
|
|
176
|
+
.createHash("sha256")
|
|
177
|
+
.update(JSON.stringify(normalizeForHash(value)))
|
|
178
|
+
.digest("hex");
|
|
179
|
+
|
|
180
|
+
const hashBuffer = (value: Buffer | undefined): string => crypto
|
|
181
|
+
.createHash("sha256")
|
|
182
|
+
.update(value ?? Buffer.from("<missing>"))
|
|
183
|
+
.digest("hex");
|
|
184
|
+
|
|
185
|
+
const sha256 = (value: unknown, label: string): string => {
|
|
186
|
+
if (typeof value !== "string" || !/^[0-9a-f]{64}$/.test(value)) {
|
|
187
|
+
throw new Error(`${label} must be a lowercase SHA-256 hash`);
|
|
188
|
+
}
|
|
189
|
+
return value;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const nonEmptyString = (value: unknown, label: string): string => {
|
|
193
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
194
|
+
throw new Error(`${label} is required`);
|
|
195
|
+
}
|
|
196
|
+
return value;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const isoTimestamp = (value: unknown, label: string): string => {
|
|
200
|
+
const timestamp = nonEmptyString(value, label);
|
|
201
|
+
if (Number.isNaN(Date.parse(timestamp))) {
|
|
202
|
+
throw new Error(`${label} must be an ISO timestamp`);
|
|
203
|
+
}
|
|
204
|
+
return timestamp;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const validateContractPlan = (
|
|
208
|
+
value: unknown,
|
|
209
|
+
label: string,
|
|
210
|
+
): MigrationContractPlan => {
|
|
211
|
+
if (!isRecord(value)) throw new Error(`${label} must be an object`);
|
|
212
|
+
exactKeys(value, [
|
|
213
|
+
"instanceId",
|
|
214
|
+
"contractInfoKey",
|
|
215
|
+
"address",
|
|
216
|
+
"actualContract",
|
|
217
|
+
"proxyKind",
|
|
218
|
+
], ["implementationAddress"], label);
|
|
219
|
+
const instanceId = safeId(value.instanceId, `${label}.instanceId`);
|
|
220
|
+
const actualContract = safeId(value.actualContract, `${label}.actualContract`);
|
|
221
|
+
if (typeof value.contractInfoKey !== "string" || !value.contractInfoKey.trim()) {
|
|
222
|
+
throw new Error(`${label}.contractInfoKey is required`);
|
|
223
|
+
}
|
|
224
|
+
const keyParts = value.contractInfoKey.split(":");
|
|
225
|
+
if (
|
|
226
|
+
keyParts.length > 2
|
|
227
|
+
|| keyParts[0] !== instanceId
|
|
228
|
+
|| keyParts.at(-1) !== actualContract
|
|
229
|
+
) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
`${label}.contractInfoKey must identify ${instanceId} and actual contract ${actualContract}`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
if (
|
|
235
|
+
value.proxyKind !== "none"
|
|
236
|
+
&& value.proxyKind !== "uups"
|
|
237
|
+
&& value.proxyKind !== "transparent"
|
|
238
|
+
) {
|
|
239
|
+
throw new Error(`${label}.proxyKind is invalid`);
|
|
240
|
+
}
|
|
241
|
+
const implementationAddress = value.implementationAddress === undefined
|
|
242
|
+
? undefined
|
|
243
|
+
: address(value.implementationAddress, `${label}.implementationAddress`);
|
|
244
|
+
if (value.proxyKind === "none" && implementationAddress) {
|
|
245
|
+
throw new Error(`${label} non-proxy contract cannot declare implementationAddress`);
|
|
246
|
+
}
|
|
247
|
+
if (value.proxyKind !== "none" && !implementationAddress) {
|
|
248
|
+
throw new Error(`${label} proxy contract requires implementationAddress`);
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
instanceId,
|
|
252
|
+
contractInfoKey: value.contractInfoKey,
|
|
253
|
+
address: address(value.address, `${label}.address`),
|
|
254
|
+
actualContract,
|
|
255
|
+
proxyKind: value.proxyKind,
|
|
256
|
+
...(implementationAddress ? { implementationAddress } : {}),
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
export const validateMigrationStatePlan = (value: unknown): MigrationStatePlan => {
|
|
261
|
+
if (!isRecord(value)) throw new Error("Migration state plan must be an object");
|
|
262
|
+
exactKeys(value, ["version", "migrationId", "sourceProject", "networks"], [], "Migration state plan");
|
|
263
|
+
if (value.version !== 1) throw new Error("Migration state plan version must be 1");
|
|
264
|
+
const migrationId = safeId(value.migrationId, "Migration state plan migrationId");
|
|
265
|
+
if (typeof value.sourceProject !== "string" || !value.sourceProject.trim()) {
|
|
266
|
+
throw new Error("Migration state plan sourceProject is required");
|
|
267
|
+
}
|
|
268
|
+
if (!isRecord(value.networks) || Object.keys(value.networks).length === 0) {
|
|
269
|
+
throw new Error("Migration state plan must contain at least one network");
|
|
270
|
+
}
|
|
271
|
+
const networks: MigrationStatePlan["networks"] = {};
|
|
272
|
+
for (const [network, rawNetwork] of Object.entries(value.networks)) {
|
|
273
|
+
safeId(network, "Migration target network");
|
|
274
|
+
if (!isRecord(rawNetwork)) throw new Error(`Migration network ${network} must be an object`);
|
|
275
|
+
exactKeys(rawNetwork, [
|
|
276
|
+
"sourceNetwork",
|
|
277
|
+
"releaseStage",
|
|
278
|
+
"chainId",
|
|
279
|
+
"manifestDir",
|
|
280
|
+
"contracts",
|
|
281
|
+
], [], `Migration network ${network}`);
|
|
282
|
+
const sourceNetwork = safeId(
|
|
283
|
+
rawNetwork.sourceNetwork,
|
|
284
|
+
`Migration network ${network}.sourceNetwork`,
|
|
285
|
+
);
|
|
286
|
+
if (
|
|
287
|
+
rawNetwork.releaseStage !== "development"
|
|
288
|
+
&& rawNetwork.releaseStage !== "production"
|
|
289
|
+
) {
|
|
290
|
+
throw new Error(`Migration network ${network}.releaseStage is invalid`);
|
|
291
|
+
}
|
|
292
|
+
if (!Number.isSafeInteger(rawNetwork.chainId) || Number(rawNetwork.chainId) <= 0) {
|
|
293
|
+
throw new Error(`Migration network ${network}.chainId must be a positive integer`);
|
|
294
|
+
}
|
|
295
|
+
if (
|
|
296
|
+
typeof rawNetwork.manifestDir !== "string"
|
|
297
|
+
|| !SAFE_MANIFEST_DIR.test(rawNetwork.manifestDir)
|
|
298
|
+
) {
|
|
299
|
+
throw new Error(`Migration network ${network}.manifestDir is unsafe`);
|
|
300
|
+
}
|
|
301
|
+
if (!Array.isArray(rawNetwork.contracts) || rawNetwork.contracts.length === 0) {
|
|
302
|
+
throw new Error(`Migration network ${network} must contain contracts`);
|
|
303
|
+
}
|
|
304
|
+
const contracts = rawNetwork.contracts.map((contract, index) => (
|
|
305
|
+
validateContractPlan(contract, `Migration network ${network}.contracts[${index}]`)
|
|
306
|
+
));
|
|
307
|
+
if (new Set(contracts.map((contract) => contract.instanceId)).size !== contracts.length) {
|
|
308
|
+
throw new Error(`Migration network ${network} contains duplicate instanceId values`);
|
|
309
|
+
}
|
|
310
|
+
networks[network] = {
|
|
311
|
+
sourceNetwork,
|
|
312
|
+
releaseStage: rawNetwork.releaseStage,
|
|
313
|
+
chainId: Number(rawNetwork.chainId),
|
|
314
|
+
manifestDir: rawNetwork.manifestDir,
|
|
315
|
+
contracts,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
version: 1,
|
|
320
|
+
migrationId,
|
|
321
|
+
sourceProject: value.sourceProject.trim(),
|
|
322
|
+
networks,
|
|
323
|
+
};
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
export const migrationConfirmationToken = (
|
|
327
|
+
inputPlan: unknown,
|
|
328
|
+
network: string,
|
|
329
|
+
): string => {
|
|
330
|
+
const plan = validateMigrationStatePlan(inputPlan);
|
|
331
|
+
const networkPlan = plan.networks[network];
|
|
332
|
+
if (!networkPlan) throw new Error(`Unknown migration network: ${network}`);
|
|
333
|
+
return [
|
|
334
|
+
"migration-v1",
|
|
335
|
+
plan.migrationId,
|
|
336
|
+
network,
|
|
337
|
+
networkPlan.releaseStage,
|
|
338
|
+
networkPlan.manifestDir,
|
|
339
|
+
hashValue({
|
|
340
|
+
migrationId: plan.migrationId,
|
|
341
|
+
sourceProject: plan.sourceProject,
|
|
342
|
+
network,
|
|
343
|
+
networkPlan,
|
|
344
|
+
}),
|
|
345
|
+
].join(":");
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
export const validateMigrationImportEvidence = (
|
|
349
|
+
value: unknown,
|
|
350
|
+
): MigrationImportEvidence => {
|
|
351
|
+
if (!isRecord(value)) throw new Error("Migration import evidence must be an object");
|
|
352
|
+
exactKeys(value, [
|
|
353
|
+
"version",
|
|
354
|
+
"status",
|
|
355
|
+
"integrityHash",
|
|
356
|
+
"migrationId",
|
|
357
|
+
"sourceProject",
|
|
358
|
+
"planHash",
|
|
359
|
+
"contractInfoHash",
|
|
360
|
+
"deploymentInfoHash",
|
|
361
|
+
"networks",
|
|
362
|
+
"confirmations",
|
|
363
|
+
"bytecodeCheck",
|
|
364
|
+
"before",
|
|
365
|
+
"importedAt",
|
|
366
|
+
], [], "Migration import evidence");
|
|
367
|
+
if (value.version !== 1 || value.status !== "imported") {
|
|
368
|
+
throw new Error("Migration import evidence version or status is invalid");
|
|
369
|
+
}
|
|
370
|
+
const migrationId = safeId(value.migrationId, "Migration import evidence migrationId");
|
|
371
|
+
const sourceProject = nonEmptyString(
|
|
372
|
+
value.sourceProject,
|
|
373
|
+
"Migration import evidence sourceProject",
|
|
374
|
+
);
|
|
375
|
+
if (!isRecord(value.networks) || Object.keys(value.networks).length === 0) {
|
|
376
|
+
throw new Error("Migration import evidence networks are required");
|
|
377
|
+
}
|
|
378
|
+
const networks: MigrationImportEvidence["networks"] = {};
|
|
379
|
+
for (const [network, rawNetwork] of Object.entries(value.networks)) {
|
|
380
|
+
safeId(network, "Migration import evidence network");
|
|
381
|
+
if (!isRecord(rawNetwork)) {
|
|
382
|
+
throw new Error(`Migration import evidence network ${network} must be an object`);
|
|
383
|
+
}
|
|
384
|
+
exactKeys(rawNetwork, [
|
|
385
|
+
"sourceNetwork",
|
|
386
|
+
"releaseStage",
|
|
387
|
+
"chainId",
|
|
388
|
+
"manifestDir",
|
|
389
|
+
"contracts",
|
|
390
|
+
], [], `Migration import evidence network ${network}`);
|
|
391
|
+
if (
|
|
392
|
+
rawNetwork.releaseStage !== "development"
|
|
393
|
+
&& rawNetwork.releaseStage !== "production"
|
|
394
|
+
) {
|
|
395
|
+
throw new Error(`Migration import evidence network ${network} releaseStage is invalid`);
|
|
396
|
+
}
|
|
397
|
+
if (!Number.isSafeInteger(rawNetwork.chainId) || Number(rawNetwork.chainId) <= 0) {
|
|
398
|
+
throw new Error(`Migration import evidence network ${network} chainId is invalid`);
|
|
399
|
+
}
|
|
400
|
+
if (
|
|
401
|
+
typeof rawNetwork.manifestDir !== "string"
|
|
402
|
+
|| !SAFE_MANIFEST_DIR.test(rawNetwork.manifestDir)
|
|
403
|
+
) {
|
|
404
|
+
throw new Error(`Migration import evidence network ${network} manifestDir is unsafe`);
|
|
405
|
+
}
|
|
406
|
+
if (!Number.isSafeInteger(rawNetwork.contracts) || Number(rawNetwork.contracts) <= 0) {
|
|
407
|
+
throw new Error(`Migration import evidence network ${network} contracts is invalid`);
|
|
408
|
+
}
|
|
409
|
+
networks[network] = {
|
|
410
|
+
sourceNetwork: safeId(
|
|
411
|
+
rawNetwork.sourceNetwork,
|
|
412
|
+
`Migration import evidence network ${network} sourceNetwork`,
|
|
413
|
+
),
|
|
414
|
+
releaseStage: rawNetwork.releaseStage,
|
|
415
|
+
chainId: Number(rawNetwork.chainId),
|
|
416
|
+
manifestDir: rawNetwork.manifestDir,
|
|
417
|
+
contracts: Number(rawNetwork.contracts),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
if (
|
|
421
|
+
!Array.isArray(value.confirmations)
|
|
422
|
+
|| value.confirmations.length !== Object.keys(networks).length
|
|
423
|
+
|| value.confirmations.some((token) => typeof token !== "string" || !token)
|
|
424
|
+
|| new Set(value.confirmations).size !== value.confirmations.length
|
|
425
|
+
) {
|
|
426
|
+
throw new Error("Migration import evidence confirmations are invalid");
|
|
427
|
+
}
|
|
428
|
+
if (!isRecord(value.bytecodeCheck)) {
|
|
429
|
+
throw new Error("Migration import evidence bytecodeCheck must be an object");
|
|
430
|
+
}
|
|
431
|
+
exactKeys(
|
|
432
|
+
value.bytecodeCheck,
|
|
433
|
+
["status", "reportPath", "reportHash"],
|
|
434
|
+
[],
|
|
435
|
+
"Migration import evidence bytecodeCheck",
|
|
436
|
+
);
|
|
437
|
+
const expectedReportPath = `scripts/migrations/${migrationId}/bytecode-check.json`;
|
|
438
|
+
if (
|
|
439
|
+
value.bytecodeCheck.status !== "passed"
|
|
440
|
+
|| value.bytecodeCheck.reportPath !== expectedReportPath
|
|
441
|
+
) {
|
|
442
|
+
throw new Error("Migration import evidence bytecode report path or status is invalid");
|
|
443
|
+
}
|
|
444
|
+
if (!isRecord(value.before)) {
|
|
445
|
+
throw new Error("Migration import evidence before must be an object");
|
|
446
|
+
}
|
|
447
|
+
exactKeys(
|
|
448
|
+
value.before,
|
|
449
|
+
["contractInfoHash", "deploymentInfoHash"],
|
|
450
|
+
[],
|
|
451
|
+
"Migration import evidence before",
|
|
452
|
+
);
|
|
453
|
+
const canonical: MigrationImportEvidencePayload = {
|
|
454
|
+
version: 1,
|
|
455
|
+
status: "imported",
|
|
456
|
+
migrationId,
|
|
457
|
+
sourceProject,
|
|
458
|
+
planHash: sha256(value.planHash, "Migration import evidence planHash"),
|
|
459
|
+
contractInfoHash: sha256(
|
|
460
|
+
value.contractInfoHash,
|
|
461
|
+
"Migration import evidence contractInfoHash",
|
|
462
|
+
),
|
|
463
|
+
deploymentInfoHash: sha256(
|
|
464
|
+
value.deploymentInfoHash,
|
|
465
|
+
"Migration import evidence deploymentInfoHash",
|
|
466
|
+
),
|
|
467
|
+
networks,
|
|
468
|
+
confirmations: [...value.confirmations] as string[],
|
|
469
|
+
bytecodeCheck: {
|
|
470
|
+
status: "passed",
|
|
471
|
+
reportPath: expectedReportPath,
|
|
472
|
+
reportHash: sha256(
|
|
473
|
+
value.bytecodeCheck.reportHash,
|
|
474
|
+
"Migration import evidence bytecode reportHash",
|
|
475
|
+
),
|
|
476
|
+
},
|
|
477
|
+
before: {
|
|
478
|
+
contractInfoHash: sha256(
|
|
479
|
+
value.before.contractInfoHash,
|
|
480
|
+
"Migration import evidence before.contractInfoHash",
|
|
481
|
+
),
|
|
482
|
+
deploymentInfoHash: sha256(
|
|
483
|
+
value.before.deploymentInfoHash,
|
|
484
|
+
"Migration import evidence before.deploymentInfoHash",
|
|
485
|
+
),
|
|
486
|
+
},
|
|
487
|
+
importedAt: isoTimestamp(value.importedAt, "Migration import evidence importedAt"),
|
|
488
|
+
};
|
|
489
|
+
const integrityHash = sha256(
|
|
490
|
+
value.integrityHash,
|
|
491
|
+
"Migration import evidence integrityHash",
|
|
492
|
+
);
|
|
493
|
+
if (hashValue(canonical) !== integrityHash) {
|
|
494
|
+
throw new Error("Migration import evidence integrity hash mismatch");
|
|
495
|
+
}
|
|
496
|
+
return { ...canonical, integrityHash };
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
export const resolveConfinedMigrationPath = (
|
|
500
|
+
root: string,
|
|
501
|
+
requested: string,
|
|
502
|
+
options: { label?: string; requireFile?: boolean } = {},
|
|
503
|
+
): string => {
|
|
504
|
+
const label = options.label ?? "Migration path";
|
|
505
|
+
const projectRoot = path.resolve(root);
|
|
506
|
+
const rootStat = lstatIfPresent(projectRoot);
|
|
507
|
+
if (!rootStat) {
|
|
508
|
+
throw new Error(`${label} project root does not exist: ${projectRoot}`);
|
|
509
|
+
}
|
|
510
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
511
|
+
throw new Error(`${label} project root must be a real directory: ${projectRoot}`);
|
|
512
|
+
}
|
|
513
|
+
const realRoot = fs.realpathSync(projectRoot);
|
|
514
|
+
const candidate = path.isAbsolute(requested)
|
|
515
|
+
? path.resolve(requested)
|
|
516
|
+
: path.resolve(projectRoot, requested);
|
|
517
|
+
const relative = path.relative(projectRoot, candidate);
|
|
518
|
+
if (
|
|
519
|
+
!relative
|
|
520
|
+
|| relative === ".."
|
|
521
|
+
|| relative.startsWith(`..${path.sep}`)
|
|
522
|
+
|| path.isAbsolute(relative)
|
|
523
|
+
) {
|
|
524
|
+
throw new Error(`${label} escapes the project root: ${requested}`);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const segments = relative.split(path.sep);
|
|
528
|
+
let current = projectRoot;
|
|
529
|
+
let missing = false;
|
|
530
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
531
|
+
current = path.join(current, segments[index]);
|
|
532
|
+
const stat = lstatIfPresent(current);
|
|
533
|
+
if (!stat) {
|
|
534
|
+
missing = true;
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
if (stat.isSymbolicLink()) {
|
|
538
|
+
throw new Error(`${label} cannot traverse a symlink: ${current}`);
|
|
539
|
+
}
|
|
540
|
+
if (index < segments.length - 1 && !stat.isDirectory()) {
|
|
541
|
+
throw new Error(`${label} ancestor is not a directory: ${current}`);
|
|
542
|
+
}
|
|
543
|
+
const realCurrent = fs.realpathSync(current);
|
|
544
|
+
const realRelative = path.relative(realRoot, realCurrent);
|
|
545
|
+
if (
|
|
546
|
+
realRelative === ".."
|
|
547
|
+
|| realRelative.startsWith(`..${path.sep}`)
|
|
548
|
+
|| path.isAbsolute(realRelative)
|
|
549
|
+
) {
|
|
550
|
+
throw new Error(`${label} resolves outside the project root: ${requested}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (options.requireFile) {
|
|
555
|
+
const stat = missing ? undefined : lstatIfPresent(candidate);
|
|
556
|
+
if (!stat) {
|
|
557
|
+
throw new Error(`${label} does not exist: ${requested}`);
|
|
558
|
+
}
|
|
559
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
560
|
+
throw new Error(`${label} must be a regular file: ${requested}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return candidate;
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const snapshot = (filePath: string): Buffer | undefined => {
|
|
567
|
+
const stat = lstatIfPresent(filePath);
|
|
568
|
+
if (!stat) return undefined;
|
|
569
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
570
|
+
throw new Error(`Migration state path must be a regular file: ${filePath}`);
|
|
571
|
+
}
|
|
572
|
+
return fs.readFileSync(filePath);
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
const readRegularJson = (filePath: string, label: string): unknown => {
|
|
576
|
+
const contents = snapshot(filePath);
|
|
577
|
+
if (contents === undefined) throw new Error(`${label} is missing: ${filePath}`);
|
|
578
|
+
try {
|
|
579
|
+
return JSON.parse(contents.toString("utf8"));
|
|
580
|
+
} catch (error) {
|
|
581
|
+
throw new Error(
|
|
582
|
+
`${label} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
export const resolveGateToolCandidateCheck = (
|
|
588
|
+
result: { status: number | null; error?: { message?: unknown } },
|
|
589
|
+
reportPath: string,
|
|
590
|
+
): { status: "passed" | "failed"; report: unknown } => {
|
|
591
|
+
const reportStat = lstatIfPresent(reportPath);
|
|
592
|
+
if (reportStat && (reportStat.isSymbolicLink() || !reportStat.isFile())) {
|
|
593
|
+
throw new Error(`gate-tool bytecode report must be a regular file: ${reportPath}`);
|
|
594
|
+
}
|
|
595
|
+
if (result.status === 0) {
|
|
596
|
+
if (!reportStat) {
|
|
597
|
+
throw new Error(`gate-tool exited zero but did not create a report: ${reportPath}`);
|
|
598
|
+
}
|
|
599
|
+
return {
|
|
600
|
+
status: "passed",
|
|
601
|
+
report: readRegularJson(reportPath, "gate-tool bytecode report"),
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
const report = !reportStat
|
|
605
|
+
? { error: String(result.error?.message ?? `gate-tool exited with ${result.status ?? 1}`) }
|
|
606
|
+
: readRegularJson(reportPath, "gate-tool bytecode report");
|
|
607
|
+
return { status: "failed", report };
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
const serializeJson = (value: unknown): Buffer => (
|
|
611
|
+
Buffer.from(`${JSON.stringify(value, null, 2)}\n`)
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
const fsyncDirectory = (directory: string): void => {
|
|
615
|
+
let descriptor: number | undefined;
|
|
616
|
+
try {
|
|
617
|
+
descriptor = fs.openSync(directory, "r");
|
|
618
|
+
fs.fsyncSync(descriptor);
|
|
619
|
+
} catch (error) {
|
|
620
|
+
const code = error && typeof error === "object" && "code" in error
|
|
621
|
+
? String((error as { code?: unknown }).code)
|
|
622
|
+
: undefined;
|
|
623
|
+
if (code !== "EINVAL" && code !== "ENOTSUP" && code !== "EPERM") throw error;
|
|
624
|
+
} finally {
|
|
625
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
const writeBufferNew = (filePath: string, contents: Buffer): void => {
|
|
630
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
631
|
+
let descriptor: number | undefined;
|
|
632
|
+
let created = false;
|
|
633
|
+
try {
|
|
634
|
+
descriptor = fs.openSync(filePath, "wx", 0o600);
|
|
635
|
+
created = true;
|
|
636
|
+
fs.writeFileSync(descriptor, contents);
|
|
637
|
+
fs.fsyncSync(descriptor);
|
|
638
|
+
fs.closeSync(descriptor);
|
|
639
|
+
descriptor = undefined;
|
|
640
|
+
fsyncDirectory(path.dirname(filePath));
|
|
641
|
+
} catch (error) {
|
|
642
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
643
|
+
if (created && lstatIfPresent(filePath)) fs.unlinkSync(filePath);
|
|
644
|
+
throw error;
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
const renameDurable = (sourcePath: string, targetPath: string): void => {
|
|
649
|
+
fs.renameSync(sourcePath, targetPath);
|
|
650
|
+
fsyncDirectory(path.dirname(targetPath));
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
const unlinkDurable = (filePath: string): void => {
|
|
654
|
+
const stat = lstatIfPresent(filePath);
|
|
655
|
+
if (!stat) return;
|
|
656
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
657
|
+
throw new Error(`Migration transaction path must be a regular file: ${filePath}`);
|
|
658
|
+
}
|
|
659
|
+
fs.unlinkSync(filePath);
|
|
660
|
+
fsyncDirectory(path.dirname(filePath));
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
const writeBufferAtomic = (filePath: string, contents: Buffer): void => {
|
|
664
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
665
|
+
const temporary = `${filePath}.${process.pid}.${Date.now()}.${crypto.randomBytes(6).toString("hex")}.tmp`;
|
|
666
|
+
let descriptor: number | undefined;
|
|
667
|
+
try {
|
|
668
|
+
descriptor = fs.openSync(temporary, "wx", 0o600);
|
|
669
|
+
fs.writeFileSync(descriptor, contents);
|
|
670
|
+
fs.fsyncSync(descriptor);
|
|
671
|
+
fs.closeSync(descriptor);
|
|
672
|
+
descriptor = undefined;
|
|
673
|
+
renameDurable(temporary, filePath);
|
|
674
|
+
} catch (error) {
|
|
675
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
676
|
+
if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
|
|
677
|
+
throw error;
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
const defaultIo: Required<Pick<MigrationStateIo, "writeJson" | "rename">> = {
|
|
682
|
+
writeJson: (filePath, value, options = {}) => {
|
|
683
|
+
if (options.exclusive) {
|
|
684
|
+
writeBufferNew(filePath, serializeJson(value));
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
writeBufferAtomic(filePath, serializeJson(value));
|
|
688
|
+
},
|
|
689
|
+
rename: renameDurable,
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
const restoreSnapshots = (
|
|
693
|
+
snapshots: Array<{ filePath: string; contents: Buffer | undefined }>,
|
|
694
|
+
): void => {
|
|
695
|
+
for (const { filePath, contents } of [...snapshots].reverse()) {
|
|
696
|
+
if (contents === undefined) {
|
|
697
|
+
unlinkDurable(filePath);
|
|
698
|
+
} else {
|
|
699
|
+
writeBufferAtomic(filePath, contents);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
interface MigrationCommitTarget {
|
|
705
|
+
filePath: string;
|
|
706
|
+
relativePath: string;
|
|
707
|
+
value: unknown;
|
|
708
|
+
contents?: Buffer;
|
|
709
|
+
stagedPath?: string;
|
|
710
|
+
stagedRelativePath?: string;
|
|
711
|
+
before: Buffer | undefined;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
interface MigrationJournalTarget {
|
|
715
|
+
relativePath: string;
|
|
716
|
+
stagedRelativePath: string;
|
|
717
|
+
beforeBase64: string | null;
|
|
718
|
+
beforeHash: string;
|
|
719
|
+
afterHash: string;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
interface MigrationTransactionJournal {
|
|
723
|
+
version: 1;
|
|
724
|
+
status: "prepared";
|
|
725
|
+
migrationId: string;
|
|
726
|
+
planHash: string;
|
|
727
|
+
evidenceHash: string;
|
|
728
|
+
targets: MigrationJournalTarget[];
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const projectRelative = (root: string, filePath: string): string => (
|
|
732
|
+
path.relative(root, filePath).split(path.sep).join("/")
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
const decodeJournalSnapshot = (
|
|
736
|
+
encoded: unknown,
|
|
737
|
+
expectedHash: string,
|
|
738
|
+
label: string,
|
|
739
|
+
): Buffer | undefined => {
|
|
740
|
+
if (encoded === null) {
|
|
741
|
+
if (hashBuffer(undefined) !== expectedHash) {
|
|
742
|
+
throw new Error(`${label} missing snapshot hash is invalid`);
|
|
743
|
+
}
|
|
744
|
+
return undefined;
|
|
745
|
+
}
|
|
746
|
+
if (typeof encoded !== "string") {
|
|
747
|
+
throw new Error(`${label} beforeBase64 must be a string or null`);
|
|
748
|
+
}
|
|
749
|
+
const contents = Buffer.from(encoded, "base64");
|
|
750
|
+
if (contents.toString("base64") !== encoded || hashBuffer(contents) !== expectedHash) {
|
|
751
|
+
throw new Error(`${label} before snapshot is invalid`);
|
|
752
|
+
}
|
|
753
|
+
return contents;
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
const validateMigrationTransactionJournal = (
|
|
757
|
+
value: unknown,
|
|
758
|
+
options: {
|
|
759
|
+
root: string;
|
|
760
|
+
plan: MigrationStatePlan;
|
|
761
|
+
planHash: string;
|
|
762
|
+
expectedTargets: Array<{ filePath: string; relativePath: string }>;
|
|
763
|
+
},
|
|
764
|
+
): Array<{
|
|
765
|
+
filePath: string;
|
|
766
|
+
stagedPath: string;
|
|
767
|
+
before: Buffer | undefined;
|
|
768
|
+
afterHash: string;
|
|
769
|
+
}> => {
|
|
770
|
+
if (!isRecord(value)) throw new Error("Migration transaction journal must be an object");
|
|
771
|
+
exactKeys(value, [
|
|
772
|
+
"version",
|
|
773
|
+
"status",
|
|
774
|
+
"migrationId",
|
|
775
|
+
"planHash",
|
|
776
|
+
"evidenceHash",
|
|
777
|
+
"targets",
|
|
778
|
+
], [], "Migration transaction journal");
|
|
779
|
+
if (
|
|
780
|
+
value.version !== 1
|
|
781
|
+
|| value.status !== "prepared"
|
|
782
|
+
|| value.migrationId !== options.plan.migrationId
|
|
783
|
+
|| value.planHash !== options.planHash
|
|
784
|
+
) {
|
|
785
|
+
throw new Error("Migration transaction journal does not match the active plan");
|
|
786
|
+
}
|
|
787
|
+
const evidenceHash = sha256(
|
|
788
|
+
value.evidenceHash,
|
|
789
|
+
"Migration transaction journal evidenceHash",
|
|
790
|
+
);
|
|
791
|
+
if (
|
|
792
|
+
!Array.isArray(value.targets)
|
|
793
|
+
|| value.targets.length !== options.expectedTargets.length
|
|
794
|
+
) {
|
|
795
|
+
throw new Error("Migration transaction journal target set is invalid");
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const stagedPaths = new Set<string>();
|
|
799
|
+
const targets = value.targets.map((rawTarget, index) => {
|
|
800
|
+
const expected = options.expectedTargets[index];
|
|
801
|
+
if (!isRecord(rawTarget)) {
|
|
802
|
+
throw new Error(`Migration transaction journal target ${index} must be an object`);
|
|
803
|
+
}
|
|
804
|
+
exactKeys(rawTarget, [
|
|
805
|
+
"relativePath",
|
|
806
|
+
"stagedRelativePath",
|
|
807
|
+
"beforeBase64",
|
|
808
|
+
"beforeHash",
|
|
809
|
+
"afterHash",
|
|
810
|
+
], [], `Migration transaction journal target ${index}`);
|
|
811
|
+
if (rawTarget.relativePath !== expected.relativePath) {
|
|
812
|
+
throw new Error(`Migration transaction journal target ${index} path is invalid`);
|
|
813
|
+
}
|
|
814
|
+
if (typeof rawTarget.stagedRelativePath !== "string") {
|
|
815
|
+
throw new Error(`Migration transaction journal target ${index} staged path is invalid`);
|
|
816
|
+
}
|
|
817
|
+
const stagedRelativePath = rawTarget.stagedRelativePath;
|
|
818
|
+
const expectedDirectory = path.posix.dirname(expected.relativePath);
|
|
819
|
+
const stagedName = path.posix.basename(stagedRelativePath);
|
|
820
|
+
if (
|
|
821
|
+
path.posix.dirname(stagedRelativePath) !== expectedDirectory
|
|
822
|
+
|| !stagedName.startsWith(`.${path.posix.basename(expected.relativePath)}.`)
|
|
823
|
+
|| !stagedName.endsWith(".stage")
|
|
824
|
+
|| stagedPaths.has(stagedRelativePath)
|
|
825
|
+
) {
|
|
826
|
+
throw new Error(`Migration transaction journal target ${index} staged path is unsafe`);
|
|
827
|
+
}
|
|
828
|
+
stagedPaths.add(stagedRelativePath);
|
|
829
|
+
const beforeHash = sha256(
|
|
830
|
+
rawTarget.beforeHash,
|
|
831
|
+
`Migration transaction journal target ${index} beforeHash`,
|
|
832
|
+
);
|
|
833
|
+
const afterHash = sha256(
|
|
834
|
+
rawTarget.afterHash,
|
|
835
|
+
`Migration transaction journal target ${index} afterHash`,
|
|
836
|
+
);
|
|
837
|
+
const stagedPath = resolveConfinedMigrationPath(options.root, stagedRelativePath, {
|
|
838
|
+
label: `Migration transaction staged target ${index}`,
|
|
839
|
+
});
|
|
840
|
+
return {
|
|
841
|
+
filePath: expected.filePath,
|
|
842
|
+
stagedPath,
|
|
843
|
+
before: decodeJournalSnapshot(
|
|
844
|
+
rawTarget.beforeBase64,
|
|
845
|
+
beforeHash,
|
|
846
|
+
`Migration transaction journal target ${index}`,
|
|
847
|
+
),
|
|
848
|
+
afterHash,
|
|
849
|
+
};
|
|
850
|
+
});
|
|
851
|
+
if (targets.at(-1)?.afterHash !== evidenceHash) {
|
|
852
|
+
throw new Error("Migration transaction journal evidence hash is inconsistent");
|
|
853
|
+
}
|
|
854
|
+
return targets;
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
const recoverMigrationTransaction = (options: {
|
|
858
|
+
root: string;
|
|
859
|
+
journalFile: string;
|
|
860
|
+
plan: MigrationStatePlan;
|
|
861
|
+
planHash: string;
|
|
862
|
+
expectedTargets: Array<{ filePath: string; relativePath: string }>;
|
|
863
|
+
}): "none" | "rolled_back" | "committed" => {
|
|
864
|
+
if (!fs.existsSync(options.journalFile)) return "none";
|
|
865
|
+
const targets = validateMigrationTransactionJournal(
|
|
866
|
+
readRegularJson(options.journalFile, "Migration transaction journal"),
|
|
867
|
+
options,
|
|
868
|
+
);
|
|
869
|
+
const committed = targets.every((target) => {
|
|
870
|
+
const contents = snapshot(target.filePath);
|
|
871
|
+
return contents !== undefined && hashBuffer(contents) === target.afterHash;
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
if (!committed) {
|
|
875
|
+
restoreSnapshots(targets.map((target) => ({
|
|
876
|
+
filePath: target.filePath,
|
|
877
|
+
contents: target.before,
|
|
878
|
+
})));
|
|
879
|
+
}
|
|
880
|
+
for (const target of targets) unlinkDurable(target.stagedPath);
|
|
881
|
+
unlinkDurable(options.journalFile);
|
|
882
|
+
return committed ? "committed" : "rolled_back";
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
const stageMigrationTargets = (
|
|
886
|
+
root: string,
|
|
887
|
+
targets: MigrationCommitTarget[],
|
|
888
|
+
io: Required<Pick<MigrationStateIo, "writeJson" | "rename">> & MigrationStateIo,
|
|
889
|
+
): void => {
|
|
890
|
+
const nonce = `${process.pid}-${Date.now()}-${crypto.randomBytes(6).toString("hex")}`;
|
|
891
|
+
for (const target of targets) {
|
|
892
|
+
target.stagedRelativePath = path.posix.join(
|
|
893
|
+
path.posix.dirname(target.relativePath),
|
|
894
|
+
`.${path.posix.basename(target.relativePath)}.${nonce}.stage`,
|
|
895
|
+
);
|
|
896
|
+
target.stagedPath = resolveConfinedMigrationPath(root, target.stagedRelativePath, {
|
|
897
|
+
label: "Migration staged output",
|
|
898
|
+
});
|
|
899
|
+
io.writeJson(target.stagedPath, target.value, { exclusive: true });
|
|
900
|
+
target.contents = snapshot(target.stagedPath);
|
|
901
|
+
if (target.contents === undefined) {
|
|
902
|
+
throw new Error(`Migration staged output is missing: ${target.stagedRelativePath}`);
|
|
903
|
+
}
|
|
904
|
+
let stagedValue: unknown;
|
|
905
|
+
try {
|
|
906
|
+
stagedValue = JSON.parse(target.contents.toString("utf8"));
|
|
907
|
+
} catch (error) {
|
|
908
|
+
throw new Error(
|
|
909
|
+
`Migration staged output is invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
if (hashValue(stagedValue) !== hashValue(target.value)) {
|
|
913
|
+
throw new Error(`Migration staged output changed value: ${target.stagedRelativePath}`);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
const cleanupUncommittedStages = (targets: MigrationCommitTarget[]): void => {
|
|
919
|
+
for (const target of targets) {
|
|
920
|
+
if (target.stagedPath) unlinkDurable(target.stagedPath);
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
export const importMigrationState = async (
|
|
925
|
+
options: ImportMigrationStateOptions,
|
|
926
|
+
): Promise<MigrationImportEvidence> => {
|
|
927
|
+
const root = path.resolve(options.root);
|
|
928
|
+
fs.mkdirSync(root, { recursive: true });
|
|
929
|
+
const plan = validateMigrationStatePlan(options.plan);
|
|
930
|
+
const expectedConfirmations = Object.keys(plan.networks).sort().map((network) => (
|
|
931
|
+
migrationConfirmationToken(plan, network)
|
|
932
|
+
));
|
|
933
|
+
const providedConfirmations = new Set(options.confirmations);
|
|
934
|
+
for (const [index, token] of expectedConfirmations.entries()) {
|
|
935
|
+
if (!providedConfirmations.has(token)) {
|
|
936
|
+
const network = Object.keys(plan.networks).sort()[index];
|
|
937
|
+
throw new Error(`Missing migration confirmation for ${network}: ${token}`);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const evidenceRelative = `scripts/migrations/${plan.migrationId}/state-import.json`;
|
|
942
|
+
const bytecodeReportRelative = `scripts/migrations/${plan.migrationId}/bytecode-check.json`;
|
|
943
|
+
const journalRelative = `scripts/migrations/${plan.migrationId}/.state-import.transaction.json`;
|
|
944
|
+
const evidenceFile = resolveConfinedMigrationPath(root, evidenceRelative, {
|
|
945
|
+
label: "Migration evidence path",
|
|
946
|
+
});
|
|
947
|
+
const bytecodeReportFile = resolveConfinedMigrationPath(root, bytecodeReportRelative, {
|
|
948
|
+
label: "Migration bytecode report path",
|
|
949
|
+
});
|
|
950
|
+
const contractInfoFile = resolveConfinedMigrationPath(root, "contractInfo.json", {
|
|
951
|
+
label: "Migration contractInfo path",
|
|
952
|
+
});
|
|
953
|
+
const deploymentInfoFile = resolveConfinedMigrationPath(root, "deploymentInfo.json", {
|
|
954
|
+
label: "Migration deploymentInfo path",
|
|
955
|
+
});
|
|
956
|
+
const journalFile = resolveConfinedMigrationPath(root, journalRelative, {
|
|
957
|
+
label: "Migration transaction journal path",
|
|
958
|
+
});
|
|
959
|
+
const planHash = hashValue(plan);
|
|
960
|
+
let previousEvidence: MigrationImportEvidence | undefined;
|
|
961
|
+
if (fs.existsSync(evidenceFile)) {
|
|
962
|
+
previousEvidence = validateMigrationImportEvidence(
|
|
963
|
+
readRegularJson(evidenceFile, "Migration import evidence"),
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
const contractInfo: ContractInfo = {};
|
|
967
|
+
const deploymentInfo: DeploymentInfo = { version: 1, networks: {} };
|
|
968
|
+
const importedAt = previousEvidence?.version === 1
|
|
969
|
+
&& previousEvidence.status === "imported"
|
|
970
|
+
&& previousEvidence.migrationId === plan.migrationId
|
|
971
|
+
&& previousEvidence.planHash === planHash
|
|
972
|
+
? previousEvidence.importedAt
|
|
973
|
+
: (options.now ?? (() => new Date().toISOString()))();
|
|
974
|
+
|
|
975
|
+
for (const [network, networkPlan] of Object.entries(plan.networks)) {
|
|
976
|
+
const target = options.targets[network];
|
|
977
|
+
if (!target) throw new Error(`Missing target config for migration network ${network}`);
|
|
978
|
+
if (target.environment !== networkPlan.releaseStage) {
|
|
979
|
+
throw new Error(
|
|
980
|
+
`Migration release stage mismatch for ${network}: expected ${networkPlan.releaseStage}, received ${target.environment}`,
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
if (target.openzeppelin?.manifestDir !== networkPlan.manifestDir) {
|
|
984
|
+
throw new Error(
|
|
985
|
+
`Migration manifest directory mismatch for ${network}: expected ${networkPlan.manifestDir}, received ${String(target.openzeppelin?.manifestDir)}`,
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
contractInfo[network] = {};
|
|
990
|
+
deploymentInfo.networks[network] = {};
|
|
991
|
+
for (const contract of networkPlan.contracts) {
|
|
992
|
+
const inspection = await options.inspect(network, contract);
|
|
993
|
+
if (inspection.chainId !== networkPlan.chainId) {
|
|
994
|
+
throw new Error(
|
|
995
|
+
`ChainId mismatch for ${network}.${contract.instanceId}: expected ${networkPlan.chainId}, received ${inspection.chainId}`,
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
if (!inspection.hasCode) {
|
|
999
|
+
throw new Error(`No chain code for ${network}.${contract.instanceId}`);
|
|
1000
|
+
}
|
|
1001
|
+
if (!inspection.artifactExists) {
|
|
1002
|
+
throw new Error(`Artifact does not exist for ${network}.${contract.actualContract}`);
|
|
1003
|
+
}
|
|
1004
|
+
if (
|
|
1005
|
+
inspection.address
|
|
1006
|
+
&& address(inspection.address, `${network}.${contract.instanceId} inspected address`).toLowerCase()
|
|
1007
|
+
!== contract.address.toLowerCase()
|
|
1008
|
+
) {
|
|
1009
|
+
throw new Error(`Address mismatch for ${network}.${contract.instanceId}`);
|
|
1010
|
+
}
|
|
1011
|
+
if (inspection.proxyKind !== contract.proxyKind) {
|
|
1012
|
+
throw new Error(
|
|
1013
|
+
`Proxy kind mismatch for ${network}.${contract.instanceId}: expected ${contract.proxyKind}, received ${inspection.proxyKind}`,
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
const inspectedImplementation = inspection.implementationAddress
|
|
1017
|
+
? address(
|
|
1018
|
+
inspection.implementationAddress,
|
|
1019
|
+
`${network}.${contract.instanceId} inspected implementation`,
|
|
1020
|
+
)
|
|
1021
|
+
: undefined;
|
|
1022
|
+
if (
|
|
1023
|
+
(contract.implementationAddress ?? "").toLowerCase()
|
|
1024
|
+
!== (inspectedImplementation ?? "").toLowerCase()
|
|
1025
|
+
) {
|
|
1026
|
+
throw new Error(`Implementation address mismatch for ${network}.${contract.instanceId}`);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
contractInfo[network][contract.contractInfoKey] = contract.address;
|
|
1030
|
+
deploymentInfo.networks[network][contract.instanceId] = {
|
|
1031
|
+
contractInfoKey: contract.contractInfoKey,
|
|
1032
|
+
address: contract.address,
|
|
1033
|
+
actualContract: contract.actualContract,
|
|
1034
|
+
...(contract.proxyKind === "none" ? {} : { proxyKind: contract.proxyKind }),
|
|
1035
|
+
...(contract.implementationAddress
|
|
1036
|
+
? { implementationAddress: contract.implementationAddress }
|
|
1037
|
+
: {}),
|
|
1038
|
+
source: {
|
|
1039
|
+
kind: "migration",
|
|
1040
|
+
migrationId: plan.migrationId,
|
|
1041
|
+
evidencePath: evidenceRelative,
|
|
1042
|
+
},
|
|
1043
|
+
updatedAt: importedAt,
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const canonicalContractInfo = validateContractInfo(contractInfo);
|
|
1049
|
+
const canonicalDeploymentInfo = validateDeploymentInfo(deploymentInfo);
|
|
1050
|
+
const bytecodeCheck = await options.verifyCandidate(canonicalContractInfo);
|
|
1051
|
+
if (bytecodeCheck.status !== "passed") {
|
|
1052
|
+
throw new Error("Migration candidate bytecode verification failed");
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const contractInfoHash = hashValue(canonicalContractInfo);
|
|
1056
|
+
const deploymentInfoHash = hashValue(canonicalDeploymentInfo);
|
|
1057
|
+
const expectedCommitTargets = [
|
|
1058
|
+
{ filePath: contractInfoFile, relativePath: "contractInfo.json" },
|
|
1059
|
+
{ filePath: deploymentInfoFile, relativePath: "deploymentInfo.json" },
|
|
1060
|
+
{ filePath: bytecodeReportFile, relativePath: bytecodeReportRelative },
|
|
1061
|
+
{ filePath: evidenceFile, relativePath: evidenceRelative },
|
|
1062
|
+
];
|
|
1063
|
+
|
|
1064
|
+
return withContractInfoLock(root, () => {
|
|
1065
|
+
recoverMigrationTransaction({
|
|
1066
|
+
root,
|
|
1067
|
+
journalFile,
|
|
1068
|
+
plan,
|
|
1069
|
+
planHash,
|
|
1070
|
+
expectedTargets: expectedCommitTargets,
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
if (fs.existsSync(evidenceFile)) {
|
|
1074
|
+
const existing = validateMigrationImportEvidence(
|
|
1075
|
+
readRegularJson(evidenceFile, "Migration import evidence"),
|
|
1076
|
+
);
|
|
1077
|
+
const existingContractInfo = readRegularJson(
|
|
1078
|
+
contractInfoFile,
|
|
1079
|
+
"Migration contractInfo",
|
|
1080
|
+
);
|
|
1081
|
+
const existingDeploymentInfo = readRegularJson(
|
|
1082
|
+
deploymentInfoFile,
|
|
1083
|
+
"Migration deploymentInfo",
|
|
1084
|
+
);
|
|
1085
|
+
const existingBytecodeReport = readRegularJson(
|
|
1086
|
+
bytecodeReportFile,
|
|
1087
|
+
"Migration bytecode report",
|
|
1088
|
+
);
|
|
1089
|
+
const expectedNetworks = Object.fromEntries(
|
|
1090
|
+
Object.entries(plan.networks).map(([network, networkPlan]) => [
|
|
1091
|
+
network,
|
|
1092
|
+
{
|
|
1093
|
+
sourceNetwork: networkPlan.sourceNetwork,
|
|
1094
|
+
releaseStage: networkPlan.releaseStage,
|
|
1095
|
+
chainId: networkPlan.chainId,
|
|
1096
|
+
manifestDir: networkPlan.manifestDir,
|
|
1097
|
+
contracts: networkPlan.contracts.length,
|
|
1098
|
+
},
|
|
1099
|
+
]),
|
|
1100
|
+
);
|
|
1101
|
+
if (
|
|
1102
|
+
existing.version === 1
|
|
1103
|
+
&& existing.status === "imported"
|
|
1104
|
+
&& existing.migrationId === plan.migrationId
|
|
1105
|
+
&& existing.sourceProject === plan.sourceProject
|
|
1106
|
+
&& existing.planHash === planHash
|
|
1107
|
+
&& existing.contractInfoHash === contractInfoHash
|
|
1108
|
+
&& existing.deploymentInfoHash === deploymentInfoHash
|
|
1109
|
+
&& hashValue(existing.networks) === hashValue(expectedNetworks)
|
|
1110
|
+
&& hashValue(existing.confirmations) === hashValue(expectedConfirmations)
|
|
1111
|
+
&& existing.bytecodeCheck.reportPath === bytecodeReportRelative
|
|
1112
|
+
&& hashValue(existingBytecodeReport) === existing.bytecodeCheck.reportHash
|
|
1113
|
+
&& hashValue(existingContractInfo) === contractInfoHash
|
|
1114
|
+
&& hashValue(existingDeploymentInfo) === deploymentInfoHash
|
|
1115
|
+
) {
|
|
1116
|
+
return existing;
|
|
1117
|
+
}
|
|
1118
|
+
if (hashValue(existingBytecodeReport) !== existing.bytecodeCheck.reportHash) {
|
|
1119
|
+
throw new Error(`Migration bytecode report hash mismatch: ${bytecodeReportRelative}`);
|
|
1120
|
+
}
|
|
1121
|
+
throw new Error(`Migration evidence already exists with different state: ${evidenceRelative}`);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
if (fs.existsSync(bytecodeReportFile)) {
|
|
1125
|
+
throw new Error(
|
|
1126
|
+
`Migration bytecode report already exists without committed evidence: ${bytecodeReportRelative}`,
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const snapshots = [
|
|
1131
|
+
{ filePath: contractInfoFile, contents: snapshot(contractInfoFile) },
|
|
1132
|
+
{ filePath: deploymentInfoFile, contents: snapshot(deploymentInfoFile) },
|
|
1133
|
+
{ filePath: bytecodeReportFile, contents: snapshot(bytecodeReportFile) },
|
|
1134
|
+
{ filePath: evidenceFile, contents: snapshot(evidenceFile) },
|
|
1135
|
+
];
|
|
1136
|
+
const evidencePayload: MigrationImportEvidencePayload = {
|
|
1137
|
+
version: 1,
|
|
1138
|
+
status: "imported",
|
|
1139
|
+
migrationId: plan.migrationId,
|
|
1140
|
+
sourceProject: plan.sourceProject,
|
|
1141
|
+
planHash,
|
|
1142
|
+
contractInfoHash,
|
|
1143
|
+
deploymentInfoHash,
|
|
1144
|
+
networks: Object.fromEntries(Object.entries(plan.networks).map(([network, networkPlan]) => [
|
|
1145
|
+
network,
|
|
1146
|
+
{
|
|
1147
|
+
sourceNetwork: networkPlan.sourceNetwork,
|
|
1148
|
+
releaseStage: networkPlan.releaseStage,
|
|
1149
|
+
chainId: networkPlan.chainId,
|
|
1150
|
+
manifestDir: networkPlan.manifestDir,
|
|
1151
|
+
contracts: networkPlan.contracts.length,
|
|
1152
|
+
},
|
|
1153
|
+
])),
|
|
1154
|
+
confirmations: expectedConfirmations,
|
|
1155
|
+
bytecodeCheck: {
|
|
1156
|
+
status: "passed",
|
|
1157
|
+
reportPath: bytecodeReportRelative,
|
|
1158
|
+
reportHash: hashValue(bytecodeCheck.report),
|
|
1159
|
+
},
|
|
1160
|
+
before: {
|
|
1161
|
+
contractInfoHash: hashBuffer(snapshots[0].contents),
|
|
1162
|
+
deploymentInfoHash: hashBuffer(snapshots[1].contents),
|
|
1163
|
+
},
|
|
1164
|
+
importedAt,
|
|
1165
|
+
};
|
|
1166
|
+
const evidence: MigrationImportEvidence = {
|
|
1167
|
+
...evidencePayload,
|
|
1168
|
+
integrityHash: hashValue(evidencePayload),
|
|
1169
|
+
};
|
|
1170
|
+
const io = {
|
|
1171
|
+
...defaultIo,
|
|
1172
|
+
...(options.io ?? {}),
|
|
1173
|
+
} as Required<Pick<MigrationStateIo, "writeJson" | "rename">> & MigrationStateIo;
|
|
1174
|
+
const commitTargets: MigrationCommitTarget[] = [
|
|
1175
|
+
{
|
|
1176
|
+
...expectedCommitTargets[0],
|
|
1177
|
+
value: canonicalContractInfo,
|
|
1178
|
+
before: snapshots[0].contents,
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
...expectedCommitTargets[1],
|
|
1182
|
+
value: canonicalDeploymentInfo,
|
|
1183
|
+
before: snapshots[1].contents,
|
|
1184
|
+
},
|
|
1185
|
+
{
|
|
1186
|
+
...expectedCommitTargets[2],
|
|
1187
|
+
value: bytecodeCheck.report,
|
|
1188
|
+
before: snapshots[2].contents,
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
...expectedCommitTargets[3],
|
|
1192
|
+
value: evidence,
|
|
1193
|
+
before: snapshots[3].contents,
|
|
1194
|
+
},
|
|
1195
|
+
];
|
|
1196
|
+
try {
|
|
1197
|
+
stageMigrationTargets(root, commitTargets, io);
|
|
1198
|
+
const journalTargets: MigrationJournalTarget[] = commitTargets.map((target) => ({
|
|
1199
|
+
relativePath: target.relativePath,
|
|
1200
|
+
stagedRelativePath: target.stagedRelativePath as string,
|
|
1201
|
+
beforeBase64: target.before === undefined ? null : target.before.toString("base64"),
|
|
1202
|
+
beforeHash: hashBuffer(target.before),
|
|
1203
|
+
afterHash: hashBuffer(target.contents),
|
|
1204
|
+
}));
|
|
1205
|
+
const journal: MigrationTransactionJournal = {
|
|
1206
|
+
version: 1,
|
|
1207
|
+
status: "prepared",
|
|
1208
|
+
migrationId: plan.migrationId,
|
|
1209
|
+
planHash,
|
|
1210
|
+
evidenceHash: journalTargets.at(-1)?.afterHash as string,
|
|
1211
|
+
targets: journalTargets,
|
|
1212
|
+
};
|
|
1213
|
+
writeBufferNew(journalFile, serializeJson(journal));
|
|
1214
|
+
|
|
1215
|
+
for (const [index, target] of commitTargets.entries()) {
|
|
1216
|
+
io.rename(target.stagedPath as string, target.filePath);
|
|
1217
|
+
io.afterRename?.(target.stagedPath as string, target.filePath, index);
|
|
1218
|
+
const committedContents = snapshot(target.filePath);
|
|
1219
|
+
if (
|
|
1220
|
+
committedContents === undefined
|
|
1221
|
+
|| hashBuffer(committedContents) !== journalTargets[index].afterHash
|
|
1222
|
+
) {
|
|
1223
|
+
throw new Error(`Migration commit verification failed: ${target.relativePath}`);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
// Evidence is renamed last and is the immutable transaction commit marker.
|
|
1227
|
+
unlinkDurable(journalFile);
|
|
1228
|
+
return evidence;
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
if (fs.existsSync(journalFile)) {
|
|
1231
|
+
const disposition = recoverMigrationTransaction({
|
|
1232
|
+
root,
|
|
1233
|
+
journalFile,
|
|
1234
|
+
plan,
|
|
1235
|
+
planHash,
|
|
1236
|
+
expectedTargets: expectedCommitTargets,
|
|
1237
|
+
});
|
|
1238
|
+
if (disposition === "committed") return evidence;
|
|
1239
|
+
} else {
|
|
1240
|
+
cleanupUncommittedStages(commitTargets);
|
|
1241
|
+
restoreSnapshots(snapshots);
|
|
1242
|
+
}
|
|
1243
|
+
throw error;
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
};
|