movehat 0.2.0 → 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.
Files changed (184) hide show
  1. package/README.md +132 -279
  2. package/dist/__tests__/deployContract.test.js +56 -47
  3. package/dist/__tests__/deployContract.test.js.map +1 -1
  4. package/dist/__tests__/exports.test.d.ts +2 -0
  5. package/dist/__tests__/exports.test.d.ts.map +1 -0
  6. package/dist/__tests__/exports.test.js +30 -0
  7. package/dist/__tests__/exports.test.js.map +1 -0
  8. package/dist/__tests__/fixtures/sigint-deploy-harness.d.ts +4 -3
  9. package/dist/__tests__/fixtures/sigint-deploy-harness.d.ts.map +1 -1
  10. package/dist/__tests__/fixtures/sigint-deploy-harness.js +8 -7
  11. package/dist/__tests__/fixtures/sigint-deploy-harness.js.map +1 -1
  12. package/dist/__tests__/fork/api.test.js +7 -2
  13. package/dist/__tests__/fork/api.test.js.map +1 -1
  14. package/dist/__tests__/fork/api.timeout.test.d.ts +2 -0
  15. package/dist/__tests__/fork/api.timeout.test.d.ts.map +1 -0
  16. package/dist/__tests__/fork/api.timeout.test.js +98 -0
  17. package/dist/__tests__/fork/api.timeout.test.js.map +1 -0
  18. package/dist/__tests__/harness/Harness.proxy.test.js +7 -11
  19. package/dist/__tests__/harness/Harness.proxy.test.js.map +1 -1
  20. package/dist/__tests__/harness/codeObject.deploy.test.js +1 -1
  21. package/dist/__tests__/harness/codeObject.deploy.test.js.map +1 -1
  22. package/dist/__tests__/harness/view.test.js +3 -3
  23. package/dist/commands/__tests__/compile.toml-mutation.test.d.ts +2 -0
  24. package/dist/commands/__tests__/compile.toml-mutation.test.d.ts.map +1 -0
  25. package/dist/commands/__tests__/compile.toml-mutation.test.js +69 -0
  26. package/dist/commands/__tests__/compile.toml-mutation.test.js.map +1 -0
  27. package/dist/commands/__tests__/init.test.js +73 -11
  28. package/dist/commands/__tests__/init.test.js.map +1 -1
  29. package/dist/commands/__tests__/run.test.js +3 -3
  30. package/dist/commands/__tests__/run.test.js.map +1 -1
  31. package/dist/commands/init.d.ts +22 -0
  32. package/dist/commands/init.d.ts.map +1 -1
  33. package/dist/commands/init.js +55 -6
  34. package/dist/commands/init.js.map +1 -1
  35. package/dist/core/AccountManager.d.ts +0 -3
  36. package/dist/core/AccountManager.d.ts.map +1 -1
  37. package/dist/core/AccountManager.js +14 -7
  38. package/dist/core/AccountManager.js.map +1 -1
  39. package/dist/core/Publisher.d.ts +0 -5
  40. package/dist/core/Publisher.d.ts.map +1 -1
  41. package/dist/core/Publisher.js +52 -76
  42. package/dist/core/Publisher.js.map +1 -1
  43. package/dist/core/__tests__/AccountManager.global-state.test.d.ts +2 -0
  44. package/dist/core/__tests__/AccountManager.global-state.test.d.ts.map +1 -0
  45. package/dist/core/__tests__/AccountManager.global-state.test.js +69 -0
  46. package/dist/core/__tests__/AccountManager.global-state.test.js.map +1 -0
  47. package/dist/core/__tests__/movementProfile.test.d.ts +2 -0
  48. package/dist/core/__tests__/movementProfile.test.d.ts.map +1 -0
  49. package/dist/core/__tests__/movementProfile.test.js +112 -0
  50. package/dist/core/__tests__/movementProfile.test.js.map +1 -0
  51. package/dist/core/config.js +6 -5
  52. package/dist/core/config.js.map +1 -1
  53. package/dist/core/contract.d.ts +0 -3
  54. package/dist/core/contract.d.ts.map +1 -1
  55. package/dist/core/contract.js +0 -3
  56. package/dist/core/contract.js.map +1 -1
  57. package/dist/core/deployments.d.ts +0 -6
  58. package/dist/core/deployments.d.ts.map +1 -1
  59. package/dist/core/deployments.js +0 -12
  60. package/dist/core/deployments.js.map +1 -1
  61. package/dist/core/movementProfile.d.ts +55 -22
  62. package/dist/core/movementProfile.d.ts.map +1 -1
  63. package/dist/core/movementProfile.js +77 -99
  64. package/dist/core/movementProfile.js.map +1 -1
  65. package/dist/fork/__tests__/manager.test.js +1 -1
  66. package/dist/fork/__tests__/server.cors.test.d.ts +2 -0
  67. package/dist/fork/__tests__/server.cors.test.d.ts.map +1 -0
  68. package/dist/fork/__tests__/server.cors.test.js +79 -0
  69. package/dist/fork/__tests__/server.cors.test.js.map +1 -0
  70. package/dist/fork/api.d.ts +9 -1
  71. package/dist/fork/api.d.ts.map +1 -1
  72. package/dist/fork/api.js +37 -7
  73. package/dist/fork/api.js.map +1 -1
  74. package/dist/fork/manager.d.ts +1 -21
  75. package/dist/fork/manager.d.ts.map +1 -1
  76. package/dist/fork/manager.js +1 -41
  77. package/dist/fork/manager.js.map +1 -1
  78. package/dist/fork/server.d.ts +20 -1
  79. package/dist/fork/server.d.ts.map +1 -1
  80. package/dist/fork/server.js +19 -9
  81. package/dist/fork/server.js.map +1 -1
  82. package/dist/fork/test.d.ts +0 -1
  83. package/dist/fork/test.d.ts.map +1 -1
  84. package/dist/fork/test.js.map +1 -1
  85. package/dist/harness/Harness.d.ts +11 -13
  86. package/dist/harness/Harness.d.ts.map +1 -1
  87. package/dist/harness/Harness.js +13 -13
  88. package/dist/harness/Harness.js.map +1 -1
  89. package/dist/harness/codeObject.d.ts.map +1 -1
  90. package/dist/harness/codeObject.js +31 -38
  91. package/dist/harness/codeObject.js.map +1 -1
  92. package/dist/harness/script.d.ts +3 -3
  93. package/dist/harness/script.d.ts.map +1 -1
  94. package/dist/harness/script.js +33 -29
  95. package/dist/harness/script.js.map +1 -1
  96. package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.d.ts +2 -0
  97. package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.d.ts.map +1 -0
  98. package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.js +172 -0
  99. package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.js.map +1 -0
  100. package/dist/helpers/setupLocalTesting.d.ts +1 -2
  101. package/dist/helpers/setupLocalTesting.d.ts.map +1 -1
  102. package/dist/helpers/setupLocalTesting.js +28 -2
  103. package/dist/helpers/setupLocalTesting.js.map +1 -1
  104. package/dist/index.d.ts +1 -0
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js +0 -1
  107. package/dist/index.js.map +1 -1
  108. package/dist/node/LocalNodeManager.d.ts +8 -0
  109. package/dist/node/LocalNodeManager.d.ts.map +1 -1
  110. package/dist/node/LocalNodeManager.js +10 -1
  111. package/dist/node/LocalNodeManager.js.map +1 -1
  112. package/dist/node/__tests__/LocalNodeManager.api-port.test.d.ts +2 -0
  113. package/dist/node/__tests__/LocalNodeManager.api-port.test.d.ts.map +1 -0
  114. package/dist/node/__tests__/LocalNodeManager.api-port.test.js +55 -0
  115. package/dist/node/__tests__/LocalNodeManager.api-port.test.js.map +1 -0
  116. package/dist/node/__tests__/LocalNodeManager.test.js +4 -3
  117. package/dist/node/__tests__/LocalNodeManager.test.js.map +1 -1
  118. package/dist/runtime.d.ts.map +1 -1
  119. package/dist/runtime.js +1 -3
  120. package/dist/runtime.js.map +1 -1
  121. package/dist/templates/move/Move.toml +1 -1
  122. package/dist/templates/move/sources/Counter.move +31 -4
  123. package/dist/templates/scripts/deploy-counter.ts +11 -1
  124. package/dist/templates/tests/Counter.test.ts +2 -2
  125. package/dist/types/config.d.ts +8 -1
  126. package/dist/types/config.d.ts.map +1 -1
  127. package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.d.ts +2 -0
  128. package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.d.ts.map +1 -0
  129. package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.js +43 -0
  130. package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.js.map +1 -0
  131. package/dist/utils/address.d.ts +0 -4
  132. package/dist/utils/address.d.ts.map +1 -1
  133. package/dist/utils/address.js +0 -4
  134. package/dist/utils/address.js.map +1 -1
  135. package/dist/utils/childProcessAdapter.d.ts +7 -0
  136. package/dist/utils/childProcessAdapter.d.ts.map +1 -1
  137. package/dist/utils/childProcessAdapter.js +23 -6
  138. package/dist/utils/childProcessAdapter.js.map +1 -1
  139. package/package.json +2 -1
  140. package/src/__tests__/deployContract.test.ts +59 -50
  141. package/src/__tests__/exports.test.ts +32 -0
  142. package/src/__tests__/fixtures/sigint-deploy-harness.ts +8 -7
  143. package/src/__tests__/fork/api.test.ts +7 -2
  144. package/src/__tests__/fork/api.timeout.test.ts +150 -0
  145. package/src/__tests__/harness/Harness.proxy.test.ts +7 -11
  146. package/src/__tests__/harness/codeObject.deploy.test.ts +1 -1
  147. package/src/__tests__/harness/view.test.ts +3 -3
  148. package/src/commands/__tests__/compile.toml-mutation.test.ts +77 -0
  149. package/src/commands/__tests__/init.test.ts +96 -11
  150. package/src/commands/__tests__/run.test.ts +3 -3
  151. package/src/commands/init.ts +77 -6
  152. package/src/core/AccountManager.ts +18 -13
  153. package/src/core/Publisher.ts +58 -85
  154. package/src/core/__tests__/AccountManager.global-state.test.ts +83 -0
  155. package/src/core/__tests__/movementProfile.test.ts +131 -0
  156. package/src/core/config.ts +9 -5
  157. package/src/core/contract.ts +0 -3
  158. package/src/core/deployments.ts +0 -12
  159. package/src/core/movementProfile.ts +75 -127
  160. package/src/fork/__tests__/manager.test.ts +1 -1
  161. package/src/fork/__tests__/server.cors.test.ts +101 -0
  162. package/src/fork/api.ts +69 -10
  163. package/src/fork/manager.ts +1 -41
  164. package/src/fork/server.ts +38 -9
  165. package/src/fork/test.ts +0 -1
  166. package/src/harness/Harness.ts +16 -13
  167. package/src/harness/codeObject.ts +38 -48
  168. package/src/harness/script.ts +40 -39
  169. package/src/helpers/__tests__/setupLocalTesting.fork-network.test.ts +212 -0
  170. package/src/helpers/setupLocalTesting.ts +37 -4
  171. package/src/index.ts +9 -2
  172. package/src/node/LocalNodeManager.ts +24 -2
  173. package/src/node/__tests__/LocalNodeManager.api-port.test.ts +62 -0
  174. package/src/node/__tests__/LocalNodeManager.test.ts +5 -4
  175. package/src/runtime.ts +1 -3
  176. package/src/templates/move/Move.toml +1 -1
  177. package/src/templates/move/sources/Counter.move +31 -4
  178. package/src/templates/scripts/deploy-counter.ts +11 -1
  179. package/src/templates/tests/Counter.test.ts +2 -2
  180. package/src/types/config.ts +8 -1
  181. package/src/types/runtime.ts +2 -2
  182. package/src/utils/__tests__/childProcessAdapter.maxBuffer.test.ts +51 -0
  183. package/src/utils/address.ts +0 -4
  184. package/src/utils/childProcessAdapter.ts +35 -6
