movehat 0.2.8 → 0.2.9

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.
@@ -1,4 +1,4 @@
1
- import { Account } from "@aptos-labs/ts-sdk";
1
+ import { Account, Aptos } from "@aptos-labs/ts-sdk";
2
2
  import { MovehatConfig } from "../types/config.js";
3
3
  import { DeploymentInfo } from "./deployments.js";
4
4
  import type { ChildProcessAdapter } from "../utils/childProcessAdapter.js";
@@ -12,6 +12,13 @@ export interface PublishInput {
12
12
  config: MovehatConfig;
13
13
  account: Account;
14
14
  packageDir?: string | undefined;
15
+ /**
16
+ * Publish via the TypeScript SDK instead of the Movement CLI. Set when the
17
+ * backend is movelite, whose REST responses the Movement CLI cannot parse.
18
+ * Requires `aptos`.
19
+ */
20
+ sdkPublish?: boolean | undefined;
21
+ aptos?: Aptos | undefined;
15
22
  }
16
23
  /**
17
24
  * Publishes a Move module via the Movement CLI.
@@ -22,4 +29,16 @@ export declare class Publisher {
22
29
  private readonly deps;
23
30
  constructor(deps?: PublisherDeps);
24
31
  deploy(input: PublishInput): Promise<DeploymentInfo>;
32
+ /**
33
+ * Publish via the Movement CLI (`movement move publish`). The default path
34
+ * for real Movement nodes, forks, and testnet. Returns the parsed tx hash.
35
+ */
36
+ private publishViaCli;
37
+ /**
38
+ * Publish the already-built package via the TypeScript SDK. Used when the
39
+ * backend is movelite. Reads the compiled artifacts the CLI build produced
40
+ * under `<dir>/build/<pkg>/`, submits a `0x1::code::publish_package_txn`,
41
+ * and waits for it. Returns the on-chain tx hash.
42
+ */
43
+ private publishViaSdk;
25
44
  }
@@ -1,4 +1,6 @@
1
1
  import { PrivateKey, PrivateKeyVariants } from "@aptos-labs/ts-sdk";
2
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
3
+ import { join } from "node:path";
2
4
  import { extractNamedAddresses } from "../commands/compile.js";
3
5
  import { saveDeployment, loadDeployment, validateSafeName, } from "./deployments.js";
4
6
  import { validatePathSafety } from "./shell.js";
@@ -74,103 +76,34 @@ export class Publisher {
74
76
  .join(","),
75
77
  ]
76
78
  : [];
79
+ // The SDK publish path reads package-metadata.bcs from the build
80
+ // output; `--save-metadata` makes the build emit it. The CLI path
81
+ // doesn't need it (`move publish` rebuilds metadata internally).
82
+ const saveMetadataArgs = input.sdkPublish ? ["--save-metadata"] : [];
77
83
  // Build first with named addresses
78
84
  const buildResult = await withSpinner("Building package", () => runCli({
79
85
  command: "movement",
80
- args: ["move", "build", "--package-dir", safeDir, ...namedAddrArgs],
86
+ args: [
87
+ "move",
88
+ "build",
89
+ "--package-dir",
90
+ safeDir,
91
+ ...namedAddrArgs,
92
+ ...saveMetadataArgs,
93
+ ],
81
94
  timeoutMs: 120000, // 2 minutes for git dependency downloads
82
95
  }, { adapter: this.deps.adapter }));
83
96
  if (isVerbose() && buildResult.stdout) {
84
97
  logger.info(buildResult.stdout.trim(), 2);
85
98
  }
