movehat 0.2.1 → 0.2.2
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/dist/__tests__/deployContract.test.js +56 -47
- package/dist/__tests__/deployContract.test.js.map +1 -1
- package/dist/__tests__/exports.test.d.ts +2 -0
- package/dist/__tests__/exports.test.d.ts.map +1 -0
- package/dist/__tests__/exports.test.js +30 -0
- package/dist/__tests__/exports.test.js.map +1 -0
- package/dist/__tests__/fixtures/sigint-deploy-harness.d.ts +4 -3
- package/dist/__tests__/fixtures/sigint-deploy-harness.d.ts.map +1 -1
- package/dist/__tests__/fixtures/sigint-deploy-harness.js +8 -7
- package/dist/__tests__/fixtures/sigint-deploy-harness.js.map +1 -1
- package/dist/__tests__/fork/api.test.js +5 -0
- package/dist/__tests__/fork/api.test.js.map +1 -1
- package/dist/__tests__/fork/api.timeout.test.d.ts +2 -0
- package/dist/__tests__/fork/api.timeout.test.d.ts.map +1 -0
- package/dist/__tests__/fork/api.timeout.test.js +98 -0
- package/dist/__tests__/fork/api.timeout.test.js.map +1 -0
- package/dist/commands/__tests__/compile.toml-mutation.test.d.ts +2 -0
- package/dist/commands/__tests__/compile.toml-mutation.test.d.ts.map +1 -0
- package/dist/commands/__tests__/compile.toml-mutation.test.js +69 -0
- package/dist/commands/__tests__/compile.toml-mutation.test.js.map +1 -0
- package/dist/commands/__tests__/init.test.js +73 -11
- package/dist/commands/__tests__/init.test.js.map +1 -1
- package/dist/commands/init.d.ts +22 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +55 -6
- package/dist/commands/init.js.map +1 -1
- package/dist/core/AccountManager.d.ts.map +1 -1
- package/dist/core/AccountManager.js +14 -2
- package/dist/core/AccountManager.js.map +1 -1
- package/dist/core/Publisher.d.ts.map +1 -1
- package/dist/core/Publisher.js +52 -68
- package/dist/core/Publisher.js.map +1 -1
- package/dist/core/__tests__/AccountManager.global-state.test.d.ts +2 -0
- package/dist/core/__tests__/AccountManager.global-state.test.d.ts.map +1 -0
- package/dist/core/__tests__/AccountManager.global-state.test.js +69 -0
- package/dist/core/__tests__/AccountManager.global-state.test.js.map +1 -0
- package/dist/core/__tests__/movementProfile.test.d.ts +2 -0
- package/dist/core/__tests__/movementProfile.test.d.ts.map +1 -0
- package/dist/core/__tests__/movementProfile.test.js +112 -0
- package/dist/core/__tests__/movementProfile.test.js.map +1 -0
- package/dist/core/config.js +6 -5
- package/dist/core/config.js.map +1 -1
- package/dist/core/movementProfile.d.ts +55 -22
- package/dist/core/movementProfile.d.ts.map +1 -1
- package/dist/core/movementProfile.js +77 -99
- package/dist/core/movementProfile.js.map +1 -1
- package/dist/fork/__tests__/server.cors.test.d.ts +2 -0
- package/dist/fork/__tests__/server.cors.test.d.ts.map +1 -0
- package/dist/fork/__tests__/server.cors.test.js +79 -0
- package/dist/fork/__tests__/server.cors.test.js.map +1 -0
- package/dist/fork/api.d.ts +9 -1
- package/dist/fork/api.d.ts.map +1 -1
- package/dist/fork/api.js +37 -7
- package/dist/fork/api.js.map +1 -1
- package/dist/fork/server.d.ts +20 -1
- package/dist/fork/server.d.ts.map +1 -1
- package/dist/fork/server.js +19 -9
- package/dist/fork/server.js.map +1 -1
- package/dist/harness/Harness.d.ts +6 -2
- package/dist/harness/Harness.d.ts.map +1 -1
- package/dist/harness/Harness.js +8 -2
- package/dist/harness/Harness.js.map +1 -1
- package/dist/harness/codeObject.d.ts.map +1 -1
- package/dist/harness/codeObject.js +30 -33
- package/dist/harness/codeObject.js.map +1 -1
- package/dist/harness/script.d.ts +3 -3
- package/dist/harness/script.d.ts.map +1 -1
- package/dist/harness/script.js +33 -29
- package/dist/harness/script.js.map +1 -1
- package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.d.ts +2 -0
- package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.d.ts.map +1 -0
- package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.js +172 -0
- package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.js.map +1 -0
- package/dist/helpers/setupLocalTesting.d.ts.map +1 -1
- package/dist/helpers/setupLocalTesting.js +28 -2
- package/dist/helpers/setupLocalTesting.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/node/LocalNodeManager.d.ts +8 -0
- package/dist/node/LocalNodeManager.d.ts.map +1 -1
- package/dist/node/LocalNodeManager.js +10 -1
- package/dist/node/LocalNodeManager.js.map +1 -1
- package/dist/node/__tests__/LocalNodeManager.api-port.test.d.ts +2 -0
- package/dist/node/__tests__/LocalNodeManager.api-port.test.d.ts.map +1 -0
- package/dist/node/__tests__/LocalNodeManager.api-port.test.js +55 -0
- package/dist/node/__tests__/LocalNodeManager.api-port.test.js.map +1 -0
- package/dist/node/__tests__/LocalNodeManager.test.js +4 -3
- package/dist/node/__tests__/LocalNodeManager.test.js.map +1 -1
- package/dist/templates/move/Move.toml +1 -1
- package/dist/templates/move/sources/Counter.move +31 -4
- package/dist/templates/scripts/deploy-counter.ts +10 -0
- package/dist/types/config.d.ts +8 -1
- package/dist/types/config.d.ts.map +1 -1
- package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.d.ts +2 -0
- package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.d.ts.map +1 -0
- package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.js +43 -0
- package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.js.map +1 -0
- package/dist/utils/childProcessAdapter.d.ts +7 -0
- package/dist/utils/childProcessAdapter.d.ts.map +1 -1
- package/dist/utils/childProcessAdapter.js +20 -2
- package/dist/utils/childProcessAdapter.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/deployContract.test.ts +59 -50
- package/src/__tests__/exports.test.ts +32 -0
- package/src/__tests__/fixtures/sigint-deploy-harness.ts +8 -7
- package/src/__tests__/fork/api.test.ts +5 -0
- package/src/__tests__/fork/api.timeout.test.ts +150 -0
- package/src/commands/__tests__/compile.toml-mutation.test.ts +77 -0
- package/src/commands/__tests__/init.test.ts +96 -11
- package/src/commands/init.ts +77 -6
- package/src/core/AccountManager.ts +18 -1
- package/src/core/Publisher.ts +58 -77
- package/src/core/__tests__/AccountManager.global-state.test.ts +83 -0
- package/src/core/__tests__/movementProfile.test.ts +131 -0
- package/src/core/config.ts +9 -5
- package/src/core/movementProfile.ts +75 -127
- package/src/fork/__tests__/server.cors.test.ts +101 -0
- package/src/fork/api.ts +69 -10
- package/src/fork/server.ts +38 -9
- package/src/harness/Harness.ts +11 -2
- package/src/harness/codeObject.ts +37 -43
- package/src/harness/script.ts +40 -39
- package/src/helpers/__tests__/setupLocalTesting.fork-network.test.ts +212 -0
- package/src/helpers/setupLocalTesting.ts +36 -2
- package/src/index.ts +9 -1
- package/src/node/LocalNodeManager.ts +24 -2
- package/src/node/__tests__/LocalNodeManager.api-port.test.ts +62 -0
- package/src/node/__tests__/LocalNodeManager.test.ts +4 -3
- package/src/templates/move/Move.toml +1 -1
- package/src/templates/move/sources/Counter.move +31 -4
- package/src/templates/scripts/deploy-counter.ts +10 -0
- package/src/types/config.ts +8 -1
- package/src/utils/__tests__/childProcessAdapter.maxBuffer.test.ts +51 -0
- package/src/utils/childProcessAdapter.ts +32 -2
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
import { randomUUID } from "crypto";
|
|
1
|
+
import { PrivateKey, PrivateKeyVariants } from "@aptos-labs/ts-sdk";
|
|
4
2
|
import type { MovehatRuntime } from "../types/runtime.js";
|
|
5
3
|
import type {
|
|
6
4
|
DeployCodeObjectOptions,
|
|
@@ -14,7 +12,7 @@ import {
|
|
|
14
12
|
validateSafeName,
|
|
15
13
|
type DeploymentInfo,
|
|
16
14
|
} from "../core/deployments.js";
|
|
17
|
-
import { validatePathSafety
|
|
15
|
+
import { validatePathSafety } from "../core/shell.js";
|
|
18
16
|
import {
|
|
19
17
|
CliExecutionError,
|
|
20
18
|
ModuleAlreadyDeployedError,
|
|
@@ -24,10 +22,9 @@ import { runCli } from "../utils/runCli.js";
|
|
|
24
22
|
import { parseTxHash } from "../utils/parseCliOutput.js";
|
|
25
23
|
import { logger } from "../ui/index.js";
|
|
26
24
|
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
removeProfileSync,
|
|
25
|
+
writeTempKeyFile,
|
|
26
|
+
removeKeyFile,
|
|
27
|
+
removeKeyFileSyncBestEffort,
|
|
31
28
|
ensureSignalHandler,
|
|
32
29
|
cleanupCallbacks,
|
|
33
30
|
} from "../core/movementProfile.js";
|
|
@@ -170,9 +167,7 @@ async function executeMovementMoveObject(
|
|
|
170
167
|
}
|
|
171
168
|
|
|
172
169
|
const dir = opts.packageDir || config.moveDir;
|
|
173
|
-
const profile = `movehat-deploy-${randomUUID().slice(0, 8)}`;
|
|
174
170
|
const safeDir = validatePathSafety(dir, "package directory");
|
|
175
|
-
const safeProfile = validateProfileSafety(profile);
|
|
176
171
|
|
|
177
172
|
logger.step(
|
|
178
173
|
`${subcommand === "deploy-object" ? "Deploying" : "Upgrading"} module "${moduleName}" from ${dir}...`
|
|
@@ -213,30 +208,28 @@ async function executeMovementMoveObject(
|
|
|
213
208
|
);
|
|
214
209
|
if (buildResult.stdout) console.log(buildResult.stdout.trim());
|
|
215
210
|
|
|
216
|
-
//
|
|
217
|
-
// raw
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
211
|
+
// Format the private key into AIP-80 shape so the Movement CLI
|
|
212
|
+
// doesn't emit its raw-hex deprecation warning. `formatPrivateKey`
|
|
213
|
+
// is idempotent for already-prefixed inputs.
|
|
214
|
+
const formattedPrivateKey = PrivateKey.formatPrivateKey(
|
|
215
|
+
config.privateKey,
|
|
216
|
+
PrivateKeyVariants.Ed25519,
|
|
217
|
+
);
|
|
222
218
|
|
|
223
|
-
|
|
219
|
+
// Pass the private key via a 0o600 temp file (--private-key-file)
|
|
220
|
+
// and the on-chain address via --sender-account. This avoids the
|
|
221
|
+
// CLI's profile-yaml lookup entirely — no CWD / HOME / .aptos /
|
|
222
|
+
// .movement dance, no CLI-variant dependency.
|
|
223
|
+
const keyFilePath = writeTempKeyFile(formattedPrivateKey);
|
|
224
224
|
|
|
225
|
-
// Register SIGINT-safe sync cleanup BEFORE
|
|
226
|
-
//
|
|
225
|
+
// Register SIGINT-safe sync cleanup BEFORE invoking the CLI so
|
|
226
|
+
// the private key never persists on disk after an abnormal exit.
|
|
227
|
+
// The signal-handler path uses the best-effort variant because the
|
|
228
|
+
// event loop is dead and we cannot logger.warning.
|
|
227
229
|
ensureSignalHandler();
|
|
228
|
-
const syncCleanup = () =>
|
|
230
|
+
const syncCleanup = () => removeKeyFileSyncBestEffort(keyFilePath);
|
|
229
231
|
cleanupCallbacks.add(syncCleanup);
|
|
230
232
|
|
|
231
|
-
await withYamlLock(() =>
|
|
232
|
-
addProfile(movementConfigPath, profile, {
|
|
233
|
-
private_key: cleanPrivateKey,
|
|
234
|
-
public_key: account.publicKey.toString(),
|
|
235
|
-
account: deployerAddress,
|
|
236
|
-
rest_url: config.rpc,
|
|
237
|
-
})
|
|
238
|
-
);
|
|
239
|
-
|
|
240
233
|
let deployOut = "";
|
|
241
234
|
try {
|
|
242
235
|
logger.step(
|
|
@@ -258,8 +251,10 @@ async function executeMovementMoveObject(
|
|
|
258
251
|
safeDir,
|
|
259
252
|
"--url",
|
|
260
253
|
config.rpc,
|
|
261
|
-
"--
|
|
262
|
-
|
|
254
|
+
"--private-key-file",
|
|
255
|
+
keyFilePath,
|
|
256
|
+
"--sender-account",
|
|
257
|
+
deployerAddress,
|
|
263
258
|
"--assume-yes",
|
|
264
259
|
...includedArtifacts,
|
|
265
260
|
...namedAddrArgs,
|
|
@@ -273,18 +268,17 @@ async function executeMovementMoveObject(
|
|
|
273
268
|
if (result.stdout) console.log(result.stdout.trim());
|
|
274
269
|
if (result.stderr) console.error(result.stderr.trim());
|
|
275
270
|
} finally {
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
);
|
|
271
|
+
// Unlink via the observable helper — emit a warning if the file
|
|
272
|
+
// could not be removed AND still exists on disk (private key
|
|
273
|
+
// would persist silently otherwise). ENOENT and races are
|
|
274
|
+
// treated as benign success.
|
|
275
|
+
const cleanupErr = removeKeyFile(keyFilePath);
|
|
276
|
+
if (cleanupErr) {
|
|
277
|
+
logger.warning(
|
|
278
|
+
`Failed to remove temp key file '${keyFilePath}': ${cleanupErr.message}. ` +
|
|
279
|
+
`The file has mode 0o600 but should be removed manually: rm ${keyFilePath}`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
288
282
|
cleanupCallbacks.delete(syncCleanup);
|
|
289
283
|
}
|
|
290
284
|
|
package/src/harness/script.ts
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
import { existsSync } from "fs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { randomUUID } from "crypto";
|
|
2
|
+
import { extname } from "path";
|
|
3
|
+
import { PrivateKey, PrivateKeyVariants } from "@aptos-labs/ts-sdk";
|
|
5
4
|
import type { MovehatRuntime } from "../types/runtime.js";
|
|
6
5
|
import type {
|
|
7
6
|
RunMoveScriptOptions,
|
|
8
7
|
MoveScriptResult,
|
|
9
8
|
} from "../types/harness.js";
|
|
10
|
-
import { validatePathSafety
|
|
9
|
+
import { validatePathSafety } from "../core/shell.js";
|
|
11
10
|
import { CliExecutionError } from "../errors.js";
|
|
12
11
|
import { runCli } from "../utils/runCli.js";
|
|
13
12
|
import { parseTxHash } from "../utils/parseCliOutput.js";
|
|
14
13
|
import { logger } from "../ui/index.js";
|
|
15
14
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
removeProfileSync,
|
|
15
|
+
writeTempKeyFile,
|
|
16
|
+
removeKeyFile,
|
|
17
|
+
removeKeyFileSyncBestEffort,
|
|
20
18
|
ensureSignalHandler,
|
|
21
19
|
cleanupCallbacks,
|
|
22
20
|
} from "../core/movementProfile.js";
|
|
@@ -29,9 +27,9 @@ import {
|
|
|
29
27
|
* - `.mv` compiled bytecode → `--compiled-script-path`
|
|
30
28
|
*
|
|
31
29
|
* Reuses Publisher's security model via the shared `movementProfile`
|
|
32
|
-
* helpers: per-
|
|
33
|
-
*
|
|
34
|
-
*
|
|
30
|
+
* helpers: per-invocation temp key file (0o600), SIGINT-safe sync
|
|
31
|
+
* cleanup, `--private-key-file` auth (key never appears in `ps`
|
|
32
|
+
* output or in the user's `~/.aptos/config.yaml`).
|
|
35
33
|
*
|
|
36
34
|
* Returns {@link MoveScriptResult}. `txHash` is guaranteed; `success`
|
|
37
35
|
* and `vmStatus` are best-effort parsed from the CLI's Result JSON.
|
|
@@ -70,8 +68,6 @@ export async function runMoveScript(
|
|
|
70
68
|
}
|
|
71
69
|
|
|
72
70
|
const safeScriptPath = validatePathSafety(options.scriptPath, "script path");
|
|
73
|
-
const profile = `movehat-script-${randomUUID().slice(0, 8)}`;
|
|
74
|
-
const safeProfile = validateProfileSafety(profile);
|
|
75
71
|
|
|
76
72
|
logger.step(
|
|
77
73
|
`Running Move script '${options.scriptPath}' on ${config.network}...`
|
|
@@ -80,26 +76,28 @@ export async function runMoveScript(
|
|
|
80
76
|
try {
|
|
81
77
|
const deployerAddress = account.accountAddress.toString();
|
|
82
78
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
// Format the private key into AIP-80 shape before writing to the
|
|
80
|
+
// temp key file. `formatPrivateKey` is idempotent for already-
|
|
81
|
+
// prefixed inputs.
|
|
82
|
+
const formattedPrivateKey = PrivateKey.formatPrivateKey(
|
|
83
|
+
config.privateKey,
|
|
84
|
+
PrivateKeyVariants.Ed25519,
|
|
85
|
+
);
|
|
87
86
|
|
|
88
|
-
|
|
87
|
+
// Pass the private key via a 0o600 temp file (--private-key-file)
|
|
88
|
+
// and the on-chain address via --sender-account. Avoids the CLI's
|
|
89
|
+
// profile-yaml lookup chain entirely (no CWD / HOME / .aptos /
|
|
90
|
+
// .movement dance, no CLI-variant dependency).
|
|
91
|
+
const keyFilePath = writeTempKeyFile(formattedPrivateKey);
|
|
89
92
|
|
|
93
|
+
// SIGINT-safe sync cleanup BEFORE the CLI call so the private key
|
|
94
|
+
// never persists on disk after an abnormal exit. The signal-handler
|
|
95
|
+
// path uses the best-effort variant because the event loop is dead
|
|
96
|
+
// and we cannot logger.warning.
|
|
90
97
|
ensureSignalHandler();
|
|
91
|
-
const syncCleanup = () =>
|
|
98
|
+
const syncCleanup = () => removeKeyFileSyncBestEffort(keyFilePath);
|
|
92
99
|
cleanupCallbacks.add(syncCleanup);
|
|
93
100
|
|
|
94
|
-
await withYamlLock(() =>
|
|
95
|
-
addProfile(movementConfigPath, profile, {
|
|
96
|
-
private_key: cleanPrivateKey,
|
|
97
|
-
public_key: account.publicKey.toString(),
|
|
98
|
-
account: deployerAddress,
|
|
99
|
-
rest_url: config.rpc,
|
|
100
|
-
})
|
|
101
|
-
);
|
|
102
|
-
|
|
103
101
|
let scriptOut = "";
|
|
104
102
|
try {
|
|
105
103
|
const typeArgsFragment: string[] =
|
|
@@ -117,8 +115,10 @@ export async function runMoveScript(
|
|
|
117
115
|
args: [
|
|
118
116
|
"move",
|
|
119
117
|
"run-script",
|
|
120
|
-
"--
|
|
121
|
-
|
|
118
|
+
"--private-key-file",
|
|
119
|
+
keyFilePath,
|
|
120
|
+
"--sender-account",
|
|
121
|
+
deployerAddress,
|
|
122
122
|
"--url",
|
|
123
123
|
config.rpc,
|
|
124
124
|
"--assume-yes",
|
|
@@ -135,15 +135,16 @@ export async function runMoveScript(
|
|
|
135
135
|
if (result.stdout) console.log(result.stdout.trim());
|
|
136
136
|
if (result.stderr) console.error(result.stderr.trim());
|
|
137
137
|
} finally {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
138
|
+
// Observable cleanup — emit a warning if the unlink failed and
|
|
139
|
+
// the file is still on disk (private key would persist silently
|
|
140
|
+
// otherwise).
|
|
141
|
+
const cleanupErr = removeKeyFile(keyFilePath);
|
|
142
|
+
if (cleanupErr) {
|
|
143
|
+
logger.warning(
|
|
144
|
+
`Failed to remove temp key file '${keyFilePath}': ${cleanupErr.message}. ` +
|
|
145
|
+
`The file has mode 0o600 but should be removed manually: rm ${keyFilePath}`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
147
148
|
cleanupCallbacks.delete(syncCleanup);
|
|
148
149
|
}
|
|
149
150
|
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* F1 — Harness.createFork(network) must honor the requested network.
|
|
8
|
+
*
|
|
9
|
+
* Mocks ForkManager so we can capture which RPC URL the fresh-fork
|
|
10
|
+
* code path picks. Strategy mirrors the pattern in
|
|
11
|
+
* src/fork/__tests__/manager.test.ts: replace the manager module with
|
|
12
|
+
* a stub that records every call to `initialize`.
|
|
13
|
+
*
|
|
14
|
+
* The mock also implements `load()` + `getMetadata()` so the
|
|
15
|
+
* "fork-exists, wrong network" case (audit-f1 follow-up) can exercise
|
|
16
|
+
* the metadata-mismatch guard in the `else` branch of `setupWithFork`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
interface InitCall {
|
|
20
|
+
nodeUrl: string;
|
|
21
|
+
networkName?: string;
|
|
22
|
+
apiKey?: string;
|
|
23
|
+
}
|
|
24
|
+
const initializeCalls: InitCall[] = [];
|
|
25
|
+
|
|
26
|
+
vi.mock("../../fork/manager.js", async () => {
|
|
27
|
+
const fs = await import("node:fs");
|
|
28
|
+
return {
|
|
29
|
+
ForkManager: class {
|
|
30
|
+
forkPath: string;
|
|
31
|
+
metadata: { network: string; nodeUrl: string } | null = null;
|
|
32
|
+
constructor(forkPath: string) {
|
|
33
|
+
this.forkPath = forkPath;
|
|
34
|
+
}
|
|
35
|
+
async initialize(nodeUrl: string, networkName?: string, apiKey?: string) {
|
|
36
|
+
const entry: InitCall = { nodeUrl };
|
|
37
|
+
if (networkName !== undefined) entry.networkName = networkName;
|
|
38
|
+
if (apiKey !== undefined) entry.apiKey = apiKey;
|
|
39
|
+
initializeCalls.push(entry);
|
|
40
|
+
this.metadata = { network: networkName ?? "custom", nodeUrl };
|
|
41
|
+
}
|
|
42
|
+
load() {
|
|
43
|
+
const raw = fs.readFileSync(`${this.forkPath}/metadata.json`, "utf-8");
|
|
44
|
+
this.metadata = JSON.parse(raw);
|
|
45
|
+
}
|
|
46
|
+
getMetadata() {
|
|
47
|
+
if (!this.metadata) {
|
|
48
|
+
throw new Error("Fork not initialized");
|
|
49
|
+
}
|
|
50
|
+
return this.metadata;
|
|
51
|
+
}
|
|
52
|
+
setApiKey() {}
|
|
53
|
+
async resetState() {}
|
|
54
|
+
async fundAccount() {}
|
|
55
|
+
async fundMultipleAccounts() {}
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
vi.mock("../../fork/server.js", () => {
|
|
61
|
+
return {
|
|
62
|
+
ForkServer: class {
|
|
63
|
+
constructor(_p: string, _port: number) {}
|
|
64
|
+
async start() {}
|
|
65
|
+
async stop() {}
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
vi.mock("../../runtime.js", () => ({
|
|
71
|
+
initRuntime: vi.fn(async () => ({})),
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
vi.mock("../../core/AccountManager.js", () => {
|
|
75
|
+
let _seq = 0;
|
|
76
|
+
return {
|
|
77
|
+
AccountManager: {
|
|
78
|
+
createBatch(labels: readonly string[]) {
|
|
79
|
+
const out: Record<string, { accountAddress: { toString(): string } }> = {};
|
|
80
|
+
for (const l of labels) {
|
|
81
|
+
_seq++;
|
|
82
|
+
const addr = "0x" + _seq.toString(16).padStart(64, "0");
|
|
83
|
+
out[l] = { accountAddress: { toString: () => addr } };
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
},
|
|
87
|
+
exportPrivateKeys(_labels: readonly string[]) {
|
|
88
|
+
return { deployer: "0x" + "1".repeat(64) };
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Imported after mocks so vi.hoisted ordering applies.
|
|
95
|
+
import { setupLocalTesting } from "../setupLocalTesting.js";
|
|
96
|
+
import { logger } from "../../ui/index.js";
|
|
97
|
+
|
|
98
|
+
describe("F1 — setupLocalTesting honors forkNetwork", () => {
|
|
99
|
+
let cwdBackup: string;
|
|
100
|
+
let tmpRoot: string;
|
|
101
|
+
|
|
102
|
+
beforeEach(() => {
|
|
103
|
+
initializeCalls.length = 0;
|
|
104
|
+
cwdBackup = process.cwd();
|
|
105
|
+
tmpRoot = mkdtempSync(join(tmpdir(), "movehat-f1-"));
|
|
106
|
+
process.chdir(tmpRoot);
|
|
107
|
+
vi.spyOn(logger, "step").mockImplementation(() => undefined);
|
|
108
|
+
vi.spyOn(logger, "success").mockImplementation(() => undefined);
|
|
109
|
+
vi.spyOn(logger, "plain").mockImplementation(() => undefined);
|
|
110
|
+
vi.spyOn(logger, "newline").mockImplementation(() => undefined);
|
|
111
|
+
vi.spyOn(logger, "warning").mockImplementation(() => undefined);
|
|
112
|
+
vi.spyOn(logger, "error").mockImplementation(() => undefined);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
afterEach(() => {
|
|
116
|
+
process.chdir(cwdBackup);
|
|
117
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
118
|
+
vi.restoreAllMocks();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("uses the mainnet RPC when forkNetwork = 'mainnet'", async () => {
|
|
122
|
+
await setupLocalTesting({
|
|
123
|
+
mode: "fork",
|
|
124
|
+
forkNetwork: "mainnet",
|
|
125
|
+
accountLabels: ["deployer"],
|
|
126
|
+
autoFund: false,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(initializeCalls).toHaveLength(1);
|
|
130
|
+
const call = initializeCalls[0]!;
|
|
131
|
+
expect(call.networkName).toBe("mainnet");
|
|
132
|
+
expect(call.nodeUrl).not.toMatch(/testnet/i);
|
|
133
|
+
expect(call.nodeUrl).toMatch(/mainnet/i);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("uses the testnet RPC when forkNetwork = 'testnet'", async () => {
|
|
137
|
+
await setupLocalTesting({
|
|
138
|
+
mode: "fork",
|
|
139
|
+
forkNetwork: "testnet",
|
|
140
|
+
accountLabels: ["deployer"],
|
|
141
|
+
autoFund: false,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(initializeCalls).toHaveLength(1);
|
|
145
|
+
const call = initializeCalls[0]!;
|
|
146
|
+
expect(call.networkName).toBe("testnet");
|
|
147
|
+
expect(call.nodeUrl).toMatch(/testnet/i);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("uses forkRpcUrl override when supplied for a custom network", async () => {
|
|
151
|
+
await setupLocalTesting({
|
|
152
|
+
mode: "fork",
|
|
153
|
+
forkNetwork: "custom",
|
|
154
|
+
forkRpcUrl: "https://my-custom-node.example/v1",
|
|
155
|
+
accountLabels: ["deployer"],
|
|
156
|
+
autoFund: false,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(initializeCalls).toHaveLength(1);
|
|
160
|
+
const call = initializeCalls[0]!;
|
|
161
|
+
expect(call.networkName).toBe("custom");
|
|
162
|
+
expect(call.nodeUrl).toBe("https://my-custom-node.example/v1");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("rejects a non-built-in forkNetwork when no forkRpcUrl is provided", async () => {
|
|
166
|
+
await expect(
|
|
167
|
+
setupLocalTesting({
|
|
168
|
+
mode: "fork",
|
|
169
|
+
forkNetwork: "some-unknown-network",
|
|
170
|
+
accountLabels: ["deployer"],
|
|
171
|
+
autoFund: false,
|
|
172
|
+
})
|
|
173
|
+
).rejects.toThrow(/forkRpcUrl/i);
|
|
174
|
+
expect(initializeCalls).toHaveLength(0);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("rejects when an existing fork's saved network does not match the requested one (audit-f1 follow-up)", async () => {
|
|
178
|
+
// Pre-seed `.movehat/forks/test-local/metadata.json` with the
|
|
179
|
+
// wrong network so the `forkExists` branch fires and loads stale
|
|
180
|
+
// metadata. Without the metadata-mismatch guard, setupLocalTesting
|
|
181
|
+
// would silently serve a testnet snapshot while the caller thinks
|
|
182
|
+
// it's reading mainnet.
|
|
183
|
+
const forkDir = join(tmpRoot, ".movehat", "forks", "test-local");
|
|
184
|
+
mkdirSync(forkDir, { recursive: true });
|
|
185
|
+
writeFileSync(
|
|
186
|
+
join(forkDir, "metadata.json"),
|
|
187
|
+
JSON.stringify({
|
|
188
|
+
network: "testnet",
|
|
189
|
+
nodeUrl: "https://testnet.movementnetwork.xyz/v1",
|
|
190
|
+
chainId: 250,
|
|
191
|
+
ledgerVersion: "0",
|
|
192
|
+
timestamp: "0",
|
|
193
|
+
epoch: "0",
|
|
194
|
+
blockHeight: "0",
|
|
195
|
+
createdAt: new Date().toISOString(),
|
|
196
|
+
}),
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
await expect(
|
|
200
|
+
setupLocalTesting({
|
|
201
|
+
mode: "fork",
|
|
202
|
+
forkNetwork: "mainnet",
|
|
203
|
+
accountLabels: ["deployer"],
|
|
204
|
+
autoFund: false,
|
|
205
|
+
})
|
|
206
|
+
).rejects.toThrow(/network mismatch|created for|requested/i);
|
|
207
|
+
// Must not have re-initialized — the existing dir is what poisons
|
|
208
|
+
// the load path, and silently reinitializing would clobber the
|
|
209
|
+
// user's saved snapshot.
|
|
210
|
+
expect(initializeCalls).toHaveLength(0);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -9,6 +9,25 @@ import { AccountManager } from "../core/AccountManager.js";
|
|
|
9
9
|
import { logger } from "../ui/index.js";
|
|
10
10
|
import type { LocalTestOptions } from "../types/config.js";
|
|
11
11
|
|
|
12
|
+
const BUILTIN_FORK_RPCS: Record<string, string> = {
|
|
13
|
+
testnet: "https://testnet.movementnetwork.xyz/v1",
|
|
14
|
+
mainnet: "https://mainnet.movementnetwork.xyz/v1",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function resolveForkRpcUrl(
|
|
18
|
+
network: string,
|
|
19
|
+
override: string | undefined
|
|
20
|
+
): string {
|
|
21
|
+
if (override !== undefined) return override;
|
|
22
|
+
const builtin = BUILTIN_FORK_RPCS[network];
|
|
23
|
+
if (builtin !== undefined) return builtin;
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Cannot fork unknown network "${network}" without a forkRpcUrl. ` +
|
|
26
|
+
`Either pass forkRpcUrl in LocalTestOptions or use one of: ` +
|
|
27
|
+
`${Object.keys(BUILTIN_FORK_RPCS).join(", ")}.`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
12
31
|
/**
|
|
13
32
|
* Context returned by {@link setupLocalTesting}.
|
|
14
33
|
*
|
|
@@ -264,8 +283,8 @@ async function setupWithFork(
|
|
|
264
283
|
|
|
265
284
|
if (!forkExists) {
|
|
266
285
|
logger.step(`Creating fork from ${forkNetwork}...`);
|
|
267
|
-
const
|
|
268
|
-
await forkManager.initialize(
|
|
286
|
+
const rpcUrl = resolveForkRpcUrl(forkNetwork, options.forkRpcUrl);
|
|
287
|
+
await forkManager.initialize(rpcUrl, forkNetwork, options.forkApiKey);
|
|
269
288
|
logger.success(`Fork created at ${forkPath}`);
|
|
270
289
|
logger.newline();
|
|
271
290
|
} else {
|
|
@@ -278,6 +297,21 @@ async function setupWithFork(
|
|
|
278
297
|
}
|
|
279
298
|
forkManager.load();
|
|
280
299
|
|
|
300
|
+
// Guard against the audit-f1 follow-up case: the default forkName
|
|
301
|
+
// ("test-local") doesn't encode the network, so a fork created for
|
|
302
|
+
// testnet would silently serve mainnet requests. Refuse to load
|
|
303
|
+
// when the saved metadata's network doesn't match what the caller
|
|
304
|
+
// asked for — the user must either pass a network-specific
|
|
305
|
+
// `forkName` or delete the stale directory.
|
|
306
|
+
const savedNetwork = forkManager.getMetadata().network;
|
|
307
|
+
if (savedNetwork !== forkNetwork) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
`Fork at ${forkPath} was created for network "${savedNetwork}" but ` +
|
|
310
|
+
`you requested "${forkNetwork}". Use a different forkName ` +
|
|
311
|
+
`(e.g. "${forkNetwork}-local") or delete ${forkPath} to recreate.`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
281
315
|
if (forkResetState) {
|
|
282
316
|
logger.step("Resetting fork state...");
|
|
283
317
|
await forkManager.resetState();
|
package/src/index.ts
CHANGED
|
@@ -19,4 +19,12 @@ export type { ForkMetadata, AccountState, LedgerInfo, AccountData, AccountResour
|
|
|
19
19
|
export { ModuleAlreadyDeployedError, PostPublishError } from "./errors.js";
|
|
20
20
|
|
|
21
21
|
export { Harness, HarnessDisposedError } from "./harness/index.js";
|
|
22
|
-
export type { HarnessMode } from "./harness/index.js";
|
|
22
|
+
export type { HarnessMode } from "./harness/index.js";
|
|
23
|
+
export type {
|
|
24
|
+
DeployCodeObjectOptions,
|
|
25
|
+
UpgradeCodeObjectOptions,
|
|
26
|
+
CodeObjectInfo,
|
|
27
|
+
RunViewFunctionOptions,
|
|
28
|
+
RunMoveScriptOptions,
|
|
29
|
+
MoveScriptResult,
|
|
30
|
+
} from "./types/harness.js";
|
|
@@ -12,7 +12,15 @@ export interface LocalNodeOptions {
|
|
|
12
12
|
testDir?: string; // Directory for node data (default: .movehat/local-node)
|
|
13
13
|
forceRestart?: boolean; // Clean state and start fresh
|
|
14
14
|
faucetPort?: number; // Faucet port (default: 8081)
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* REST API port. Movement CLI (`movement node run-localnet`) does
|
|
17
|
+
* not accept a flag to change this — the node always binds 8080.
|
|
18
|
+
* Passing any other value triggers a warning at construction time
|
|
19
|
+
* and is replaced with 8080. Field is kept for source compatibility.
|
|
20
|
+
*
|
|
21
|
+
* @deprecated Movement CLI does not honor this. Omit it.
|
|
22
|
+
*/
|
|
23
|
+
apiPort?: number;
|
|
16
24
|
readyPort?: number; // Ready server port (default: 8070)
|
|
17
25
|
silent?: boolean; // Suppress node output
|
|
18
26
|
/**
|
|
@@ -22,6 +30,8 @@ export interface LocalNodeOptions {
|
|
|
22
30
|
adapter?: ChildProcessAdapter;
|
|
23
31
|
}
|
|
24
32
|
|
|
33
|
+
const MOVEMENT_API_PORT = 8080;
|
|
34
|
+
|
|
25
35
|
export interface LocalNodeInfo {
|
|
26
36
|
rpcUrl: string;
|
|
27
37
|
faucetUrl: string;
|
|
@@ -47,11 +57,23 @@ export class LocalNodeManager {
|
|
|
47
57
|
|
|
48
58
|
constructor(options: LocalNodeOptions = {}) {
|
|
49
59
|
this.adapter = options.adapter ?? defaultChildProcessAdapter;
|
|
60
|
+
if (
|
|
61
|
+
options.apiPort !== undefined &&
|
|
62
|
+
options.apiPort !== MOVEMENT_API_PORT
|
|
63
|
+
) {
|
|
64
|
+
// Movement CLI hardcodes the REST API port to 8080. Surfacing
|
|
65
|
+
// the requested port via getNodeInfo() would lie about where
|
|
66
|
+
// the node actually listens.
|
|
67
|
+
logger.warning(
|
|
68
|
+
`LocalNodeManager: apiPort=${options.apiPort} is not supported by ` +
|
|
69
|
+
`movement node run-localnet; forcing REST API port to 8080.`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
50
72
|
this.options = {
|
|
51
73
|
testDir: options.testDir || join(process.cwd(), ".movehat", "local-node"),
|
|
52
74
|
forceRestart: options.forceRestart ?? false,
|
|
53
75
|
faucetPort: options.faucetPort || 8081,
|
|
54
|
-
apiPort:
|
|
76
|
+
apiPort: MOVEMENT_API_PORT,
|
|
55
77
|
readyPort: options.readyPort || 8070,
|
|
56
78
|
silent: options.silent ?? false,
|
|
57
79
|
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
import { LocalNodeManager } from "../LocalNodeManager.js";
|
|
7
|
+
import { logger } from "../../ui/index.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* F9 — `apiPort` must not lie about where the node listens.
|
|
11
|
+
*
|
|
12
|
+
* `movement node run-localnet` (Movement CLI 7.4.0) does NOT accept a
|
|
13
|
+
* flag to change the REST API port. It always binds 8080. Earlier
|
|
14
|
+
* versions of LocalNodeManager accepted `apiPort: 9000` from the
|
|
15
|
+
* caller, stored it, and surfaced `http://127.0.0.1:9000` from
|
|
16
|
+
* `getNodeInfo()` — but the actual node was still on 8080. That
|
|
17
|
+
* mismatch would silently surface as "Movement command failed" with
|
|
18
|
+
* no useful signal. F9 closes the gap by refusing to lie: the
|
|
19
|
+
* effective port is 8080 regardless of what the caller passes, with a
|
|
20
|
+
* warning when they pass anything else.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
describe("F9 — LocalNodeManager apiPort is constrained to 8080", () => {
|
|
24
|
+
let tmpDir: string;
|
|
25
|
+
let warnSpy: ReturnType<typeof vi.spyOn>;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
tmpDir = mkdtempSync(join(tmpdir(), "movehat-f9-"));
|
|
29
|
+
warnSpy = vi.spyOn(logger, "warning").mockImplementation(() => undefined);
|
|
30
|
+
vi.spyOn(logger, "step").mockImplementation(() => undefined);
|
|
31
|
+
vi.spyOn(logger, "plain").mockImplementation(() => undefined);
|
|
32
|
+
vi.spyOn(logger, "newline").mockImplementation(() => undefined);
|
|
33
|
+
vi.spyOn(logger, "success").mockImplementation(() => undefined);
|
|
34
|
+
vi.spyOn(logger, "error").mockImplementation(() => undefined);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
vi.restoreAllMocks();
|
|
39
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("ignores non-default apiPort, forces 8080, and warns", () => {
|
|
43
|
+
const mgr = new LocalNodeManager({ testDir: tmpDir, apiPort: 9000 });
|
|
44
|
+
expect(mgr.getNodeInfo().rpcUrl).toBe("http://127.0.0.1:8080");
|
|
45
|
+
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
46
|
+
const msg = warnSpy.mock.calls[0]?.[0] as string;
|
|
47
|
+
expect(msg).toMatch(/8080/);
|
|
48
|
+
expect(msg).toMatch(/apiPort|REST API port/i);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("accepts apiPort: 8080 without warning", () => {
|
|
52
|
+
const mgr = new LocalNodeManager({ testDir: tmpDir, apiPort: 8080 });
|
|
53
|
+
expect(mgr.getNodeInfo().rpcUrl).toBe("http://127.0.0.1:8080");
|
|
54
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("accepts omitted apiPort without warning (default path)", () => {
|
|
58
|
+
const mgr = new LocalNodeManager({ testDir: tmpDir });
|
|
59
|
+
expect(mgr.getNodeInfo().rpcUrl).toBe("http://127.0.0.1:8080");
|
|
60
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
});
|