@@ -2,6 +2,19 @@ import http from 'http';
2
2
  import { URL } from 'url';
3
3
  import { ForkManager } from './manager.js';
4
4
 
5
+ export interface ForkServerOptions {
6
+ /**
7
+ * Origins allowed to make cross-origin requests. When unset (default),
8
+ * no `Access-Control-Allow-Origin` header is emitted — any browser
9
+ * cross-origin read is rejected by the user agent. Setting this to a
10
+ * non-empty list opts into echoing matching `Origin` request headers
11
+ * back. Wildcard `'*'` is intentionally NOT supported: cached fork
12
+ * state may include resources that should not be readable by every
13
+ * page in the dev's browser.
14
+ */
15
+ corsAllowOrigins?: readonly string[];
16
+ }
17
+
5
18
  /**
6
19
  * Fork Server - Serves fork data via Movement L1 RPC API
7
20
  * Emulates a Movement L1 node using local fork storage
@@ -11,16 +24,38 @@ export class ForkServer {
11
24
  private forkManager: ForkManager;
12
25
  private port: number;
13
26
  private host: string;
27
+ private readonly corsAllowOrigins: ReadonlySet<string>;
14
28
 
15
29
  /**
16
30
  * @param host Interface to bind. Defaults to `127.0.0.1` so cached fork
17
31
  * state (which may include sensitive resources) is not exposed on the LAN.
18
32
  * Pass `'0.0.0.0'` only if you intentionally need to expose the server.
33
+ * @param options Optional CORS allowlist (see {@link ForkServerOptions}).
19
34
  */
