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