movehat 0.2.1 → 0.2.3

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 (176) hide show
  1. package/dist/__tests__/deployContract.test.js +56 -47
  2. package/dist/__tests__/deployContract.test.js.map +1 -1
  3. package/dist/__tests__/exports.test.d.ts +2 -0
  4. package/dist/__tests__/exports.test.d.ts.map +1 -0
  5. package/dist/__tests__/exports.test.js +30 -0
  6. package/dist/__tests__/exports.test.js.map +1 -0
  7. package/dist/__tests__/fixtures/sigint-deploy-harness.d.ts +4 -3
  8. package/dist/__tests__/fixtures/sigint-deploy-harness.d.ts.map +1 -1
  9. package/dist/__tests__/fixtures/sigint-deploy-harness.js +8 -7
  10. package/dist/__tests__/fixtures/sigint-deploy-harness.js.map +1 -1
  11. package/dist/__tests__/fork/api.test.js +5 -0
  12. package/dist/__tests__/fork/api.test.js.map +1 -1
  13. package/dist/__tests__/fork/api.timeout.test.d.ts +2 -0
  14. package/dist/__tests__/fork/api.timeout.test.d.ts.map +1 -0
  15. package/dist/__tests__/fork/api.timeout.test.js +98 -0
  16. package/dist/__tests__/fork/api.timeout.test.js.map +1 -0
  17. package/dist/cli.js +4 -0
  18. package/dist/cli.js.map +1 -1
  19. package/dist/commands/__tests__/compile.toml-mutation.test.d.ts +2 -0
  20. package/dist/commands/__tests__/compile.toml-mutation.test.d.ts.map +1 -0
  21. package/dist/commands/__tests__/compile.toml-mutation.test.js +69 -0
  22. package/dist/commands/__tests__/compile.toml-mutation.test.js.map +1 -0
  23. package/dist/commands/__tests__/init.test.js +73 -11
  24. package/dist/commands/__tests__/init.test.js.map +1 -1
  25. package/dist/commands/compile.d.ts.map +1 -1
  26. package/dist/commands/compile.js +19 -10
  27. package/dist/commands/compile.js.map +1 -1
  28. package/dist/commands/init.d.ts +22 -0
  29. package/dist/commands/init.d.ts.map +1 -1
  30. package/dist/commands/init.js +55 -6
  31. package/dist/commands/init.js.map +1 -1
  32. package/dist/commands/test.js +12 -19
  33. package/dist/commands/test.js.map +1 -1
  34. package/dist/core/AccountManager.d.ts.map +1 -1
  35. package/dist/core/AccountManager.js +14 -2
  36. package/dist/core/AccountManager.js.map +1 -1
  37. package/dist/core/Publisher.d.ts.map +1 -1
  38. package/dist/core/Publisher.js +72 -82
  39. package/dist/core/Publisher.js.map +1 -1
  40. package/dist/core/__tests__/AccountManager.global-state.test.d.ts +2 -0
  41. package/dist/core/__tests__/AccountManager.global-state.test.d.ts.map +1 -0
  42. package/dist/core/__tests__/AccountManager.global-state.test.js +69 -0
  43. package/dist/core/__tests__/AccountManager.global-state.test.js.map +1 -0
  44. package/dist/core/__tests__/movementProfile.test.d.ts +2 -0
  45. package/dist/core/__tests__/movementProfile.test.d.ts.map +1 -0
  46. package/dist/core/__tests__/movementProfile.test.js +112 -0
  47. package/dist/core/__tests__/movementProfile.test.js.map +1 -0
  48. package/dist/core/config.d.ts.map +1 -1
  49. package/dist/core/config.js +14 -10
  50. package/dist/core/config.js.map +1 -1
  51. package/dist/core/deployments.d.ts.map +1 -1
  52. package/dist/core/deployments.js +4 -2
  53. package/dist/core/deployments.js.map +1 -1
  54. package/dist/core/movementProfile.d.ts +55 -22
  55. package/dist/core/movementProfile.d.ts.map +1 -1
  56. package/dist/core/movementProfile.js +77 -99
  57. package/dist/core/movementProfile.js.map +1 -1
  58. package/dist/fork/__tests__/server.cors.test.d.ts +2 -0
  59. package/dist/fork/__tests__/server.cors.test.d.ts.map +1 -0
  60. package/dist/fork/__tests__/server.cors.test.js +79 -0
  61. package/dist/fork/__tests__/server.cors.test.js.map +1 -0
  62. package/dist/fork/api.d.ts +9 -1
  63. package/dist/fork/api.d.ts.map +1 -1
  64. package/dist/fork/api.js +37 -7
  65. package/dist/fork/api.js.map +1 -1
  66. package/dist/fork/manager.js +10 -10
  67. package/dist/fork/manager.js.map +1 -1
  68. package/dist/fork/server.d.ts +20 -1
  69. package/dist/fork/server.d.ts.map +1 -1
  70. package/dist/fork/server.js +40 -24
  71. package/dist/fork/server.js.map +1 -1
  72. package/dist/fork/test.d.ts.map +1 -1
  73. package/dist/fork/test.js +3 -2
  74. package/dist/fork/test.js.map +1 -1
  75. package/dist/harness/Harness.d.ts +6 -2
  76. package/dist/harness/Harness.d.ts.map +1 -1
  77. package/dist/harness/Harness.js +8 -2
  78. package/dist/harness/Harness.js.map +1 -1
  79. package/dist/harness/codeObject.d.ts.map +1 -1
  80. package/dist/harness/codeObject.js +41 -41
  81. package/dist/harness/codeObject.js.map +1 -1
  82. package/dist/harness/script.d.ts +3 -3
  83. package/dist/harness/script.d.ts.map +1 -1
  84. package/dist/harness/script.js +42 -35
  85. package/dist/harness/script.js.map +1 -1
  86. package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.d.ts +2 -0
  87. package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.d.ts.map +1 -0
  88. package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.js +172 -0
  89. package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.js.map +1 -0
  90. package/dist/helpers/setupLocalTesting.d.ts.map +1 -1
  91. package/dist/helpers/setupLocalTesting.js +31 -5
  92. package/dist/helpers/setupLocalTesting.js.map +1 -1
  93. package/dist/index.d.ts +1 -0
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/node/LocalNodeManager.d.ts +8 -0
  96. package/dist/node/LocalNodeManager.d.ts.map +1 -1
  97. package/dist/node/LocalNodeManager.js +70 -23
  98. package/dist/node/LocalNodeManager.js.map +1 -1
  99. package/dist/node/__tests__/LocalNodeManager.api-port.test.d.ts +2 -0
  100. package/dist/node/__tests__/LocalNodeManager.api-port.test.d.ts.map +1 -0
  101. package/dist/node/__tests__/LocalNodeManager.api-port.test.js +55 -0
  102. package/dist/node/__tests__/LocalNodeManager.api-port.test.js.map +1 -0
  103. package/dist/node/__tests__/LocalNodeManager.test.js +114 -14
  104. package/dist/node/__tests__/LocalNodeManager.test.js.map +1 -1
  105. package/dist/templates/move/Move.toml +1 -1
  106. package/dist/templates/move/sources/Counter.move +31 -4
  107. package/dist/templates/scripts/deploy-counter.ts +10 -0
  108. package/dist/types/config.d.ts +8 -1
  109. package/dist/types/config.d.ts.map +1 -1
  110. package/dist/ui/__tests__/logger.test.d.ts +2 -0
  111. package/dist/ui/__tests__/logger.test.d.ts.map +1 -0
  112. package/dist/ui/__tests__/logger.test.js +75 -0
  113. package/dist/ui/__tests__/logger.test.js.map +1 -0
  114. package/dist/ui/formatters.d.ts +0 -16
  115. package/dist/ui/formatters.d.ts.map +1 -1
  116. package/dist/ui/formatters.js +1 -1
  117. package/dist/ui/formatters.js.map +1 -1
  118. package/dist/ui/logger.d.ts +41 -0
  119. package/dist/ui/logger.d.ts.map +1 -1
  120. package/dist/ui/logger.js +49 -0
  121. package/dist/ui/logger.js.map +1 -1
  122. package/dist/ui/spinner.d.ts +25 -0
  123. package/dist/ui/spinner.d.ts.map +1 -1
  124. package/dist/ui/spinner.js +44 -0
  125. package/dist/ui/spinner.js.map +1 -1
  126. package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.d.ts +2 -0
  127. package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.d.ts.map +1 -0
  128. package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.js +43 -0
  129. package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.js.map +1 -0
  130. package/dist/utils/childProcessAdapter.d.ts +7 -0
  131. package/dist/utils/childProcessAdapter.d.ts.map +1 -1
  132. package/dist/utils/childProcessAdapter.js +20 -2
  133. package/dist/utils/childProcessAdapter.js.map +1 -1
  134. package/package.json +1 -1
  135. package/src/__tests__/deployContract.test.ts +59 -50
  136. package/src/__tests__/exports.test.ts +32 -0
  137. package/src/__tests__/fixtures/sigint-deploy-harness.ts +8 -7
  138. package/src/__tests__/fork/api.test.ts +5 -0
  139. package/src/__tests__/fork/api.timeout.test.ts +150 -0
  140. package/src/cli.ts +4 -0
  141. package/src/commands/__tests__/compile.toml-mutation.test.ts +77 -0
  142. package/src/commands/__tests__/init.test.ts +96 -11
  143. package/src/commands/compile.ts +24 -15
  144. package/src/commands/init.ts +77 -6
  145. package/src/commands/test.ts +12 -19
  146. package/src/core/AccountManager.ts +18 -1
  147. package/src/core/Publisher.ts +103 -107
  148. package/src/core/__tests__/AccountManager.global-state.test.ts +83 -0
  149. package/src/core/__tests__/movementProfile.test.ts +131 -0
  150. package/src/core/config.ts +18 -11
  151. package/src/core/deployments.ts +5 -4
  152. package/src/core/movementProfile.ts +75 -127
  153. package/src/fork/__tests__/server.cors.test.ts +101 -0
  154. package/src/fork/api.ts +69 -10
  155. package/src/fork/manager.ts +10 -10
  156. package/src/fork/server.ts +59 -24
  157. package/src/fork/test.ts +3 -2
  158. package/src/harness/Harness.ts +11 -2
  159. package/src/harness/codeObject.ts +45 -48
  160. package/src/harness/script.ts +47 -43
  161. package/src/helpers/__tests__/setupLocalTesting.fork-network.test.ts +212 -0
  162. package/src/helpers/setupLocalTesting.ts +39 -5
  163. package/src/index.ts +9 -1
  164. package/src/node/LocalNodeManager.ts +87 -26
  165. package/src/node/__tests__/LocalNodeManager.api-port.test.ts +62 -0
  166. package/src/node/__tests__/LocalNodeManager.test.ts +144 -17
  167. package/src/templates/move/Move.toml +1 -1
  168. package/src/templates/move/sources/Counter.move +31 -4
  169. package/src/templates/scripts/deploy-counter.ts +10 -0
  170. package/src/types/config.ts +8 -1
  171. package/src/ui/__tests__/logger.test.ts +89 -0
  172. package/src/ui/formatters.ts +1 -1
  173. package/src/ui/logger.ts +62 -0
  174. package/src/ui/spinner.ts +47 -0
  175. package/src/utils/__tests__/childProcessAdapter.maxBuffer.test.ts +51 -0
  176. package/src/utils/childProcessAdapter.ts +32 -2