20
- constructor(forkPath: string, port: number = 8080, host: string = '127.0.0.1') {
35
+ constructor(
36
+ forkPath: string,
37
+ port: number = 8080,
38
+ host: string = '127.0.0.1',
39
+ options: ForkServerOptions = {}
40
+ ) {
21
41
  this.forkManager = new ForkManager(forkPath);
22
42
  this.port = port;
23
43
  this.host = host;
44
+ this.corsAllowOrigins = new Set(options.corsAllowOrigins ?? []);
45
+ }
46
+
47
+ /**
48
+ * Set CORS headers for a request when the request's `Origin` is in
49
+ * the allowlist. No-op otherwise.
50
+ */
51
+ private applyCors(req: http.IncomingMessage, res: http.ServerResponse): void {
52
+ const origin = req.headers.origin;
53
+ if (typeof origin === 'string' && this.corsAllowOrigins.has(origin)) {
54
+ res.setHeader('Access-Control-Allow-Origin', origin);
55
+ res.setHeader('Vary', 'Origin');
56
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
57
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
58
+ }
24
59
  }
25
60
 
26
61
  /**
@@ -44,10 +79,7 @@ export class ForkServer {
44
79
 
45
80
  // Only send response if headers haven't been sent yet
46
81
  if (!res.headersSent) {
47
- // Add CORS headers (same as in handleRequest)
48
- res.setHeader('Access-Control-Allow-Origin', '*');
49
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
50
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
82
+ this.applyCors(req, res);
51
83
 
52
84
  // Send generic error response (no internal details exposed)
53
85
  this.sendJSON(res, 500, {
@@ -143,10 +175,7 @@ export class ForkServer {
143
175
  // Log request
144
176
  console.log(`[${new Date().toISOString()}] ${req.method} ${pathname}`);
145
177
 
146
- // CORS headers
147
- res.setHeader('Access-Control-Allow-Origin', '*');
148
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
149
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
178
+ this.applyCors(req, res);
150
179
 
151
180
  // Handle OPTIONS for CORS preflight
152
181
  if (req.method === 'OPTIONS') {
package/src/fork/test.ts CHANGED
@@ -9,7 +9,6 @@ export interface SnapshotOptions {
9
9
  /**
10
10
  * Override the child-process adapter. Test-only — production callers
11
11
  * leave this undefined so the default spawn-based adapter is used.
12
- * Mirrors the M1.3c pattern on `runtime.deployContract`.
13
12
  */
14
13
  adapter?: ChildProcessAdapter;
15
14
  }
