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
@@ -1,7 +1,4 @@
1
- import { homedir } from "os";
2
- import { join } from "path";
3
- import { randomUUID } from "crypto";
4
- import { Account } from "@aptos-labs/ts-sdk";
1
+ import { Account, PrivateKey, PrivateKeyVariants } from "@aptos-labs/ts-sdk";
5
2
  import { MovehatConfig } from "../types/config.js";
6
3
  import { extractNamedAddresses } from "../commands/compile.js";
7
4
  import {
@@ -10,16 +7,15 @@ import {
10
7
  DeploymentInfo,
11
8
  validateSafeName,
12
9
  } from "./deployments.js";
13
- import { validatePathSafety, validateProfileSafety } from "./shell.js";
10
+ import { validatePathSafety } from "./shell.js";
14
11
  import { CliExecutionError, ModuleAlreadyDeployedError, PostPublishError } from "../errors.js";
15
12
  import { runCli } from "../utils/runCli.js";
16
13
  import { logger } from "../ui/index.js";
17
14
  import type { ChildProcessAdapter } from "../utils/childProcessAdapter.js";
18
15
  import {
19
- withYamlLock,
20
- addProfile,
21
- removeProfile,
22
- removeProfileSync,
16
+ writeTempKeyFile,
17
+ removeKeyFile,
18
+ removeKeyFileSyncBestEffort,
23
19
  ensureSignalHandler,
24
20
  cleanupCallbacks,
25
21
  } from "./movementProfile.js";
@@ -41,11 +37,6 @@ export interface PublishInput {
41
37
  /**
42
38
  * Publishes a Move module via the Movement CLI.
43
39
  *
44
- * Extracted from `runtime.deployContract` (M1.4 / #79). Carries the
45
- * destructive Move.toml-rewrite + shared-yaml-write semantics of the
46
- * original closure verbatim in this scaffold commit — bug fixes for
47
- * #36 / #37 / #38 land in subsequent commits.
48
- *
49
40
  * @internal
50
41
  */
51
42
  export class Publisher {
@@ -54,13 +45,10 @@ export class Publisher {
54
45
  async deploy(input: PublishInput): Promise<DeploymentInfo> {
55
46
  const { moduleName, config, account } = input;
56
47
 
57
- // Validate moduleName early
58
48
  validateSafeName(moduleName, "module");
59
49
 
60
- // Check if --redeploy flag was passed via CLI
61
50
  const forceRedeploy = process.env.MH_CLI_REDEPLOY === "true";
62
51
 
63
- // Check if already deployed
64
52
  const existingDeployment = loadDeployment(config.network, moduleName);
65
53
  if (existingDeployment && !forceRedeploy) {
66
54
  // Build detailed error message with all deployment info
@@ -106,18 +94,10 @@ export class Publisher {
106
94
 
107
95
  const dir = input.packageDir || config.moveDir;
108
96
 
109
- // Bug #37: use a UUID-suffixed profile name per deploy so concurrent
110
- // Publisher.deploy() calls in the same process don't fight over the
111
- // same key in ~/.aptos/config.yaml. The previous code reused
112
- // config.profile (default "default"), which meant two parallel
113
- // deploys would clobber each other's profile data mid-publish.
114
- const profile = `movehat-deploy-${randomUUID().slice(0, 8)}`;
115
-
116
97
  // Validate (no shell escape — runCli uses spawn, which takes args
117
98
  // verbatim and would treat the single-quote wrapping as part of the
118
- // literal path/profile, breaking Movement CLI argument parsing).
99
+ // literal path, breaking Movement CLI argument parsing).
119
100
  const safeDir = validatePathSafety(dir, "package directory");
120
- const safeProfile = validateProfileSafety(profile);
121
101
 
122
102
  logger.step(`Publishing module "${moduleName}" from ${dir}...`);
123
103
 
@@ -156,54 +136,49 @@ export class Publisher {
156
136
  // Publish using direct parameters (avoid config file issues)
157
137
  logger.step("Publishing to blockchain...");
158
138
 
159
- // Use parameters directly instead of relying on config file
160
- // Strip any ed25519-priv- prefix if present
161
- let cleanPrivateKey = config.privateKey;
162
- if (cleanPrivateKey.startsWith("ed25519-priv-")) {
163
- cleanPrivateKey = cleanPrivateKey.replace("ed25519-priv-", "");
164
- }
139
+ // Format the private key into AIP-80 shape so the Movement CLI
140
+ // doesn't emit its raw-hex deprecation warning. `formatPrivateKey`
141
+ // is idempotent for already-prefixed inputs.
142
+ const formattedPrivateKey = PrivateKey.formatPrivateKey(
143
+ config.privateKey,
144
+ PrivateKeyVariants.Ed25519,
145
+ );
165
146
 
166
- // Bug #38: Move.toml is NOT mutated. All address overrides flow
167
- // through the `--named-addresses` flag above, which Movement CLI
168
- // applies during build + publish. The previous regex rewrite +
169
- // restore-in-finally was destructive: if the process died between
170
- // write and restore, the user's Move.toml stayed mutated.
147
+ // Move.toml is NOT mutated. All address overrides flow through
148
+ // the `--named-addresses` flag above, which Movement CLI applies
149
+ // during build + publish. Rewriting Move.toml on disk would risk
150
+ // leaving the user's file mutated if the process died before the
151
+ // restore step.
171
152
 
172
153
  let publishOut = "";
173
154
  let publishErr = "";
174
155
 
175
- // Setup Movement CLI config with private key securely.
176
- // Movement CLI uses .aptos config directory (not .movement).
177
- const movementConfigPath = join(homedir(), ".aptos", "config.yaml");
178
-
179
- // Register a sync cleanup hook BEFORE writing the private key.
180
- // If the user Ctrl+C's (or the process is SIGTERM'd) between the
181
- // yaml write and our async finally, the SIGINT handler iterates
182
- // every registered callback and removes this deploy's profile
183
- // synchronously closes bug #36 (private key persisting on disk
184
- // after abnormal exit).
156
+ // Pass the private key to Movement CLI via a 0o600 temp file
157
+ // (`--private-key-file <path>`) and the on-chain address via
158
+ // `--sender-account <addr>`. This avoids the CLI's profile-yaml
159
+ // lookup chain entirely — no CWD / HOME / .aptos / .movement
160
+ // dance, no CLI-variant dependency.
161
+ const keyFilePath = writeTempKeyFile(formattedPrivateKey);
162
+
163
+ // Register a sync cleanup hook BEFORE invoking the CLI. If the
164
+ // user Ctrl+C's (or the process is SIGTERM'd) between the file
165
+ // write and our finally, the SIGINT handler iterates every
166
+ // registered callback and unlinks this deploy's key file
167
+ // synchronously so the private key never persists on disk after
168
+ // an abnormal exit. The signal-handler path uses the
169
+ // best-effort variant because the event loop is dead and we
170
+ // cannot logger.warning.
185
171
  ensureSignalHandler();
186
- const syncCleanup = () => removeProfileSync(movementConfigPath, profile);
172
+ const syncCleanup = () => removeKeyFileSyncBestEffort(keyFilePath);
187
173
  cleanupCallbacks.add(syncCleanup);
188
174
 
189
- // Add our deploy profile under the unique key. The mutex serializes
190
- // read-modify-write cycles so concurrent deploys in the same process
191
- // can't drop each other's profiles. Other user profiles in the same
192
- // file are preserved untouched.
193
- await withYamlLock(() =>
194
- addProfile(movementConfigPath, profile, {
195
- private_key: cleanPrivateKey,
196
- public_key: account.publicKey.toString(),
197
- account: deployerAddress,
198
- rest_url: config.rpc,
199
- })
200
- );
201
-
202
175
  try {
203
- // Execute publish command without exposing private key in CLI.
204
- // Routed through runCli so stdout/stderr are redacted of any
205
- // `ed25519-priv-…` shape before reaching console.log/console.error
206
- // or the thrown CliExecutionError that's bug #43.
176
+ // Execute publish command. Private key reaches the CLI via the
177
+ // temp key file path (--private-key-file) never on the
178
+ // command line so it can't leak through `ps aux`. runCli's
179
+ // stdout/stderr redaction still applies as defense in depth
180
+ // for any `ed25519-priv-…` substring that surfaces in CLI
181
+ // output (Movement CLI sometimes echoes the key on error).
207
182
  const publishResult = await runCli(
208
183
  {
209
184
  command: "movement",
@@ -214,8 +189,10 @@ export class Publisher {
214
189
  safeDir,
215
190
  "--url",
216
191
  config.rpc,
217
- "--profile",
218
- safeProfile,
192
+ "--private-key-file",
193
+ keyFilePath,
194
+ "--sender-account",
195
+ deployerAddress,
219
196
  "--assume-yes",
220
197
  ...namedAddrArgs,
221
198
  ],
@@ -228,26 +205,22 @@ export class Publisher {
228
205
  if (publishOut) console.log(publishOut.trim());
229
206
  if (publishErr) console.error(publishErr.trim());
230
207
  } finally {
231
- // Always remove our profile from the shared yaml — never restore
232
- // a "snapshot" of the whole file (that's what the old code did,
233
- // and that's the bug #37 race). Removing only our key leaves
234
- // other concurrent deploys' profiles intact.
235
- //
236
- // CRITICAL: catch + log instead of throwing. `await` in a finally
237
- // block that throws will clobber both the try block's successful
238
- // return value AND any error already propagating. Without this
239
- // catch, a yaml-write failure here would mask a successful
240
- // publish (making the deploy look failed and inviting a redeploy)
241
- // or mask the real publish error.
242
- await withYamlLock(() => removeProfile(movementConfigPath, profile)).catch((err) => {
243
- const cleanupMsg = err instanceof Error ? err.message : String(err);
208
+ // Unlink the temp key file via the observable cleanup helper.
209
+ // ENOENT and other already-gone outcomes are benign (null).
210
+ // A non-null Error means the unlink failed AND the file still
211
+ // exists on disk the private key would persist silently
212
+ // otherwise, so we emit a warning with the manual-cleanup
213
+ // hint. The SIGINT signal handler's sync callback below also
214
+ // tries to remove the same file; if SIGINT fires before this
215
+ // finally runs the file is gone and the next finally call
216
+ // sees ENOENT (benign).
217
+ const cleanupErr = removeKeyFile(keyFilePath);
218
+ if (cleanupErr) {
244
219
  logger.warning(
245
- `Failed to remove deploy profile "${profile}" from ${movementConfigPath}: ${cleanupMsg}. ` +
246
- `Run 'movement config delete-profile --profile ${profile}' to clean up manually.`
220
+ `Failed to remove temp key file '${keyFilePath}': ${cleanupErr.message}. ` +
221
+ `The file has mode 0o600 but should be removed manually: rm ${keyFilePath}`
247
222
  );
248
- });
249
- // Unregister the sync cleanup hook — normal path. (The signal
250
- // handler stays installed for the process lifetime; cheap.)
223
+ }
251
224
  cleanupCallbacks.delete(syncCleanup);
252
225
  }
253
226
 
@@ -0,0 +1,83 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
+ import { existsSync, mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ import { AccountManager } from "../AccountManager.js";
7
+
8
+ /**
9
+ * F8 — Document the two known limitations of `AccountManager`:
10
+ *
11
+ * (a) State is class-static. Two harness "sessions" in the same
12
+ * process share the pool, the labelMap, and the private-key
13
+ * map. A label re-used across sessions overwrites the entry.
14
+ * This is intentional per `Harness.ts:39-42` ("Two Harness
15
+ * instances in the same process share account labels"). This
16
+ * test captures the contract so a future refactor cannot
17
+ * silently change it.
18
+ *
19
+ * (b) `defaultPoolPath = join(process.cwd(), ".movehat", "accounts")`
20
+ * is evaluated when the module is first imported, NOT lazily on
21
+ * each call. Changing `process.cwd()` after import does not move
22
+ * the save destination. Callers needing per-test isolation must
23
+ * pass an explicit `poolPath` argument.
24
+ */
25
+
26
+ describe("F8 — AccountManager static state and import-time cwd capture", () => {
27
+ let cwdBackup: string;
28
+ let tmpDir: string;
29
+
30
+ beforeEach(() => {
31
+ AccountManager.clearPool();
32
+ cwdBackup = process.cwd();
33
+ tmpDir = mkdtempSync(join(tmpdir(), "movehat-f8-"));
34
+ });
35
+
36
+ afterEach(() => {
37
+ AccountManager.clearPool();
38
+ if (process.cwd() !== cwdBackup) {
39
+ process.chdir(cwdBackup);
40
+ }
41
+ rmSync(tmpDir, { recursive: true, force: true });
42
+ });
43
+
44
+ it("(a) re-creating an account with an existing label overwrites the labelMap entry", () => {
45
+ const first = AccountManager.createAccount("alice");
46
+ const second = AccountManager.createAccount("alice");
47
+ expect(second.accountAddress.toString()).not.toBe(
48
+ first.accountAddress.toString()
49
+ );
50
+
51
+ const lookup = AccountManager.getAccountByLabel("alice");
52
+ expect(lookup).toBeDefined();
53
+ expect(lookup!.accountAddress.toString()).toBe(
54
+ second.accountAddress.toString()
55
+ );
56
+ // The first account is still in the pool (keyed by address), only
57
+ // the label binding moved. A second harness session that creates
58
+ // its own "alice" will silently shadow the first — this is the
59
+ // documented Harness limitation. exportPrivateKeys reflects the
60
+ // current label binding (i.e. the second account).
61
+ const exportedKeys = AccountManager.exportPrivateKeys(["alice"]);
62
+ expect(exportedKeys.alice).toBeTypeOf("string");
63
+ expect(exportedKeys.alice!.length).toBeGreaterThan(0);
64
+ });
65
+
66
+ it("(b) saveAccountPool ignores a process.chdir after import; defaults to the import-time cwd", () => {
67
+ AccountManager.createAccount("bob");
68
+
69
+ process.chdir(tmpDir);
70
+ // No path argument → uses defaultPoolPath, which was set at module
71
+ // load time before this chdir.
72
+ AccountManager.saveAccountPool();
73
+
74
+ // The pool file must NOT appear under the freshly-chdir'd cwd.
75
+ const expectedAtNewCwd = join(tmpDir, ".movehat", "accounts", "test-pool.json");
76
+ expect(existsSync(expectedAtNewCwd)).toBe(false);
77
+
78
+ // Sanity check: explicit poolPath does land where the caller asked.
79
+ const explicit = join(tmpDir, "explicit");
80
+ AccountManager.saveAccountPool(explicit);
81
+ expect(existsSync(join(explicit, "test-pool.json"))).toBe(true);
82
+ });
83
+ });
@@ -0,0 +1,131 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import {
3
+ chmodSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ mkdtempSync,
7
+ rmSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import {
13
+ removeKeyFile,
14
+ removeKeyFileSyncBestEffort,
15
+ writeTempKeyFile,
16
+ } from "../movementProfile.js";
17
+
18
+ /**
19
+ * Unit tests for the two distinct cleanup helpers in movementProfile:
20
+ *
21
+ * - `removeKeyFileSyncBestEffort`: for SIGINT/SIGTERM handlers where
22
+ * the event loop is dead. Never throws, never logs, always returns
23
+ * void. We only test that it doesn't throw.
24
+ *
25
+ * - `removeKeyFile`: for normal `finally` cleanup paths. Returns
26
+ * `null` when the file is gone (removed OR already absent — both
27
+ * are benign), and returns an `Error` only when the file STILL
28
+ * exists on disk after the unlink attempt failed. The Error path
29
+ * is exactly the case the caller must surface as a warning,
30
+ * because a private-key temp file would otherwise persist
31
+ * silently.
32
+ */
33
+
34
+ describe("movementProfile cleanup helpers", () => {
35
+ let scratchDir: string;
36
+
37
+ beforeEach(() => {
38
+ scratchDir = mkdtempSync(join(tmpdir(), "movehat-profile-test-"));
39
+ });
40
+
41
+ afterEach(() => {
42
+ if (existsSync(scratchDir)) {
43
+ // Force chmod in case a test left it un-removable, then rmSync.
44
+ try {
45
+ chmodSync(scratchDir, 0o700);
46
+ } catch {
47
+ /* best-effort */
48
+ }
49
+ rmSync(scratchDir, { recursive: true, force: true });
50
+ }
51
+ });
52
+
53
+ describe("writeTempKeyFile", () => {
54
+ it("creates a 0o600 file in os.tmpdir() with the key as contents", () => {
55
+ const path = writeTempKeyFile("ed25519-priv-0x" + "a".repeat(64));
56
+ try {
57
+ expect(path.startsWith(tmpdir())).toBe(true);
58
+ expect(path).toMatch(/movehat-key-/);
59
+ expect(existsSync(path)).toBe(true);
60
+ } finally {
61
+ if (existsSync(path)) rmSync(path);
62
+ }
63
+ });
64
+ });
65
+
66
+ describe("removeKeyFileSyncBestEffort", () => {
67
+ it("removes an existing file", () => {
68
+ const path = join(scratchDir, "key");
69
+ writeFileSync(path, "x", { mode: 0o600 });
70
+ removeKeyFileSyncBestEffort(path);
71
+ expect(existsSync(path)).toBe(false);
72
+ });
73
+
74
+ it("does not throw when the file is already gone", () => {
75
+ const path = join(scratchDir, "never-existed");
76
+ expect(() => removeKeyFileSyncBestEffort(path)).not.toThrow();
77
+ });
78
+
79
+ it("does not throw when the path is a non-empty directory (would fail in stricter callers)", () => {
80
+ const dirPath = join(scratchDir, "i-am-a-directory");
81
+ mkdirSync(dirPath);
82
+ writeFileSync(join(dirPath, "child"), "x");
83
+ expect(() => removeKeyFileSyncBestEffort(dirPath)).not.toThrow();
84
+ // The dir still exists — best-effort doesn't fight EISDIR.
85
+ expect(existsSync(dirPath)).toBe(true);
86
+ });
87
+ });
88
+
89
+ describe("removeKeyFile", () => {
90
+ it("returns null when the file is removed cleanly", () => {
91
+ const path = join(scratchDir, "key-to-remove");
92
+ writeFileSync(path, "x", { mode: 0o600 });
93
+ const err = removeKeyFile(path);
94
+ expect(err).toBeNull();
95
+ expect(existsSync(path)).toBe(false);
96
+ });
97
+
98
+ it("returns null when the file was already gone (ENOENT is benign)", () => {
99
+ const path = join(scratchDir, "never-existed");
100
+ const err = removeKeyFile(path);
101
+ expect(err).toBeNull();
102
+ });
103
+
104
+ it("returns null when the file disappears between the unlink attempt and the existsSync check (race)", () => {
105
+ // Hard to provoke a real race deterministically. The contract is
106
+ // documented; the previous test covers the ENOENT short-circuit
107
+ // which is the common race outcome.
108
+ const path = join(scratchDir, "raced");
109
+ const err = removeKeyFile(path);
110
+ expect(err).toBeNull();
111
+ });
112
+
113
+ it("returns an Error when the path is a directory AND still exists post-attempt", () => {
114
+ // unlinkSync on a directory throws EISDIR (or EPERM on some
115
+ // platforms). existsSync afterwards still returns true, so this
116
+ // is the "preocupante" path the caller must surface as a warning
117
+ // — the file (here, a directory occupying the key-file path) is
118
+ // still on disk.
119
+ const dirPath = join(scratchDir, "key-but-actually-a-dir");
120
+ mkdirSync(dirPath);
121
+ writeFileSync(join(dirPath, "child"), "x"); // make sure it's not empty
122
+ const err = removeKeyFile(dirPath);
123
+ expect(err).not.toBeNull();
124
+ expect(err).toBeInstanceOf(Error);
125
+ // Error code is platform-dependent (EISDIR on linux, EPERM on
126
+ // macos), so just assert we got something Error-shaped.
127
+ expect((err as NodeJS.ErrnoException).code).toMatch(/^E/);
128
+ expect(existsSync(dirPath)).toBe(true);
129
+ });
130
+ });
131
+ });
@@ -1,7 +1,7 @@
1
1
  import { pathToFileURL } from "url";
2
2
  import { join } from "path";
3
3
  import { existsSync, statSync } from "fs";
4
- import { Account, Ed25519PrivateKey } from "@aptos-labs/ts-sdk";
4
+ import { Account, Ed25519PrivateKey, PrivateKey, PrivateKeyVariants } from "@aptos-labs/ts-sdk";
5
5
  import { MovehatConfig, MovehatUserConfig } from "../types/config.js";
6
6
 
7
7
  interface ConfigCacheEntry {
@@ -258,11 +258,15 @@ export async function resolveNetworkConfig(
258
258
  function deriveAccountAddress(privateKeyHex: string | undefined): string {
259
259
  if (!privateKeyHex) return "";
260
260
  try {
261
- const stripped = privateKeyHex.startsWith("ed25519-priv-")
262
- ? privateKeyHex.slice("ed25519-priv-".length)
263
- : privateKeyHex;
261
+ // Format into AIP-80 shape so the SDK doesn't emit a deprecation
262
+ // warning on each derivation. `formatPrivateKey` is idempotent for
263
+ // already-prefixed inputs.
264
+ const formatted = PrivateKey.formatPrivateKey(
265
+ privateKeyHex,
266
+ PrivateKeyVariants.Ed25519,
267
+ );
264
268
  const account = Account.fromPrivateKey({
265
- privateKey: new Ed25519PrivateKey(stripped),
269
+ privateKey: new Ed25519PrivateKey(formatted),
266
270
  });
267
271
  return account.accountAddress.toString();
268
272
  } catch (err) {
@@ -94,9 +94,6 @@ export class MoveContract {
94
94
  }
95
95
  }
96
96
 
97
- /**
98
- * Factory function to create a contract instance
99
- */
100
97
  export function getContract(
101
98
  aptos: Aptos,
102
99
  moduleAddress: string,
@@ -50,16 +50,10 @@ export function validateSafeName(name: string, type: "network" | "module"): void
50
50
  }
51
51
  }
52
52
 
53
- /**
54
- * Get the deployments directory path
55
- */
56
53
  function getDeploymentsDir(): string {
57
54
  return join(process.cwd(), "deployments");
58
55
  }
59
56
 
60
- /**
61
- * Get the network-specific deployments directory
62
- */
63
57
  function getNetworkDeploymentsDir(network: string): string {
64
58
  // Validate network name to prevent path traversal
65
59
  validateSafeName(network, "network");
@@ -78,9 +72,6 @@ function getNetworkDeploymentsDir(network: string): string {
78
72
  return networkDir;
79
73
  }
80
74
 
81
- /**
82
- * Save a deployment
83
- */
84
75
  export function saveDeployment(deployment: DeploymentInfo): void {
85
76
  // Validate both network and module name
86
77
  validateSafeName(deployment.network, "network");
@@ -103,9 +94,6 @@ export function saveDeployment(deployment: DeploymentInfo): void {
103
94
  }
104
95
  }
105
96
 
106
- /**
107
- * Load a deployment
108
- */
109
97
  export function loadDeployment(network: string, moduleName: string): DeploymentInfo | null {
110
98
  // Validate both network and module name
111
99
  validateSafeName(network, "network");