movehat 0.2.7 → 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.
- package/dist/cli.js +1 -1
- package/dist/commands/fork/fund.js +3 -1
- package/dist/commands/fork/serve.js +10 -5
- package/dist/commands/test-move.js +6 -3
- package/dist/core/Publisher.d.ts +20 -1
- package/dist/core/Publisher.js +167 -89
- package/dist/fork/api.d.ts +1 -1
- package/dist/fork/api.js +9 -4
- package/dist/fork/manager.d.ts +2 -2
- package/dist/fork/manager.js +4 -8
- package/dist/fork/storage.js +5 -4
- package/dist/fork/test.d.ts +1 -1
- package/dist/fork/validation.d.ts +9 -0
- package/dist/fork/validation.js +88 -0
- package/dist/harness/Harness.d.ts +3 -2
- package/dist/harness/Harness.js +4 -1
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.js +1 -0
- package/dist/helpers/move-tests.js +8 -5
- package/dist/helpers/setupLocalTesting.d.ts +2 -2
- package/dist/helpers/setupLocalTesting.js +58 -21
- package/dist/helpers/version-check.js +4 -2
- package/dist/node/MoveliteManager.d.ts +18 -0
- package/dist/node/MoveliteManager.js +152 -0
- package/dist/node/NodeProvider.d.ts +9 -0
- package/dist/node/NodeProvider.js +1 -0
- package/dist/runtime.js +2 -0
- package/dist/templates/.mocharc.json +1 -0
- package/dist/templates/tests/Counter.test.ts +6 -9
- package/dist/templates/tests/setup.ts +34 -0
- package/dist/types/config.d.ts +2 -0
- package/dist/types/fork.d.ts +24 -0
- package/dist/types/runtime.d.ts +6 -0
- package/package.json +4 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
function isObject(v) {
|
|
2
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
3
|
+
}
|
|
4
|
+
function hasString(o, key) {
|
|
5
|
+
return typeof o[key] === "string";
|
|
6
|
+
}
|
|
7
|
+
export function assertLedgerInfo(v) {
|
|
8
|
+
if (!isObject(v) ||
|
|
9
|
+
typeof v.chain_id !== "number" ||
|
|
10
|
+
!hasString(v, "epoch") ||
|
|
11
|
+
!hasString(v, "ledger_version") ||
|
|
12
|
+
!hasString(v, "oldest_ledger_version") ||
|
|
13
|
+
!hasString(v, "ledger_timestamp") ||
|
|
14
|
+
!hasString(v, "node_role") ||
|
|
15
|
+
!hasString(v, "oldest_block_height") ||
|
|
16
|
+
!hasString(v, "block_height")) {
|
|
17
|
+
throw new Error("Invalid LedgerInfo: missing or incorrectly typed fields");
|
|
18
|
+
}
|
|
19
|
+
return v;
|
|
20
|
+
}
|
|
21
|
+
export function assertAccountData(v) {
|
|
22
|
+
if (!isObject(v) ||
|
|
23
|
+
!hasString(v, "sequence_number") ||
|
|
24
|
+
!hasString(v, "authentication_key")) {
|
|
25
|
+
throw new Error("Invalid AccountData: expected object with 'sequence_number' and 'authentication_key' strings");
|
|
26
|
+
}
|
|
27
|
+
return v;
|
|
28
|
+
}
|
|
29
|
+
export function assertAccountResource(v) {
|
|
30
|
+
if (!isObject(v) || !hasString(v, "type") || !("data" in v)) {
|
|
31
|
+
throw new Error("Invalid AccountResource: expected object with 'type' string and 'data' field");
|
|
32
|
+
}
|
|
33
|
+
return v;
|
|
34
|
+
}
|
|
35
|
+
export function assertAccountResourceArray(v) {
|
|
36
|
+
if (!Array.isArray(v)) {
|
|
37
|
+
throw new Error("Invalid resources response: expected array");
|
|
38
|
+
}
|
|
39
|
+
for (let i = 0; i < v.length; i++) {
|
|
40
|
+
if (!isObject(v[i]) || !hasString(v[i], "type") || !("data" in v[i])) {
|
|
41
|
+
throw new Error(`Invalid AccountResource at index ${i}: expected object with 'type' and 'data'`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return v;
|
|
45
|
+
}
|
|
46
|
+
export function assertForkMetadata(v) {
|
|
47
|
+
if (!isObject(v) ||
|
|
48
|
+
!hasString(v, "network") ||
|
|
49
|
+
!hasString(v, "nodeUrl") ||
|
|
50
|
+
typeof v.chainId !== "number" ||
|
|
51
|
+
!hasString(v, "ledgerVersion") ||
|
|
52
|
+
!hasString(v, "timestamp") ||
|
|
53
|
+
!hasString(v, "epoch") ||
|
|
54
|
+
!hasString(v, "blockHeight") ||
|
|
55
|
+
!hasString(v, "createdAt")) {
|
|
56
|
+
throw new Error("Invalid ForkMetadata: missing or incorrectly typed fields");
|
|
57
|
+
}
|
|
58
|
+
return v;
|
|
59
|
+
}
|
|
60
|
+
export function assertAccountState(v) {
|
|
61
|
+
if (!isObject(v) ||
|
|
62
|
+
!hasString(v, "sequenceNumber") ||
|
|
63
|
+
!hasString(v, "authenticationKey")) {
|
|
64
|
+
throw new Error("Invalid AccountState: expected object with 'sequenceNumber' and 'authenticationKey' strings");
|
|
65
|
+
}
|
|
66
|
+
return v;
|
|
67
|
+
}
|
|
68
|
+
export function assertAccountStateRecord(v) {
|
|
69
|
+
if (!isObject(v)) {
|
|
70
|
+
throw new Error("Invalid account state record: expected object");
|
|
71
|
+
}
|
|
72
|
+
for (const [key, val] of Object.entries(v)) {
|
|
73
|
+
if (!isObject(val) ||
|
|
74
|
+
!hasString(val, "sequenceNumber") ||
|
|
75
|
+
!hasString(val, "authenticationKey")) {
|
|
76
|
+
throw new Error(`Invalid AccountState for key "${key}": missing 'sequenceNumber' or 'authenticationKey'`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return v;
|
|
80
|
+
}
|
|
81
|
+
export function assertCoinStore(v) {
|
|
82
|
+
if (!isObject(v) ||
|
|
83
|
+
!isObject(v.coin) ||
|
|
84
|
+
!hasString(v.coin, "value")) {
|
|
85
|
+
throw new Error("Invalid CoinStore: expected object with 'coin.value' string");
|
|
86
|
+
}
|
|
87
|
+
return v;
|
|
88
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Account } from "@aptos-labs/ts-sdk";
|
|
2
2
|
import type { MovehatRuntime } from "../types/runtime.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { NodeProvider } from "../node/NodeProvider.js";
|
|
4
4
|
import type { ForkServer } from "../fork/server.js";
|
|
5
5
|
import type { ForkManager } from "../fork/manager.js";
|
|
6
6
|
import type { LocalTestOptions } from "../types/config.js";
|
|
@@ -50,12 +50,13 @@ export declare class Harness {
|
|
|
50
50
|
*/
|
|
51
51
|
readonly accounts: Readonly<Record<string, Account>>;
|
|
52
52
|
/** @internal */
|
|
53
|
-
readonly localNode?:
|
|
53
|
+
readonly localNode?: NodeProvider;
|
|
54
54
|
/** @internal */
|
|
55
55
|
readonly forkServer?: ForkServer;
|
|
56
56
|
/** @internal */
|
|
57
57
|
readonly forkManager?: ForkManager;
|
|
58
58
|
private _poisoned;
|
|
59
|
+
private readonly ownsLocalNode;
|
|
59
60
|
private constructor();
|
|
60
61
|
/** True once `cleanup()` has been awaited at least once. */
|
|
61
62
|
get poisoned(): boolean;
|
package/dist/harness/Harness.js
CHANGED
|
@@ -54,10 +54,12 @@ export class Harness {
|
|
|
54
54
|
/** @internal */
|
|
55
55
|
forkManager;
|
|
56
56
|
_poisoned = false;
|
|
57
|
+
ownsLocalNode;
|
|
57
58
|
constructor(init) {
|
|
58
59
|
this.mode = init.mode;
|
|
59
60
|
this.runtime = init.runtime;
|
|
60
61
|
this.accounts = init.runtime.accountManager.getLabeledAccounts();
|
|
62
|
+
this.ownsLocalNode = init.ownsLocalNode ?? true;
|
|
61
63
|
if (init.localNode)
|
|
62
64
|
this.localNode = init.localNode;
|
|
63
65
|
if (init.forkServer)
|
|
@@ -81,6 +83,7 @@ export class Harness {
|
|
|
81
83
|
const init = {
|
|
82
84
|
mode: "local",
|
|
83
85
|
runtime: ctx.runtime,
|
|
86
|
+
ownsLocalNode: !options.localNode,
|
|
84
87
|
};
|
|
85
88
|
if (ctx.localNode)
|
|
86
89
|
init.localNode = ctx.localNode;
|
|
@@ -156,7 +159,7 @@ export class Harness {
|
|
|
156
159
|
if (this._poisoned)
|
|
157
160
|
return;
|
|
158
161
|
this._poisoned = true;
|
|
159
|
-
if (this.localNode) {
|
|
162
|
+
if (this.localNode && this.ownsLocalNode) {
|
|
160
163
|
await this.localNode.stop().catch(() => { });
|
|
161
164
|
}
|
|
162
165
|
if (this.forkServer) {
|
package/dist/helpers/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export { AccountManager } from "../core/AccountManager.js";
|
|
|
11
11
|
export type { StoredAccount } from "../core/AccountManager.js";
|
|
12
12
|
export { LocalNodeManager } from "../node/LocalNodeManager.js";
|
|
13
13
|
export type { LocalNodeOptions, LocalNodeInfo } from "../node/LocalNodeManager.js";
|
|
14
|
+
export { MoveliteManager, findMoveliteBinary } from "../node/MoveliteManager.js";
|
|
14
15
|
export { setupLocalTesting } from "./setupLocalTesting.js";
|
|
15
16
|
export type { LocalTestingContext } from "./setupLocalTesting.js";
|
|
16
17
|
export { setupTestFixture, setupMinimalFixture, } from "./testFixtures.js";
|
package/dist/helpers/index.js
CHANGED
|
@@ -6,5 +6,6 @@ export { saveDeployment, loadDeployment, getAllDeployments, getDeployedAddress,
|
|
|
6
6
|
export { snapshot, getForkInfo, viewForkResource, compareForkState, listSnapshots, } from "../fork/test.js";
|
|
7
7
|
export { AccountManager } from "../core/AccountManager.js";
|
|
8
8
|
export { LocalNodeManager } from "../node/LocalNodeManager.js";
|
|
9
|
+
export { MoveliteManager, findMoveliteBinary } from "../node/MoveliteManager.js";
|
|
9
10
|
export { setupLocalTesting } from "./setupLocalTesting.js";
|
|
10
11
|
export { setupTestFixture, setupMinimalFixture, } from "./testFixtures.js";
|
|
@@ -2,6 +2,7 @@ import { existsSync } from "fs";
|
|
|
2
2
|
import { resolve } from "path";
|
|
3
3
|
import { loadUserConfig } from "../core/config.js";
|
|
4
4
|
import { runCli } from "../utils/runCli.js";
|
|
5
|
+
import { logger } from "../ui/index.js";
|
|
5
6
|
/**
|
|
6
7
|
* Run Move unit tests using Movement CLI
|
|
7
8
|
* @param options Test options including filter, warnings, and skip behavior
|
|
@@ -12,8 +13,9 @@ export async function runMoveTests(options = {}) {
|
|
|
12
13
|
const moveDir = resolve(process.cwd(), userConfig.moveDir || "./move");
|
|
13
14
|
if (!existsSync(moveDir)) {
|
|
14
15
|
if (options.skipIfMissing) {
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
logger.info("No Move directory found (./move not found)");
|
|
17
|
+
logger.plain(" Skipping Move tests...");
|
|
18
|
+
logger.newline();
|
|
17
19
|
return;
|
|
18
20
|
}
|
|
19
21
|
else {
|
|
@@ -42,12 +44,13 @@ export async function runMoveTests(options = {}) {
|
|
|
42
44
|
catch (error) {
|
|
43
45
|
// Spawn-time failure (ENOENT, etc.). The original code logged a
|
|
44
46
|
// Movement-CLI-install hint here; keep that.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
logger.error(`Failed to run Move tests: ${error.message}`);
|
|
48
|
+
logger.error(" Make sure Movement CLI is installed");
|
|
47
49
|
throw error;
|
|
48
50
|
}
|
|
49
51
|
if (result.exitCode === 0) {
|
|
50
|
-
|
|
52
|
+
logger.newline();
|
|
53
|
+
logger.success("Move tests passed");
|
|
51
54
|
return;
|
|
52
55
|
}
|
|
53
56
|
throw new Error("Move tests failed");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { MovehatRuntime } from "../types/runtime.js";
|
|
2
2
|
import { ForkManager } from "../fork/manager.js";
|
|
3
3
|
import { ForkServer } from "../fork/server.js";
|
|
4
|
-
import {
|
|
4
|
+
import type { NodeProvider } from "../node/NodeProvider.js";
|
|
5
5
|
import type { LocalTestOptions } from "../types/config.js";
|
|
6
6
|
/**
|
|
7
7
|
* Context returned by {@link setupLocalTesting}.
|
|
@@ -17,7 +17,7 @@ import type { LocalTestOptions } from "../types/config.js";
|
|
|
17
17
|
export interface LocalTestingContext {
|
|
18
18
|
runtime: MovehatRuntime;
|
|
19
19
|
/** @internal */
|
|
20
|
-
localNode?:
|
|
20
|
+
localNode?: NodeProvider;
|
|
21
21
|
/** @internal */
|
|
22
22
|
forkServer?: ForkServer;
|
|
23
23
|
/** @internal */
|
|
@@ -4,6 +4,7 @@ import { initRuntime } from "../runtime.js";
|
|
|
4
4
|
import { ForkManager } from "../fork/manager.js";
|
|
5
5
|
import { ForkServer } from "../fork/server.js";
|
|
6
6
|
import { LocalNodeManager } from "../node/LocalNodeManager.js";
|
|
7
|
+
import { MoveliteManager, findMoveliteBinary } from "../node/MoveliteManager.js";
|
|
7
8
|
import { AccountManager } from "../core/AccountManager.js";
|
|
8
9
|
import { logger } from "../ui/index.js";
|
|
9
10
|
const BUILTIN_FORK_RPCS = {
|
|
@@ -64,11 +65,13 @@ export async function setupLocalTesting(options = {}) {
|
|
|
64
65
|
logger.kv("Accounts", accountLabels.join(", "), 2);
|
|
65
66
|
logger.newline();
|
|
66
67
|
if (mode === 'local-node') {
|
|
67
|
-
const { runtime, localNode } = await setupWithLocalNode(options, accountLabels, autoFund, defaultBalance);
|
|
68
|
+
const { runtime, localNode, ownsNode } = await setupWithLocalNode(options, accountLabels, autoFund, defaultBalance);
|
|
68
69
|
return {
|
|
69
70
|
runtime,
|
|
70
71
|
localNode,
|
|
71
72
|
teardown: async () => {
|
|
73
|
+
if (!ownsNode)
|
|
74
|
+
return;
|
|
72
75
|
logger.newline();
|
|
73
76
|
logger.step("Stopping local testing environment...");
|
|
74
77
|
await localNode.stop();
|
|
@@ -93,25 +96,56 @@ export async function setupLocalTesting(options = {}) {
|
|
|
93
96
|
};
|
|
94
97
|
}
|
|
95
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
|
+
}
|
|
96
110
|
/**
|
|
97
111
|
* Setup using local Movement node (full blockchain)
|
|
98
112
|
*/
|
|
99
113
|
async function setupWithLocalNode(options, accountLabels, autoFund, defaultBalance) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
let localNode;
|
|
115
|
+
let ownsNode;
|
|
116
|
+
let nodeInfo;
|
|
117
|
+
if (options.localNode) {
|
|
118
|
+
localNode = options.localNode;
|
|
119
|
+
ownsNode = false;
|
|
120
|
+
if (!localNode.isRunning()) {
|
|
121
|
+
throw new Error("localNode was provided but isRunning() is false. " +
|
|
122
|
+
"Start the node before passing it to setupLocalTesting.");
|
|
123
|
+
}
|
|
124
|
+
nodeInfo = localNode.getNodeInfo();
|
|
125
|
+
}
|
|
126
|
+
else if (resolveUseMovelite(options.useMovelite) && findMoveliteBinary()) {
|
|
127
|
+
localNode = new MoveliteManager(findMoveliteBinary());
|
|
128
|
+
nodeInfo = await localNode.start();
|
|
129
|
+
ownsNode = true;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
const nodeTestDir = options.nodeTestDir || join(process.cwd(), ".movehat", "local-node");
|
|
133
|
+
const nodeForceRestart = options.nodeForceRestart !== false;
|
|
134
|
+
const nodeFaucetPort = options.nodeFaucetPort || 8081;
|
|
135
|
+
const nodeApiPort = options.nodeApiPort || 8080;
|
|
136
|
+
const nodeReadyPort = options.nodeReadyPort || 8070;
|
|
137
|
+
const nodeSilent = options.nodeSilent ?? false;
|
|
138
|
+
localNode = new LocalNodeManager({
|
|
139
|
+
testDir: nodeTestDir,
|
|
140
|
+
forceRestart: nodeForceRestart,
|
|
141
|
+
faucetPort: nodeFaucetPort,
|
|
142
|
+
apiPort: nodeApiPort,
|
|
143
|
+
readyPort: nodeReadyPort,
|
|
144
|
+
silent: nodeSilent,
|
|
145
|
+
});
|
|
146
|
+
ownsNode = true;
|
|
147
|
+
nodeInfo = await localNode.start();
|
|
148
|
+
}
|
|
115
149
|
// Once the node is up, every later step (account creation, funding,
|
|
116
150
|
// runtime init, autoDeploy) is fallible. If any of them throws we
|
|
117
151
|
// must stop the node we just started — otherwise the child process
|
|
@@ -156,11 +190,14 @@ async function setupWithLocalNode(options, accountLabels, autoFund, defaultBalan
|
|
|
156
190
|
logger.step(`Auto-deploying ${options.autoDeploy.length} module(s)...`);
|
|
157
191
|
const previousRedeploy = process.env.MH_CLI_REDEPLOY;
|
|
158
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;
|
|
159
196
|
try {
|
|
160
197
|
for (const moduleName of options.autoDeploy) {
|
|
161
198
|
try {
|
|
162
199
|
logger.plain(` Deploying ${moduleName}...`);
|
|
163
|
-
await runtime.deployContract(moduleName);
|
|
200
|
+
await runtime.deployContract(moduleName, { sdkPublish });
|
|
164
201
|
logger.success(`${moduleName} deployed`, 2);
|
|
165
202
|
}
|
|
166
203
|
catch (error) {
|
|
@@ -188,12 +225,12 @@ async function setupWithLocalNode(options, accountLabels, autoFund, defaultBalan
|
|
|
188
225
|
logger.plain(` Accounts: ${Array.from(accountLabels).join(", ")}`);
|
|
189
226
|
logger.plain(` Balance per account: ${defaultBalance / 100_000_000} MOVE`);
|
|
190
227
|
logger.newline();
|
|
191
|
-
return { runtime, localNode };
|
|
228
|
+
return { runtime, localNode, ownsNode };
|
|
192
229
|
}
|
|
193
230
|
catch (error) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
231
|
+
if (ownsNode) {
|
|
232
|
+
await localNode.stop().catch(() => { });
|
|
233
|
+
}
|
|
197
234
|
throw error;
|
|
198
235
|
}
|
|
199
236
|
}
|
|
@@ -3,7 +3,7 @@ import { join } from "path";
|
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { isNewerVersion } from "./semver-utils.js";
|
|
5
5
|
import { fetchLatestVersion } from "./npm-registry.js";
|
|
6
|
-
import { box, colors, formatCommand } from "../ui/index.js";
|
|
6
|
+
import { box, colors, formatCommand, logger } from "../ui/index.js";
|
|
7
7
|
const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
|
|
8
8
|
const CACHE_DIR = join(homedir(), ".movehat");
|
|
9
9
|
const CACHE_FILE = join(CACHE_DIR, "version-cache.json");
|
|
@@ -72,7 +72,9 @@ export function checkForUpdates(currentVersion, packageName) {
|
|
|
72
72
|
const updateMessage = box(`${colors.brandBright('Update Available!')}\n\n` +
|
|
73
73
|
`${currentVersion} ${colors.dim('→')} ${colors.success(cache.latestVersion)}\n\n` +
|
|
74
74
|
`${formatCommand('movehat update')}`, { borderColor: 'warning', padding: 1 });
|
|
75
|
-
|
|
75
|
+
logger.newline();
|
|
76
|
+
logger.warning(updateMessage);
|
|
77
|
+
logger.newline();
|
|
76
78
|
}
|
|
77
79
|
// Update cache in background if needed (doesn't block)
|
|
78
80
|
if (!cache || Date.now() - cache.lastChecked > CACHE_DURATION) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Account } from "@aptos-labs/ts-sdk";
|
|
2
|
+
import type { LocalNodeInfo } from "./LocalNodeManager.js";
|
|
3
|
+
export declare class MoveliteManager {
|
|
4
|
+
private process;
|
|
5
|
+
private port;
|
|
6
|
+
private killed;
|
|
7
|
+
private readonly binaryPath;
|
|
8
|
+
constructor(binaryPath: string, port?: number);
|
|
9
|
+
start(): Promise<LocalNodeInfo>;
|
|
10
|
+
private waitForReady;
|
|
11
|
+
fundAccount(address: string, amount: number): Promise<void>;
|
|
12
|
+
fundAccounts(accounts: Account[], balance: number): Promise<void>;
|
|
13
|
+
stop(): Promise<void>;
|
|
14
|
+
private isPortInUse;
|
|
15
|
+
isRunning(): boolean;
|
|
16
|
+
getNodeInfo(): LocalNodeInfo;
|
|
17
|
+
}
|
|
18
|
+
export declare function findMoveliteBinary(): string | null;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { execSync, spawn } from "child_process";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
import { logger } from "../ui/index.js";
|
|
6
|
+
export class MoveliteManager {
|
|
7
|
+
process = null;
|
|
8
|
+
port;
|
|
9
|
+
killed = false;
|
|
10
|
+
binaryPath;
|
|
11
|
+
constructor(binaryPath, port = 8090) {
|
|
12
|
+
this.binaryPath = binaryPath;
|
|
13
|
+
this.port = port;
|
|
14
|
+
}
|
|
15
|
+
async start() {
|
|
16
|
+
const binary = this.binaryPath;
|
|
17
|
+
if (!binary) {
|
|
18
|
+
throw new Error("movelite binary not found");
|
|
19
|
+
}
|
|
20
|
+
if (await this.isPortInUse(this.port)) {
|
|
21
|
+
this.port = this.port + 1;
|
|
22
|
+
if (await this.isPortInUse(this.port)) {
|
|
23
|
+
throw new Error(`Ports ${this.port - 1} and ${this.port} are in use`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
logger.step("Starting movelite...");
|
|
27
|
+
this.process = spawn(binary, ["start", "--port", String(this.port), "--no-auth"], {
|
|
28
|
+
stdio: "pipe",
|
|
29
|
+
detached: false,
|
|
30
|
+
});
|
|
31
|
+
this.process.on("exit", () => {
|
|
32
|
+
this.process = null;
|
|
33
|
+
});
|
|
34
|
+
await this.waitForReady();
|
|
35
|
+
logger.success(`movelite ready on port ${this.port}`);
|
|
36
|
+
return this.getNodeInfo();
|
|
37
|
+
}
|
|
38
|
+
async waitForReady() {
|
|
39
|
+
const url = `http://127.0.0.1:${this.port}/v1`;
|
|
40
|
+
const timeout = 15_000;
|
|
41
|
+
const start = Date.now();
|
|
42
|
+
while (Date.now() - start < timeout) {
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch(url);
|
|
45
|
+
if (res.ok)
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// not ready yet
|
|
50
|
+
}
|
|
51
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`movelite did not become ready within ${timeout}ms`);
|
|
54
|
+
}
|
|
55
|
+
async fundAccount(address, amount) {
|
|
56
|
+
const res = await fetch(`http://127.0.0.1:${this.port}/mint?address=${address}&amount=${amount}`, { method: "POST" });
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
throw new Error(`Failed to fund account: ${res.status}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async fundAccounts(accounts, balance) {
|
|
62
|
+
for (const account of accounts) {
|
|
63
|
+
await this.fundAccount(account.accountAddress.toString(), balance);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async stop() {
|
|
67
|
+
if (!this.process || this.killed)
|
|
68
|
+
return;
|
|
69
|
+
this.killed = true;
|
|
70
|
+
this.process.kill("SIGTERM");
|
|
71
|
+
await new Promise((resolve) => {
|
|
72
|
+
const timer = setTimeout(() => {
|
|
73
|
+
if (this.process)
|
|
74
|
+
this.process.kill("SIGKILL");
|
|
75
|
+
resolve();
|
|
76
|
+
}, 5_000);
|
|
77
|
+
if (this.process) {
|
|
78
|
+
this.process.on("exit", () => {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
resolve();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
resolve();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
this.process = null;
|
|
89
|
+
}
|
|
90
|
+
async isPortInUse(port) {
|
|
91
|
+
try {
|
|
92
|
+
const res = await fetch(`http://127.0.0.1:${port}/v1`);
|
|
93
|
+
return res.ok;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
isRunning() {
|
|
100
|
+
return this.process !== null && !this.killed;
|
|
101
|
+
}
|
|
102
|
+
getNodeInfo() {
|
|
103
|
+
return {
|
|
104
|
+
rpcUrl: `http://127.0.0.1:${this.port}`,
|
|
105
|
+
faucetUrl: `http://127.0.0.1:${this.port}`,
|
|
106
|
+
readyUrl: `http://127.0.0.1:${this.port}/v1`,
|
|
107
|
+
testDir: "",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export function findMoveliteBinary() {
|
|
112
|
+
if (process.env.MOVELITE_PATH) {
|
|
113
|
+
return existsSync(process.env.MOVELITE_PATH)
|
|
114
|
+
? process.env.MOVELITE_PATH
|
|
115
|
+
: null;
|
|
116
|
+
}
|
|
117
|
+
const platforms = {
|
|
118
|
+
"darwin-arm64": "movelite-darwin-arm64",
|
|
119
|
+
"darwin-x64": "movelite-darwin-x64",
|
|
120
|
+
"linux-x64": "movelite-linux-x64",
|
|
121
|
+
"linux-arm64": "movelite-linux-arm64",
|
|
122
|
+
};
|
|
123
|
+
const key = `${process.platform}-${process.arch}`;
|
|
124
|
+
const pkg = platforms[key];
|
|
125
|
+
if (pkg) {
|
|
126
|
+
try {
|
|
127
|
+
// Resolve through the `movelite` shim (movehat's direct dependency),
|
|
128
|
+
// then resolve the platform package from movelite's own context. The
|
|
129
|
+
// platform package is movelite's dependency, not movehat's, so a direct
|
|
130
|
+
// resolve from here fails under pnpm/yarn strict layouts.
|
|
131
|
+
const req = createRequire(import.meta.url);
|
|
132
|
+
const movelitePkg = req.resolve("movelite/package.json");
|
|
133
|
+
const moveliteReq = createRequire(movelitePkg);
|
|
134
|
+
const pkgPath = moveliteReq.resolve(`${pkg}/package.json`);
|
|
135
|
+
const binPath = join(pkgPath, "..", "bin", "movelite");
|
|
136
|
+
if (existsSync(binPath))
|
|
137
|
+
return binPath;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// package not installed
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const found = execSync("which movelite", { encoding: "utf-8" }).trim();
|
|
145
|
+
if (found && existsSync(found))
|
|
146
|
+
return found;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// not in PATH
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Account } from "@aptos-labs/ts-sdk";
|
|
2
|
+
import type { LocalNodeInfo } from "./LocalNodeManager.js";
|
|
3
|
+
export interface NodeProvider {
|
|
4
|
+
start(): Promise<LocalNodeInfo>;
|
|
5
|
+
stop(): Promise<void>;
|
|
6
|
+
isRunning(): boolean;
|
|
7
|
+
getNodeInfo(): LocalNodeInfo;
|
|
8
|
+
fundAccounts(accounts: Account[], balance: number): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/runtime.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { describe, it, before, after } from "mocha";
|
|
3
3
|
import { expect } from "chai";
|
|
4
4
|
import { Harness } from "movehat";
|
|
5
|
+
import { getSharedNode } from "./setup.js";
|
|
5
6
|
|
|
6
7
|
describe("Counter Contract", () => {
|
|
7
8
|
let harness;
|
|
@@ -9,16 +10,12 @@ describe("Counter Contract", () => {
|
|
|
9
10
|
let deployer, alice, bob;
|
|
10
11
|
|
|
11
12
|
before(async function () {
|
|
12
|
-
this.timeout(60000);
|
|
13
|
-
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// accounts from the local faucet, and (via autoDeploy) builds +
|
|
17
|
-
// publishes the named modules so they're ready to use.
|
|
18
|
-
//
|
|
19
|
-
// The setupTestFixture helper still works if you prefer the older
|
|
20
|
-
// pattern; see `import { setupTestFixture } from "movehat/helpers"`.
|
|
13
|
+
this.timeout(60000);
|
|
14
|
+
|
|
15
|
+
// Reuses the shared Movement node started by root hooks (tests/setup.ts).
|
|
16
|
+
// Each spec still gets its own accounts + deployments for isolation.
|
|
21
17
|
harness = await Harness.createLocal({
|
|
18
|
+
localNode: getSharedNode(),
|
|
22
19
|
accountLabels: ["deployer", "alice", "bob"],
|
|
23
20
|
autoDeploy: ["counter"],
|
|
24
21
|
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// @ts-nocheck - This is a template file, dependencies are installed in user projects
|
|
2
|
+
import { LocalNodeManager } from "movehat/helpers";
|
|
3
|
+
|
|
4
|
+
let sharedNode;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns the shared local Movement node started by root hooks.
|
|
8
|
+
* Pass the result as `{ localNode: getSharedNode() }` to
|
|
9
|
+
* Harness.createLocal() or setupTestFixture().
|
|
10
|
+
*/
|
|
11
|
+
export function getSharedNode() {
|
|
12
|
+
if (!sharedNode || !sharedNode.isRunning()) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
"Shared node not available. " +
|
|
15
|
+
'Ensure tests/setup.ts is listed in .mocharc.json under "require".'
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return sharedNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const mochaHooks = {
|
|
22
|
+
async beforeAll() {
|
|
23
|
+
this.timeout(60000);
|
|
24
|
+
sharedNode = new LocalNodeManager({ forceRestart: true });
|
|
25
|
+
await sharedNode.start();
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
async afterAll() {
|
|
29
|
+
if (sharedNode) {
|
|
30
|
+
await sharedNode.stop();
|
|
31
|
+
sharedNode = undefined;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
};
|
package/dist/types/config.d.ts
CHANGED
|
@@ -41,6 +41,8 @@ export type LocalTestingMode = 'local-node' | 'fork';
|
|
|
41
41
|
*/
|
|
42
42
|
export interface LocalTestOptions {
|
|
43
43
|
mode?: LocalTestingMode;
|
|
44
|
+
localNode?: import("../node/LocalNodeManager.js").LocalNodeManager;
|
|
45
|
+
useMovelite?: boolean;
|
|
44
46
|
nodeTestDir?: string;
|
|
45
47
|
nodeForceRestart?: boolean;
|
|
46
48
|
nodeFaucetPort?: number;
|