@@ -36,16 +36,10 @@ interface HarnessInit {
36
36
  * a Proxy that synchronously throws {@link HarnessDisposedError} on any
37
37
  * post-`cleanup()` call to one of the deployment / script / view methods.
38
38
  *
39
- * Methods `deployCodeObject`, `upgradeCodeObject`, `runViewFunction`,
40
- * and `runMoveScript` are stubbed in M2.1 they throw at runtime until
41
- * M2.2/M2.3 lands. The type surface is complete so callers and docs can
42
- * be written against it ahead of time.
43
- *
44
- * AccountManager note: as of M2.1 the underlying account pool is a
45
- * process-wide static (see `core/AccountManager.ts`). Two Harness
46
- * instances in the same process share account labels; this is the same
47
- * constraint that already governs `setupTestFixture`. A per-Harness pool
48
- * is a future change.
39
+ * AccountManager note: the underlying account pool is a process-wide
40
+ * static (see `core/AccountManager.ts`). Two Harness instances in the
41
+ * same process share account labels; this is the same constraint that
42
+ * already governs `setupTestFixture`.
49
43
  */
50
44
  export class Harness {
51
45
  public readonly mode: HarnessMode;
@@ -98,19 +92,28 @@ export class Harness {
98
92
  * and `runMoveScript` throw with a message pointing at `createLocal`.
99
93
  * `runViewFunction` works (read-only path).
100
94
  *
101
- * @param network - Network to fork (e.g. `"testnet"`).
95
+ * @param network - Network to fork. Built-ins: `"testnet"`, `"mainnet"`.
96
+ * Any other name requires `rpcUrl`.
102
97
  * @param apiKey - Optional Movement API key. When set, every upstream
103
98
  * request from the fork's `MovementApiClient` carries
104
99
  * `Authorization: Bearer <apiKey>`. Use for rate-limited public
105
100
  * endpoints or auth-gated nodes. The key stays in process memory
106
101
  * (not persisted to the fork's on-disk metadata).
102
+ * @param rpcUrl - Required when forking a non-built-in network.
103
+ * Ignored when a fork already exists on disk (the saved metadata's
104
+ * nodeUrl is reused).
107
105
  */
108
- static async createFork(network: string, apiKey?: string): Promise<Harness> {
106
+ static async createFork(
107
+ network: string,
108
+ apiKey?: string,
109
+ rpcUrl?: string
110
+ ): Promise<Harness> {
109
111
  const setupOpts: import("../types/config.js").LocalTestOptions = {
110
112
  mode: "fork",
111
113
  forkNetwork: network,
112
114
  };
113
115
  if (apiKey !== undefined) setupOpts.forkApiKey = apiKey;
116
+ if (rpcUrl !== undefined) setupOpts.forkRpcUrl = rpcUrl;
114
117
  const ctx = await setupLocalTesting(setupOpts);
115
118
  const init: HarnessInit = {
116
119
  mode: "fork",
@@ -128,7 +131,7 @@ export class Harness {
128
131
  * spawned; transactions are submitted to the configured RPC.
129
132
  *
130
133
  * @param network - Named network from movehat.config.ts.
131
- * @param _faucetUrl - Reserved for M2.2 (auto-fund on networks with a faucet).
134
+ * @param _faucetUrl - Reserved for future auto-fund support on networks with a faucet.
132
135
  */
133
136
  static async createLive(network: string, _faucetUrl?: string): Promise<Harness> {
134
137
  const runtime = await initRuntime({ network });
@@ -1,6 +1,4 @@
1
- import { homedir } from "os";
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, validateProfileSafety } from "../core/shell.js";
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
- withYamlLock,
28
- addProfile,
29
- removeProfile,
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
- // Strip `ed25519-priv-` prefix if present Movement CLI expects the
217
- // raw hex.
218
- let cleanPrivateKey = config.privateKey;
219
- if (cleanPrivateKey.startsWith("ed25519-priv-")) {
220
- cleanPrivateKey = cleanPrivateKey.replace("ed25519-priv-", "");
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
- const movementConfigPath = join(homedir(), ".aptos", "config.yaml");
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 writing the key (same
226
- // pattern as Publisher closes bug #36).
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 = () => removeProfileSync(movementConfigPath, profile);
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
- "--profile",
262
- safeProfile,
254
+ "--private-key-file",
255
+ keyFilePath,
256
+ "--sender-account",
257
+ deployerAddress,
263
258
  "--assume-yes",
264
259
  ...includedArtifacts,
265
260
  ...namedAddrArgs,
@@ -273,24 +268,21 @@ 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
- // Best-effort profile removal. CRITICAL: catch + log instead of
277
- // re-throwing an await-in-finally that throws would clobber the
278
- // try block's success/error (the bug-#37 lesson from Publisher).
279
- await withYamlLock(() => removeProfile(movementConfigPath, profile)).catch(
280
- (err) => {
281
- const cleanupMsg = err instanceof Error ? err.message : String(err);
282
- logger.warning(
283
- `Failed to remove deploy profile "${profile}" from ${movementConfigPath}: ${cleanupMsg}. ` +
284
- `Run 'movement config delete-profile --profile ${profile}' to clean up manually.`
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
 
291
285
  // Parse object address (for deploy-object) and txHash (both flows).
292
- // No captured fixture exists at M2.2 commit time; M4 integration
293
- // tests validate against real CLI output.
294
286
  const objectAddress = opts.fixedAddress ?? parseObjectAddress(deployOut);
295
287
  const txHash = parseTxHash(deployOut);
296
288
 
@@ -361,9 +353,7 @@ async function executeMovementMoveObject(
361
353
  /**
362
354
  * Extract a code-object address from `movement move deploy-object` stdout.
363
355
  *
364
- * Movement CLI typically emits the address in one of these shapes (none
365
- * of them captured at M2.2 commit time — patterns are speculative, with
366
- * M4 integration tests as the validation gate):
356
+ * Movement CLI typically emits the address in one of these shapes:
367
357
  *
368
358
  * - Free text: `Code was successfully deployed to object address 0x…`
369
359
  * - Free text: `Object address: 0x…`
@@ -1,22 +1,20 @@
1
1
  import { existsSync } from "fs";
2
- import { homedir } from "os";
3
- import { extname, join } from "path";
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, validateProfileSafety } from "../core/shell.js";
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
- withYamlLock,
17
- addProfile,
18
- removeProfile,
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-deploy unique profile, atomic 0o600 yaml writes under
33
- * the mutex, SIGINT-safe sync cleanup, `--profile` auth (key never
34
- * appears in `ps` output).
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
- let cleanPrivateKey = config.privateKey;
84
- if (cleanPrivateKey.startsWith("ed25519-priv-")) {
85
- cleanPrivateKey = cleanPrivateKey.replace("ed25519-priv-", "");
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
- const movementConfigPath = join(homedir(), ".aptos", "config.yaml");
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 = () => removeProfileSync(movementConfigPath, profile);
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
- "--profile",
121
- safeProfile,
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
- await withYamlLock(() => removeProfile(movementConfigPath, profile)).catch(
139
- (err) => {
140
- const cleanupMsg = err instanceof Error ? err.message : String(err);
141
- logger.warning(
142
- `Failed to remove script profile "${profile}" from ${movementConfigPath}: ${cleanupMsg}. ` +
143
- `Run 'movement config delete-profile --profile ${profile}' to clean up manually.`
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
+ });