86
- // Publish using direct parameters (avoid config file issues)
87
- // Format the private key into AIP-80 shape so the Movement CLI
88
- // doesn't emit its raw-hex deprecation warning. `formatPrivateKey`
89
- // is idempotent for already-prefixed inputs.
90
- const formattedPrivateKey = PrivateKey.formatPrivateKey(config.privateKey, PrivateKeyVariants.Ed25519);
91
- // Move.toml is NOT mutated. All address overrides flow through
92
- // the `--named-addresses` flag above, which Movement CLI applies
93
- // during build + publish. Rewriting Move.toml on disk would risk
94
- // leaving the user's file mutated if the process died before the
95
- // restore step.
96
- let publishOut = "";
97
- let publishErr = "";
98
- // Pass the private key to Movement CLI via a 0o600 temp file
99
- // (`--private-key-file <path>`) and the on-chain address via
100
- // `--sender-account <addr>`. This avoids the CLI's profile-yaml
101
- // lookup chain entirely — no CWD / HOME / .aptos / .movement
102
- // dance, no CLI-variant dependency.
103
- const keyFilePath = writeTempKeyFile(formattedPrivateKey);
104
- // Register a sync cleanup hook BEFORE invoking the CLI. If the
105
- // user Ctrl+C's (or the process is SIGTERM'd) between the file
106
- // write and our finally, the SIGINT handler iterates every
107
- // registered callback and unlinks this deploy's key file
108
- // synchronously so the private key never persists on disk after
109
- // an abnormal exit. The signal-handler path uses the
110
- // best-effort variant because the event loop is dead and we
111
- // cannot logger.warning.
112
- ensureSignalHandler();
113
- const syncCleanup = () => removeKeyFileSyncBestEffort(keyFilePath);
114
- cleanupCallbacks.add(syncCleanup);
115
- try {
116
- // Execute publish command. Private key reaches the CLI via the
117
- // temp key file path (--private-key-file) — never on the
118
- // command line — so it can't leak through `ps aux`. runCli's
119
- // stdout/stderr redaction still applies as defense in depth
120
- // for any `ed25519-priv-…` substring that surfaces in CLI
121
- // output (Movement CLI sometimes echoes the key on error).
122
- const publishResult = await withSpinner("Publishing to blockchain", () => runCli({
123
- command: "movement",
124
- args: [
125
- "move",
126
- "publish",
127
- "--package-dir",
128
- safeDir,
129
- "--url",
130
- config.rpc,
131
- "--private-key-file",
132
- keyFilePath,
133
- "--sender-account",
134
- deployerAddress,
135
- "--assume-yes",
136
- ...namedAddrArgs,
137
- ],
138
- timeoutMs: 120000, // 2 minutes for blockchain transactions
139
- }, { adapter: this.deps.adapter }));
140
- publishOut = publishResult.stdout;
141
- publishErr = publishResult.stderr;
142
- // Both stdout and stderr from the publish subprocess are gated
143
- // behind isVerbose() — Movement CLI emits progress to both
144
- // streams ("Compiling, may take a little while..."), so a
145
- // visible stderr line is not by itself a failure signal. The
146
- // surrounding withSpinner converts the runCli throw on real
147
- // failure into the visible spinner.fail() output instead.
148
- if (isVerbose() && publishOut)
149
- logger.info(publishOut.trim(), 2);
150
- if (isVerbose() && publishErr)
151
- logger.info(publishErr.trim(), 2);
152
- }
153
- finally {
154
- // Unlink the temp key file via the observable cleanup helper.
155
- // ENOENT and other already-gone outcomes are benign (null).
156
- // A non-null Error means the unlink failed AND the file still
157
- // exists on disk — the private key would persist silently
158
- // otherwise, so we emit a warning with the manual-cleanup
159
- // hint. The SIGINT signal handler's sync callback below also
160
- // tries to remove the same file; if SIGINT fires before this
161
- // finally runs the file is gone and the next finally call
162
- // sees ENOENT (benign).
163
- const cleanupErr = removeKeyFile(keyFilePath);
164
- if (cleanupErr) {
165
- logger.warning(`Failed to remove temp key file '${keyFilePath}': ${cleanupErr.message}. ` +
166
- `The file has mode 0o600 but should be removed manually: rm ${keyFilePath}`);
167
- }
168
- cleanupCallbacks.delete(syncCleanup);
169
- }
170
- // Extract transaction hash from output via the shared helper
171
- // (`utils/parseCliOutput.ts`). Same regex pair as before; lifted
172
- // for reuse by harness/codeObject.ts and harness/script.ts.
173
- const txHash = parseTxHash(publishOut);
99
+ // Publish the freshly-built package. movelite cannot consume the
100
+ // Movement CLI's `move publish` REST flow (its responses omit the
101
+ // ledger headers and fields the CLI requires), so when the backend
102
+ // is movelite we publish via the TypeScript SDK instead. Every other
103
+ // backend keeps the CLI path.
104
+ const txHash = input.sdkPublish
105
+ ? await this.publishViaSdk(input, safeDir)
106
+ : await this.publishViaCli(config, safeDir, deployerAddress, namedAddrArgs);
174
107
  logger.success("Module published successfully!");