@@ -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,16 @@ 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
- import { logger } from "../ui/index.js";
13
+ import { logger, isVerbose } from "../ui/index.js";
14
+ import { withSpinner } from "../ui/spinner.js";
17
15
  import type { ChildProcessAdapter } from "../utils/childProcessAdapter.js";
18
16
  import {
19
- withYamlLock,
20
- addProfile,
21
- removeProfile,
22
- removeProfileSync,
17
+ writeTempKeyFile,
18
+ removeKeyFile,
19
+ removeKeyFileSyncBestEffort,
23
20
  ensureSignalHandler,
24
21
  cleanupCallbacks,
25
22
  } from "./movementProfile.js";
@@ -98,18 +95,10 @@ export class Publisher {
98
95
 
99
96
  const dir = input.packageDir || config.moveDir;
100
97
 
101
- // Bug #37: use a UUID-suffixed profile name per deploy so concurrent
102
- // Publisher.deploy() calls in the same process don't fight over the
103
- // same key in ~/.aptos/config.yaml. The previous code reused
104
- // config.profile (default "default"), which meant two parallel
105
- // deploys would clobber each other's profile data mid-publish.
106
- const profile = `movehat-deploy-${randomUUID().slice(0, 8)}`;
107
-
108
98
  // Validate (no shell escape — runCli uses spawn, which takes args
109
99
  // verbatim and would treat the single-quote wrapping as part of the
110
- // literal path/profile, breaking Movement CLI argument parsing).
100
+ // literal path, breaking Movement CLI argument parsing).
111
101
  const safeDir = validatePathSafety(dir, "package directory");
112
- const safeProfile = validateProfileSafety(profile);
113
102
 
114
103
  logger.step(`Publishing module "${moduleName}" from ${dir}...`);
115
104
 
@@ -134,112 +123,119 @@ export class Publisher {
134
123
  : [];
135
124
 
136
125
  // Build first with named addresses
137
- logger.step("Building package...");
138
- const buildResult = await runCli(
139
- {
140
- command: "movement",
141
- args: ["move", "build", "--package-dir", safeDir, ...namedAddrArgs],
142
- timeoutMs: 120000, // 2 minutes for git dependency downloads
143
- },
144
- { adapter: this.deps.adapter }
126
+ const buildResult = await withSpinner(
127
+ "Building package",
128
+ () =>
129
+ runCli(
130
+ {
131
+ command: "movement",
132
+ args: ["move", "build", "--package-dir", safeDir, ...namedAddrArgs],
133
+ timeoutMs: 120000, // 2 minutes for git dependency downloads
134
+ },
135
+ { adapter: this.deps.adapter }
136
+ ),
145
137
  );
146
- if (buildResult.stdout) console.log(buildResult.stdout.trim());
138
+ if (isVerbose() && buildResult.stdout) {
139
+ logger.info(buildResult.stdout.trim(), 2);
140
+ }
147
141
 
148
142
  // Publish using direct parameters (avoid config file issues)
149
- logger.step("Publishing to blockchain...");
150
143
 
151
- // Use parameters directly instead of relying on config file
152
- // Strip any ed25519-priv- prefix if present
153
- let cleanPrivateKey = config.privateKey;
154
- if (cleanPrivateKey.startsWith("ed25519-priv-")) {
155
- cleanPrivateKey = cleanPrivateKey.replace("ed25519-priv-", "");
156
- }
144
+ // Format the private key into AIP-80 shape so the Movement CLI
145
+ // doesn't emit its raw-hex deprecation warning. `formatPrivateKey`
146
+ // is idempotent for already-prefixed inputs.
147
+ const formattedPrivateKey = PrivateKey.formatPrivateKey(
148
+ config.privateKey,
149
+ PrivateKeyVariants.Ed25519,
150
+ );
157
151
 
158
- // Bug #38: Move.toml is NOT mutated. All address overrides flow
159
- // through the `--named-addresses` flag above, which Movement CLI
160
- // applies during build + publish. The previous regex rewrite +
161
- // restore-in-finally was destructive: if the process died between
162
- // write and restore, the user's Move.toml stayed mutated.
152
+ // Move.toml is NOT mutated. All address overrides flow through
153
+ // the `--named-addresses` flag above, which Movement CLI applies
154
+ // during build + publish. Rewriting Move.toml on disk would risk
155
+ // leaving the user's file mutated if the process died before the
156
+ // restore step.
163
157
 
164
158
  let publishOut = "";
165
159
  let publishErr = "";
166
160
 
167
- // Setup Movement CLI config with private key securely.
168
- // Movement CLI uses .aptos config directory (not .movement).
169
- const movementConfigPath = join(homedir(), ".aptos", "config.yaml");
170
-
171
- // Register a sync cleanup hook BEFORE writing the private key.
172
- // If the user Ctrl+C's (or the process is SIGTERM'd) between the
173
- // yaml write and our async finally, the SIGINT handler iterates
174
- // every registered callback and removes this deploy's profile
175
- // synchronously closes bug #36 (private key persisting on disk
176
- // after abnormal exit).
161
+ // Pass the private key to Movement CLI via a 0o600 temp file
162
+ // (`--private-key-file <path>`) and the on-chain address via
163
+ // `--sender-account <addr>`. This avoids the CLI's profile-yaml
164
+ // lookup chain entirely — no CWD / HOME / .aptos / .movement
165
+ // dance, no CLI-variant dependency.
166
+ const keyFilePath = writeTempKeyFile(formattedPrivateKey);
167
+
168
+ // Register a sync cleanup hook BEFORE invoking the CLI. If the
169
+ // user Ctrl+C's (or the process is SIGTERM'd) between the file
170
+ // write and our finally, the SIGINT handler iterates every
171
+ // registered callback and unlinks this deploy's key file
172
+ // synchronously so the private key never persists on disk after
173
+ // an abnormal exit. The signal-handler path uses the
174
+ // best-effort variant because the event loop is dead and we
175
+ // cannot logger.warning.
177
176
  ensureSignalHandler();
178
- const syncCleanup = () => removeProfileSync(movementConfigPath, profile);
177
+ const syncCleanup = () => removeKeyFileSyncBestEffort(keyFilePath);
179
178
  cleanupCallbacks.add(syncCleanup);
180
179
 
181
- // Add our deploy profile under the unique key. The mutex serializes
182
- // read-modify-write cycles so concurrent deploys in the same process
183
- // can't drop each other's profiles. Other user profiles in the same
184
- // file are preserved untouched.
185
- await withYamlLock(() =>
186
- addProfile(movementConfigPath, profile, {
187
- private_key: cleanPrivateKey,
188
- public_key: account.publicKey.toString(),
189
- account: deployerAddress,
190
- rest_url: config.rpc,
191
- })
192
- );
193
-
194
180
  try {
195
- // Execute publish command without exposing private key in CLI.
196
- // Routed through runCli so stdout/stderr are redacted of any
197
- // `ed25519-priv-…` shape before reaching console.log/console.error
198
- // or the thrown CliExecutionError that's bug #43.
199
- const publishResult = await runCli(
200
- {
201
- command: "movement",
202
- args: [
203
- "move",
204
- "publish",
205
- "--package-dir",
206
- safeDir,
207
- "--url",
208
- config.rpc,
209
- "--profile",
210
- safeProfile,
211
- "--assume-yes",
212
- ...namedAddrArgs,
213
- ],
214
- timeoutMs: 120000, // 2 minutes for blockchain transactions
215
- },
216
- { adapter: this.deps.adapter }
181
+ // Execute publish command. Private key reaches the CLI via the
182
+ // temp key file path (--private-key-file) never on the
183
+ // command line so it can't leak through `ps aux`. runCli's
184
+ // stdout/stderr redaction still applies as defense in depth
185
+ // for any `ed25519-priv-…` substring that surfaces in CLI
186
+ // output (Movement CLI sometimes echoes the key on error).
187
+ const publishResult = await withSpinner(
188
+ "Publishing to blockchain",
189
+ () =>
190
+ runCli(
191
+ {
192
+ command: "movement",
193
+ args: [
194
+ "move",
195
+ "publish",
196
+ "--package-dir",
197
+ safeDir,
198
+ "--url",
199
+ config.rpc,
200
+ "--private-key-file",
201
+ keyFilePath,
202
+ "--sender-account",
203
+ deployerAddress,
204
+ "--assume-yes",
205
+ ...namedAddrArgs,
206
+ ],
207
+ timeoutMs: 120000, // 2 minutes for blockchain transactions
208
+ },
209
+ { adapter: this.deps.adapter }
210
+ ),
217
211
  );
218
212
  publishOut = publishResult.stdout;
219
213
  publishErr = publishResult.stderr;
220
- if (publishOut) console.log(publishOut.trim());
221
- if (publishErr) console.error(publishErr.trim());
214
+ // Both stdout and stderr from the publish subprocess are gated
215
+ // behind isVerbose() — Movement CLI emits progress to both
216
+ // streams ("Compiling, may take a little while..."), so a
217
+ // visible stderr line is not by itself a failure signal. The
218
+ // surrounding withSpinner converts the runCli throw on real
219
+ // failure into the visible spinner.fail() output instead.
220
+ if (isVerbose() && publishOut) logger.info(publishOut.trim(), 2);
221
+ if (isVerbose() && publishErr) logger.info(publishErr.trim(), 2);
222
222
  } finally {
223
- // Always remove our profile from the shared yaml — never restore
224
- // a "snapshot" of the whole file (that's what the old code did,
225
- // and that's the bug #37 race). Removing only our key leaves
226
- // other concurrent deploys' profiles intact.
227
- //
228
- // CRITICAL: catch + log instead of throwing. `await` in a finally
229
- // block that throws will clobber both the try block's successful
230
- // return value AND any error already propagating. Without this
231
- // catch, a yaml-write failure here would mask a successful
232
- // publish (making the deploy look failed and inviting a redeploy)
233
- // or mask the real publish error.
234
- await withYamlLock(() => removeProfile(movementConfigPath, profile)).catch((err) => {
235
- const cleanupMsg = err instanceof Error ? err.message : String(err);
223
+ // Unlink the temp key file via the observable cleanup helper.
224
+ // ENOENT and other already-gone outcomes are benign (null).
225
+ // A non-null Error means the unlink failed AND the file still
226
+ // exists on disk the private key would persist silently
227
+ // otherwise, so we emit a warning with the manual-cleanup
228
+ // hint. The SIGINT signal handler's sync callback below also
229
+ // tries to remove the same file; if SIGINT fires before this
230
+ // finally runs the file is gone and the next finally call
231
+ // sees ENOENT (benign).
232
+ const cleanupErr = removeKeyFile(keyFilePath);
233
+ if (cleanupErr) {
236
234
  logger.warning(
237
- `Failed to remove deploy profile "${profile}" from ${movementConfigPath}: ${cleanupMsg}. ` +
238
- `Run 'movement config delete-profile --profile ${profile}' to clean up manually.`
235
+ `Failed to remove temp key file '${keyFilePath}': ${cleanupErr.message}. ` +
236
+ `The file has mode 0o600 but should be removed manually: rm ${keyFilePath}`
239
237
  );
240
- });
241
- // Unregister the sync cleanup hook — normal path. (The signal
242
- // handler stays installed for the process lifetime; cheap.)
238
+ }
243
239
  cleanupCallbacks.delete(syncCleanup);
244
240
  }
245
241
 
@@ -298,7 +294,7 @@ export class Publisher {
298
294
  if (error instanceof CliExecutionError) {
299
295
  // stdout/stderr are already redacted by runCli before reaching here,
300
296
  // so this branch is safe to log verbatim.
301
- if (error.stdoutPreview) console.log(error.stdoutPreview);
297
+ if (error.stdoutPreview) logger.info(error.stdoutPreview, 2);
302
298
  logger.error(`Failed to publish module: ${error.message}\n${error.stderr}`);
303
299
  } else {
304
300
  // Preserve existing behaviour for non-CLI errors (filesystem write
@@ -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,8 +1,9 @@
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
+ import { logger } from "../ui/index.js";
6
7
 
7
8
  interface ConfigCacheEntry {
8
9
  mtimeMs: number;
@@ -132,7 +133,7 @@ export async function resolveNetworkConfig(
132
133
  url: "https://testnet.movementnetwork.xyz/v1",
133
134
  chainId: "testnet",
134
135
  };
135
- console.log(`testnet not found in config - using default Movement testnet configuration`);
136
+ logger.info("testnet not found in config - using default Movement testnet configuration");
136
137
  }
137
138
 
138
139
  // Special case: Auto-generate config for local fork server
@@ -141,7 +142,7 @@ export async function resolveNetworkConfig(
141
142
  url: "http://localhost:8080/v1",
142
143
  chainId: "local",
143
144
  };
144
- console.log(`Local network not found in config - using default fork server configuration`);
145
+ logger.info("Local network not found in config - using default fork server configuration");
145
146
  }
146
147
 
147
148
  if (!networkConfig) {
@@ -187,8 +188,10 @@ export async function resolveNetworkConfig(
187
188
  // 3. Deterministic = consistent test results
188
189
  const testPrivateKey = "0x0000000000000000000000000000000000000000000000000000000000000001";
189
190
  accounts = [testPrivateKey];
190
- console.log(`\n[TESTNET] Using auto-generated test account (safe for testing only)`);
191
- console.log(`[TESTNET] For mainnet, set PRIVATE_KEY in .env\n`);
191
+ logger.newline();
192
+ logger.warning("[TESTNET] Using auto-generated test account (safe for testing only)");
193
+ logger.warning("[TESTNET] For mainnet, set PRIVATE_KEY in .env");
194
+ logger.newline();
192
195
  } else {
193
196
  // For any other network (especially mainnet), REQUIRE explicit configuration
194
197
  // This prevents accidentally using the test key on production networks
@@ -258,19 +261,23 @@ export async function resolveNetworkConfig(
258
261
  function deriveAccountAddress(privateKeyHex: string | undefined): string {
259
262
  if (!privateKeyHex) return "";
260
263
  try {
261
- const stripped = privateKeyHex.startsWith("ed25519-priv-")
262
- ? privateKeyHex.slice("ed25519-priv-".length)
263
- : privateKeyHex;
264
+ // Format into AIP-80 shape so the SDK doesn't emit a deprecation
265
+ // warning on each derivation. `formatPrivateKey` is idempotent for
266
+ // already-prefixed inputs.
267
+ const formatted = PrivateKey.formatPrivateKey(
268
+ privateKeyHex,
269
+ PrivateKeyVariants.Ed25519,
270
+ );
264
271
  const account = Account.fromPrivateKey({
265
- privateKey: new Ed25519PrivateKey(stripped),
272
+ privateKey: new Ed25519PrivateKey(formatted),
266
273
  });
267
274
  return account.accountAddress.toString();
268
275
  } catch (err) {
269
276
  // The private key may have come from several sources (network.accounts,
270
277
  // global accounts, PRIVATE_KEY env, auto-generated testnet key). Keep
271
278
  // the hint generic so it never points at the wrong source.
272
- console.warn(
273
- `[movehat] Could not derive account address from the resolved private key: ${
279
+ logger.warning(
280
+ `Could not derive account address from the resolved private key: ${
274
281
  (err as Error).message
275
282
  }. Verify the key configured for this network is a valid Ed25519 private key (with or without the "ed25519-priv-" prefix).`
276
283
  );
@@ -86,9 +86,9 @@ export function saveDeployment(deployment: DeploymentInfo): void {
86
86
  `Deployment saved: deployments/${deployment.network}/${deployment.moduleName}.json`
87
87
  );
88
88
  } catch (error) {
89
- console.error(
90
- `Failed to save deployment for ${deployment.moduleName} on ${deployment.network} at ${filePath}:`,
91
- error
89
+ const msg = error instanceof Error ? error.message : String(error);
90
+ logger.error(
91
+ `Failed to save deployment for ${deployment.moduleName} on ${deployment.network} at ${filePath}: ${msg}`
92
92
  );
93
93
  throw error;
94
94
  }
@@ -110,7 +110,8 @@ export function loadDeployment(network: string, moduleName: string): DeploymentI
110
110
  const content = readFileSync(filePath, "utf-8");
111
111
  return JSON.parse(content) as DeploymentInfo;
112
112
  } catch (error) {
113
- console.error(`Failed to load deployment for ${moduleName} on ${network}:`, error);
113
+ const msg = error instanceof Error ? error.message : String(error);
114
+ logger.error(`Failed to load deployment for ${moduleName} on ${network}: ${msg}`);
114
115
  return null;
115
116
  }
116
117
  }