175
108
  // ←← "Publish succeeded" boundary. Anything thrown below this
176
109
  // point did NOT cause the publish to fail — the module is on
@@ -226,4 +159,149 @@ export class Publisher {
226
159
  throw error;
227
160
  }
228
161
  }
162
+ /**
163
+ * Publish via the Movement CLI (`movement move publish`). The default path
164
+ * for real Movement nodes, forks, and testnet. Returns the parsed tx hash.
165
+ */
166
+ async publishViaCli(config, safeDir, deployerAddress, namedAddrArgs) {
167
+ // Format the private key into AIP-80 shape so the Movement CLI
168
+ // doesn't emit its raw-hex deprecation warning. `formatPrivateKey`
169
+ // is idempotent for already-prefixed inputs.
170
+ const formattedPrivateKey = PrivateKey.formatPrivateKey(config.privateKey, PrivateKeyVariants.Ed25519);
171
+ // Move.toml is NOT mutated. All address overrides flow through
172
+ // the `--named-addresses` flag above, which Movement CLI applies
173
+ // during build + publish. Rewriting Move.toml on disk would risk
174
+ // leaving the user's file mutated if the process died before the
175
+ // restore step.
176
+ let publishOut = "";
177
+ let publishErr = "";
178
+ // Pass the private key to Movement CLI via a 0o600 temp file
179
+ // (`--private-key-file <path>`) and the on-chain address via
180
+ // `--sender-account <addr>`. This avoids the CLI's profile-yaml
181
+ // lookup chain entirely — no CWD / HOME / .aptos / .movement
182
+ // dance, no CLI-variant dependency.
183
+ const keyFilePath = writeTempKeyFile(formattedPrivateKey);
184
+ // Register a sync cleanup hook BEFORE invoking the CLI. If the
185
+ // user Ctrl+C's (or the process is SIGTERM'd) between the file
186
+ // write and our finally, the SIGINT handler iterates every
187
+ // registered callback and unlinks this deploy's key file
188
+ // synchronously so the private key never persists on disk after
189
+ // an abnormal exit. The signal-handler path uses the
190
+ // best-effort variant because the event loop is dead and we
191
+ // cannot logger.warning.
192
+ ensureSignalHandler();
193
+ const syncCleanup = () => removeKeyFileSyncBestEffort(keyFilePath);
194
+ cleanupCallbacks.add(syncCleanup);
195
+ try {
196
+ // Execute publish command. Private key reaches the CLI via the
197
+ // temp key file path (--private-key-file) — never on the
198
+ // command line — so it can't leak through `ps aux`. runCli's
199
+ // stdout/stderr redaction still applies as defense in depth
200
+ // for any `ed25519-priv-…` substring that surfaces in CLI
201
+ // output (Movement CLI sometimes echoes the key on error).
202
+ const publishResult = await withSpinner("Publishing to blockchain", () => runCli({
203
+ command: "movement",
204
+ args: [
205
+ "move",
206
+ "publish",
207
+ "--package-dir",
208
+ safeDir,
209
+ "--url",
210
+ config.rpc,
211
+ "--private-key-file",
212
+ keyFilePath,
213
+ "--sender-account",
214
+ deployerAddress,
215
+ "--assume-yes",
216
+ ...namedAddrArgs,
217
+ ],
218
+ timeoutMs: 120000, // 2 minutes for blockchain transactions
219
+ }, { adapter: this.deps.adapter }));
220
+ publishOut = publishResult.stdout;
221
+ publishErr = publishResult.stderr;
222
+ // Both stdout and stderr from the publish subprocess are gated
223
+ // behind isVerbose() — Movement CLI emits progress to both
224
+ // streams ("Compiling, may take a little while..."), so a
225
+ // visible stderr line is not by itself a failure signal. The
226
+ // surrounding withSpinner converts the runCli throw on real
227
+ // failure into the visible spinner.fail() output instead.
228
+ if (isVerbose() && publishOut)
229
+ logger.info(publishOut.trim(), 2);
230
+ if (isVerbose() && publishErr)
231
+ logger.info(publishErr.trim(), 2);
232
+ }
233
+ finally {
234
+ // Unlink the temp key file via the observable cleanup helper.
235
+ // ENOENT and other already-gone outcomes are benign (null).
236
+ // A non-null Error means the unlink failed AND the file still
237
+ // exists on disk — the private key would persist silently
238
+ // otherwise, so we emit a warning with the manual-cleanup
239
+ // hint. The SIGINT signal handler's sync callback below also
240
+ // tries to remove the same file; if SIGINT fires before this
241
+ // finally runs the file is gone and the next finally call
242
+ // sees ENOENT (benign).
243
+ const cleanupErr = removeKeyFile(keyFilePath);
244
+ if (cleanupErr) {
245
+ logger.warning(`Failed to remove temp key file '${keyFilePath}': ${cleanupErr.message}. ` +
246
+ `The file has mode 0o600 but should be removed manually: rm ${keyFilePath}`);
247
+ }
248
+ cleanupCallbacks.delete(syncCleanup);
249
+ }
250
+ // Extract transaction hash from output via the shared helper
251
+ // (`utils/parseCliOutput.ts`). Same regex pair as before; lifted
252
+ // for reuse by harness/codeObject.ts and harness/script.ts.
253
+ return parseTxHash(publishOut);
254
+ }
255
+ /**
256
+ * Publish the already-built package via the TypeScript SDK. Used when the
257
+ * backend is movelite. Reads the compiled artifacts the CLI build produced
258
+ * under `<dir>/build/<pkg>/`, submits a `0x1::code::publish_package_txn`,
259
+ * and waits for it. Returns the on-chain tx hash.
260
+ */
261
+ async publishViaSdk(input, safeDir) {
262
+ const aptos = input.aptos;
263
+ if (!aptos) {
264
+ throw new Error("sdkPublish requires an Aptos client");
265
+ }
266
+ const buildRoot = join(safeDir, "build");
267
+ // The root package's compiled output is the single directory under
268
+ // build/ that carries a package-metadata.bcs; dependency builds live
269
+ // in nested bytecode_modules/dependencies/ and have no metadata here.
270
+ const pkgDirs = existsSync(buildRoot)
271
+ ? readdirSync(buildRoot, { withFileTypes: true })
272
+ .filter((e) => e.isDirectory())
273
+ .map((e) => join(buildRoot, e.name))
274
+ .filter((d) => existsSync(join(d, "package-metadata.bcs")))
275
+ : [];
276
+ if (pkgDirs.length !== 1) {
277
+ throw new Error(`Expected exactly one compiled package under ${buildRoot}, found ${pkgDirs.length}.`);
278
+ }
279
+ const pkgDir = pkgDirs[0];
280
+ const metadataBytes = new Uint8Array(readFileSync(join(pkgDir, "package-metadata.bcs")));
281
+ const modulesDir = join(pkgDir, "bytecode_modules");
282
+ const moduleBytecode = readdirSync(modulesDir)
283
+ .filter((f) => f.endsWith(".mv"))
284
+ .sort()
285
+ .map((f) => new Uint8Array(readFileSync(join(modulesDir, f))));
286
+ if (moduleBytecode.length === 0) {
287
+ throw new Error(`No compiled modules (*.mv) found in ${modulesDir}`);
288
+ }
289
+ return withSpinner("Publishing to blockchain", async () => {
290
+ const tx = await aptos.publishPackageTransaction({
291
+ account: input.account.accountAddress,
292
+ metadataBytes,
293
+ moduleBytecode,
294
+ });
295
+ const senderAuth = aptos.transaction.sign({
296
+ signer: input.account,
297
+ transaction: tx,
298
+ });
299
+ const committed = await aptos.transaction.submit.simple({
300
+ transaction: tx,
301
+ senderAuthenticator: senderAuth,
302
+ });
303
+ await aptos.waitForTransaction({ transactionHash: committed.hash });
304
+ return committed.hash;
305
+ });
306
+ }
229
307
  }
@@ -96,6 +96,17 @@ export async function setupLocalTesting(options = {}) {
96
96
  };
97
97
  }
98
98
  }
99
+ /**
100
+ * Resolve whether to prefer movelite. Explicit `useMovelite` wins; otherwise
101
+ * the `MOVEHAT_USE_MOVELITE` env var acts as an override (`0` disables, `1`
102
+ * enables); default is enabled. Lets tooling force a backend without editing
103
+ * test code.
104
+ */
105
+ function resolveUseMovelite(useMovelite) {
106
+ if (useMovelite !== undefined)
107
+ return useMovelite;
108
+ return process.env.MOVEHAT_USE_MOVELITE !== "0";
109
+ }
99
110
  /**
100
111
  * Setup using local Movement node (full blockchain)
101
112
  */
@@ -112,7 +123,7 @@ async function setupWithLocalNode(options, accountLabels, autoFund, defaultBalan
112
123
  }
113
124
  nodeInfo = localNode.getNodeInfo();
114
125
  }
115
- else if (options.useMovelite !== false && findMoveliteBinary()) {
126
+ else if (resolveUseMovelite(options.useMovelite) && findMoveliteBinary()) {
116
127
  localNode = new MoveliteManager(findMoveliteBinary());
117
128
  nodeInfo = await localNode.start();
118
129
  ownsNode = true;
@@ -179,11 +190,14 @@ async function setupWithLocalNode(options, accountLabels, autoFund, defaultBalan
179
190
  logger.step(`Auto-deploying ${options.autoDeploy.length} module(s)...`);
180
191
  const previousRedeploy = process.env.MH_CLI_REDEPLOY;
181
192
  process.env.MH_CLI_REDEPLOY = 'true';
193
+ // movelite's REST responses can't drive the Movement CLI publish flow,
194
+ // so deploy through the TypeScript SDK when it is the spawned backend.
195
+ const sdkPublish = localNode instanceof MoveliteManager;
182
196
  try {
183
197
  for (const moduleName of options.autoDeploy) {
184
198
  try {
185
199
  logger.plain(` Deploying ${moduleName}...`);
186
- await runtime.deployContract(moduleName);
200
+ await runtime.deployContract(moduleName, { sdkPublish });
187
201
  logger.success(`${moduleName} deployed`, 2);
188
202
  }
189
203
  catch (error) {
package/dist/runtime.js CHANGED
@@ -64,6 +64,8 @@ export async function initRuntime(options = {}) {
64
64
  config,
65
65
  account,
66
66
  packageDir: options?.packageDir,
67
+ sdkPublish: options?.sdkPublish,
68
+ aptos,
67
69
  });
68
70
  };
69
71
  const getDeployment = (moduleName) => {
@@ -24,6 +24,12 @@ export interface MovehatRuntime {
24
24
  * leave this undefined so the default spawn-based adapter is used.
25
25
  */
26
26
  adapter?: ChildProcessAdapter;
27
+ /**
28
+ * Publish the compiled package via the TypeScript SDK instead of the
29
+ * Movement CLI. Internal: setupLocalTesting sets this when the backend
30
+ * is movelite, whose REST responses the Movement CLI cannot consume.
31
+ */
32
+ sdkPublish?: boolean;
27
33
  }) => Promise<DeploymentInfo>;
28
34
  getDeployment: (moduleName: string) => DeploymentInfo | null;
29
35
  getDeployments: () => Record<string, DeploymentInfo>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "movehat",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "type": "module",
5
5
  "description": "Hardhat-like development framework for Movement L1 smart contracts",
6
6
  "bin": {