movehat 0.2.5 → 0.2.7
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/commands/run.d.ts +17 -0
- package/dist/commands/run.js +46 -32
- package/dist/core/AccountManager.d.ts +121 -139
- package/dist/core/AccountManager.js +217 -158
- package/dist/core/config.js +109 -27
- package/dist/harness/Harness.d.ts +32 -4
- package/dist/harness/Harness.js +32 -4
- package/dist/helpers/setup.js +6 -3
- package/dist/helpers/setupLocalTesting.js +12 -4
- package/dist/helpers/testFixtures.d.ts +5 -4
- package/dist/helpers/testFixtures.js +4 -5
- package/dist/index.d.ts +3 -1
- package/dist/runtime.d.ts +13 -0
- package/dist/runtime.js +8 -4
- package/dist/templates/tests/Counter.test.ts +6 -5
- package/dist/types/runtime.d.ts +2 -0
- package/dist/utils/parseCliOutput.d.ts +6 -3
- package/dist/utils/parseCliOutput.js +10 -5
- package/package.json +1 -1
package/dist/core/config.js
CHANGED
|
@@ -13,6 +13,45 @@ import { logger } from "../ui/index.js";
|
|
|
13
13
|
// same value here. No corruption, no in-flight-promise memoization
|
|
14
14
|
// needed.
|
|
15
15
|
const configCache = new Map();
|
|
16
|
+
// In-flight load deduplication (#47). When two callers hit a cold cache
|
|
17
|
+
// for the same config file concurrently, both would invoke
|
|
18
|
+
// `register()` from `tsx/esm/api` and race on the unregister cleanup.
|
|
19
|
+
// The dedup map ensures only ONE load runs per (path, mtime) burst —
|
|
20
|
+
// concurrent callers await the same Promise. Cleared after the load
|
|
21
|
+
// settles so a later edit (new mtime) can re-trigger a cold load.
|
|
22
|
+
const inFlightLoads = new Map();
|
|
23
|
+
// Hostnames recognized as safe targets for the deterministic test key
|
|
24
|
+
// auto-injection. Any URL whose hostname is not in this set causes the
|
|
25
|
+
// test-key path to be skipped even if the network NAME is 'testnet' or
|
|
26
|
+
// 'local' (#40 — name-only gating was spoofable when users named a
|
|
27
|
+
// production-pointing network 'testnet').
|
|
28
|
+
const TEST_ENDPOINT_HOSTS = new Set([
|
|
29
|
+
"testnet.movementnetwork.xyz",
|
|
30
|
+
"localhost",
|
|
31
|
+
"127.0.0.1",
|
|
32
|
+
"::1",
|
|
33
|
+
]);
|
|
34
|
+
function isKnownTestEndpoint(url) {
|
|
35
|
+
try {
|
|
36
|
+
return TEST_ENDPOINT_HOSTS.has(new URL(url).hostname.toLowerCase());
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Render a URL for log output without exposing userinfo (`user:pass@`)
|
|
43
|
+
// or query strings (`?apiKey=…`). Returns the protocol + host + pathname
|
|
44
|
+
// only, which is enough for the operator to identify the endpoint
|
|
45
|
+
// without leaking embedded credentials to CI logs.
|
|
46
|
+
function sanitizeUrlForLog(url) {
|
|
47
|
+
try {
|
|
48
|
+
const parsed = new URL(url);
|
|
49
|
+
return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return "<invalid-url>";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
16
55
|
/**
|
|
17
56
|
* Loads the user's movehat.config.{ts,js} from the current working directory.
|
|
18
57
|
*
|
|
@@ -47,32 +86,58 @@ export async function loadUserConfig() {
|
|
|
47
86
|
if (cached && cached.mtimeMs === mtimeMs) {
|
|
48
87
|
return cached.config;
|
|
49
88
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
89
|
+
// Dedup concurrent cold-cache loads of the same file (#47).
|
|
90
|
+
// Without this, two callers race on tsx's register/unregister and
|
|
91
|
+
// the second may run its `await import()` after the first calls
|
|
92
|
+
// unregister(), removing the loader mid-flight. All callers go
|
|
93
|
+
// through the same `return await loadPromise` below so the outer
|
|
94
|
+
// try/catch wraps a consistent error regardless of who started the
|
|
95
|
+
// load.
|
|
96
|
+
let loadPromise = inFlightLoads.get(configPath);
|
|
97
|
+
if (!loadPromise) {
|
|
98
|
+
loadPromise = doLoadConfig(configPath, mtimeMs);
|
|
99
|
+
inFlightLoads.set(configPath, loadPromise);
|
|
100
|
+
// Clean up after settles. `.catch(() => undefined)` after the
|
|
101
|
+
// finally chain swallows the cleanup-promise rejection so it
|
|
102
|
+
// does not become an unhandled rejection — the actual awaiters
|
|
103
|
+
// still see the original rejection via `return await loadPromise`.
|
|
104
|
+
void loadPromise
|
|
105
|
+
.finally(() => {
|
|
106
|
+
if (inFlightLoads.get(configPath) === loadPromise) {
|
|
107
|
+
inFlightLoads.delete(configPath);
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
.catch(() => undefined);
|
|
61
111
|
}
|
|
62
|
-
|
|
112
|
+
return await loadPromise;
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
throw new Error(`Failed to load configuration file '${configPath}': ${error}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function doLoadConfig(configPath, mtimeMs) {
|
|
119
|
+
let configModule;
|
|
120
|
+
if (configPath.endsWith('.ts')) {
|
|
121
|
+
const { register } = await import('tsx/esm/api');
|
|
122
|
+
const unregister = register();
|
|
123
|
+
try {
|
|
63
124
|
const configUrl = pathToFileURL(configPath).href;
|
|
64
125
|
configModule = await import(configUrl + '?mtime=' + mtimeMs);
|
|
65
126
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
throw new Error("No networks defined in configuration. Add at least one network in the 'networks' field.");
|
|
127
|
+
finally {
|
|
128
|
+
unregister();
|
|
69
129
|
}
|
|
70
|
-
configCache.set(configPath, { mtimeMs, config: userConfig });
|
|
71
|
-
return userConfig;
|
|
72
130
|
}
|
|
73
|
-
|
|
74
|
-
|
|
131
|
+
else {
|
|
132
|
+
const configUrl = pathToFileURL(configPath).href;
|
|
133
|
+
configModule = await import(configUrl + '?mtime=' + mtimeMs);
|
|
75
134
|
}
|
|
135
|
+
const userConfig = configModule.default;
|
|
136
|
+
if (!userConfig.networks || Object.keys(userConfig.networks).length === 0) {
|
|
137
|
+
throw new Error("No networks defined in configuration. Add at least one network in the 'networks' field.");
|
|
138
|
+
}
|
|
139
|
+
configCache.set(configPath, { mtimeMs, config: userConfig });
|
|
140
|
+
return userConfig;
|
|
76
141
|
}
|
|
77
142
|
/**
|
|
78
143
|
* Clear the in-memory config cache. Test-only escape hatch.
|
|
@@ -82,6 +147,7 @@ export async function loadUserConfig() {
|
|
|
82
147
|
*/
|
|
83
148
|
export function _resetConfigCache() {
|
|
84
149
|
configCache.clear();
|
|
150
|
+
inFlightLoads.clear();
|
|
85
151
|
}
|
|
86
152
|
/**
|
|
87
153
|
* Resolve configuration for a specific network
|
|
@@ -136,15 +202,30 @@ export async function resolveNetworkConfig(userConfig, networkName) {
|
|
|
136
202
|
if (accounts.length === 0 && process.env.PRIVATE_KEY) {
|
|
137
203
|
accounts = [process.env.PRIVATE_KEY];
|
|
138
204
|
}
|
|
139
|
-
// 4. Validate we have at least one account (unless using testnet/local
|
|
205
|
+
// 4. Validate we have at least one account (unless using testnet/local
|
|
206
|
+
// AND the URL is a recognized test endpoint — name alone is not enough
|
|
207
|
+
// per #40, since a user can name a network 'testnet' but point it at
|
|
208
|
+
// a production RPC and would otherwise inherit the deterministic test
|
|
209
|
+
// key with a production endpoint).
|
|
140
210
|
if (accounts.length === 0 || !accounts[0]) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
211
|
+
const isTestNetworkName = selectedNetwork === "testnet" || selectedNetwork === "local";
|
|
212
|
+
const isTestEndpoint = isTestNetworkName && isKnownTestEndpoint(networkConfig.url);
|
|
213
|
+
if (isTestNetworkName && !isTestEndpoint) {
|
|
214
|
+
// Name matches the reserved convention but URL is not a recognized
|
|
215
|
+
// test endpoint — block the deterministic test-key injection. Falls
|
|
216
|
+
// through to the standard "no accounts" throw below so the user gets
|
|
217
|
+
// actionable guidance.
|
|
218
|
+
logger.warning(`Network '${selectedNetwork}' uses a name reserved for testnet/local ` +
|
|
219
|
+
`but '${sanitizeUrlForLog(networkConfig.url)}' is not a recognized test endpoint. ` +
|
|
220
|
+
`Skipping auto-injection of the deterministic test key to protect ` +
|
|
221
|
+
`against accidental production use. Set PRIVATE_KEY explicitly, ` +
|
|
222
|
+
`configure 'accounts' in movehat.config.ts, or rename this network.`);
|
|
223
|
+
}
|
|
224
|
+
if (isTestEndpoint) {
|
|
145
225
|
// Security: Using a deterministic test account (like Hardhat's default accounts)
|
|
146
226
|
// This is SAFE because:
|
|
147
|
-
// 1. Only used for testnet/local
|
|
227
|
+
// 1. Only used for testnet/local against a known test endpoint
|
|
228
|
+
// (network NAME + URL allowlist enforced above; bypassed otherwise)
|
|
148
229
|
// 2. Perfect for transaction simulation (no real funds)
|
|
149
230
|
// 3. Deterministic = consistent test results
|
|
150
231
|
const testPrivateKey = "0x0000000000000000000000000000000000000000000000000000000000000001";
|
|
@@ -155,8 +236,9 @@ export async function resolveNetworkConfig(userConfig, networkName) {
|
|
|
155
236
|
logger.newline();
|
|
156
237
|
}
|
|
157
238
|
else {
|
|
158
|
-
// For any other network (
|
|
159
|
-
//
|
|
239
|
+
// For any other network (mainnet, or testnet/local with a non-test
|
|
240
|
+
// URL), REQUIRE explicit configuration. This prevents accidentally
|
|
241
|
+
// using the test key on production networks.
|
|
160
242
|
throw new Error(`Network '${selectedNetwork}' has no accounts configured.\n` +
|
|
161
243
|
`\n` +
|
|
162
244
|
`SECURITY: This network requires explicit account configuration.\n` +
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Account } from "@aptos-labs/ts-sdk";
|
|
1
2
|
import type { MovehatRuntime } from "../types/runtime.js";
|
|
2
3
|
import type { LocalNodeManager } from "../node/LocalNodeManager.js";
|
|
3
4
|
import type { ForkServer } from "../fork/server.js";
|
|
@@ -13,14 +14,41 @@ export type HarnessMode = "local" | "fork" | "live";
|
|
|
13
14
|
* a Proxy that synchronously throws {@link HarnessDisposedError} on any
|
|
14
15
|
* post-`cleanup()` call to one of the deployment / script / view methods.
|
|
15
16
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* Account isolation: each Harness owns a per-instance `AccountManager`
|
|
18
|
+
* (see `core/AccountManager.ts`) reachable at `harness.runtime.accountManager`.
|
|
19
|
+
* Two Harness instances in the same process have independent account
|
|
20
|
+
* pools and label maps — `Harness.createLocal({ accountLabels: ["alice"] })`
|
|
21
|
+
* twice produces two DIFFERENT alice accounts.
|
|
22
|
+
*
|
|
23
|
+
* Labeled accounts created during construction (via `accountLabels` in
|
|
24
|
+
* `createLocal`) are snapshotted onto `harness.accounts` at construction
|
|
25
|
+
* time. Late additions via `harness.runtime.accountManager.createAccount(...)`
|
|
26
|
+
* are NOT reflected in the `accounts` Record — that's a snapshot, not a
|
|
27
|
+
* live view. For live mode (`Harness.createLive`), `accounts` is `{}`
|
|
28
|
+
* because the live factory does not create labeled accounts; reach into
|
|
29
|
+
* `harness.runtime.accountManager.*` for advanced operations.
|
|
30
|
+
*
|
|
31
|
+
* The `AccountManager` class-static methods remain available in 0.2.x
|
|
32
|
+
* for backward compatibility (deprecation warning lands in 0.2.7,
|
|
33
|
+
* removal in 0.3.0 — see #270). New code should prefer
|
|
34
|
+
* `harness.accounts.<label>` for the common read path.
|
|
20
35
|
*/
|
|
21
36
|
export declare class Harness {
|
|
22
37
|
readonly mode: HarnessMode;
|
|
23
38
|
readonly runtime: MovehatRuntime;
|
|
39
|
+
/**
|
|
40
|
+
* Labeled accounts snapshotted from `runtime.accountManager` at
|
|
41
|
+
* construction time. For `createLocal`, this is populated from the
|
|
42
|
+
* `accountLabels` option. For `createFork`, populated the same way.
|
|
43
|
+
* For `createLive`, empty (live mode does not create labeled accounts;
|
|
44
|
+
* use `harness.runtime.accountManager.loadAccountFromPrivateKey(...)`
|
|
45
|
+
* for direct key loading).
|
|
46
|
+
*
|
|
47
|
+
* Typed `Readonly` so the snapshot can't be mutated via
|
|
48
|
+
* `harness.accounts.alice = X`. Reach into
|
|
49
|
+
* `harness.runtime.accountManager.*` when you need mutable access.
|
|
50
|
+
*/
|
|
51
|
+
readonly accounts: Readonly<Record<string, Account>>;
|
|
24
52
|
/** @internal */
|
|
25
53
|
readonly localNode?: LocalNodeManager;
|
|
26
54
|
/** @internal */
|
package/dist/harness/Harness.js
CHANGED
|
@@ -12,14 +12,41 @@ import { runMoveScript } from "./script.js";
|
|
|
12
12
|
* a Proxy that synchronously throws {@link HarnessDisposedError} on any
|
|
13
13
|
* post-`cleanup()` call to one of the deployment / script / view methods.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* Account isolation: each Harness owns a per-instance `AccountManager`
|
|
16
|
+
* (see `core/AccountManager.ts`) reachable at `harness.runtime.accountManager`.
|
|
17
|
+
* Two Harness instances in the same process have independent account
|
|
18
|
+
* pools and label maps — `Harness.createLocal({ accountLabels: ["alice"] })`
|
|
19
|
+
* twice produces two DIFFERENT alice accounts.
|
|
20
|
+
*
|
|
21
|
+
* Labeled accounts created during construction (via `accountLabels` in
|
|
22
|
+
* `createLocal`) are snapshotted onto `harness.accounts` at construction
|
|
23
|
+
* time. Late additions via `harness.runtime.accountManager.createAccount(...)`
|
|
24
|
+
* are NOT reflected in the `accounts` Record — that's a snapshot, not a
|
|
25
|
+
* live view. For live mode (`Harness.createLive`), `accounts` is `{}`
|
|
26
|
+
* because the live factory does not create labeled accounts; reach into
|
|
27
|
+
* `harness.runtime.accountManager.*` for advanced operations.
|
|
28
|
+
*
|
|
29
|
+
* The `AccountManager` class-static methods remain available in 0.2.x
|
|
30
|
+
* for backward compatibility (deprecation warning lands in 0.2.7,
|
|
31
|
+
* removal in 0.3.0 — see #270). New code should prefer
|
|
32
|
+
* `harness.accounts.<label>` for the common read path.
|
|
19
33
|
*/
|
|
20
34
|
export class Harness {
|
|
21
35
|
mode;
|
|
22
36
|
runtime;
|
|
37
|
+
/**
|
|
38
|
+
* Labeled accounts snapshotted from `runtime.accountManager` at
|
|
39
|
+
* construction time. For `createLocal`, this is populated from the
|
|
40
|
+
* `accountLabels` option. For `createFork`, populated the same way.
|
|
41
|
+
* For `createLive`, empty (live mode does not create labeled accounts;
|
|
42
|
+
* use `harness.runtime.accountManager.loadAccountFromPrivateKey(...)`
|
|
43
|
+
* for direct key loading).
|
|
44
|
+
*
|
|
45
|
+
* Typed `Readonly` so the snapshot can't be mutated via
|
|
46
|
+
* `harness.accounts.alice = X`. Reach into
|
|
47
|
+
* `harness.runtime.accountManager.*` when you need mutable access.
|
|
48
|
+
*/
|
|
49
|
+
accounts;
|
|
23
50
|
/** @internal */
|
|
24
51
|
localNode;
|
|
25
52
|
/** @internal */
|
|
@@ -30,6 +57,7 @@ export class Harness {
|
|
|
30
57
|
constructor(init) {
|
|
31
58
|
this.mode = init.mode;
|
|
32
59
|
this.runtime = init.runtime;
|
|
60
|
+
this.accounts = init.runtime.accountManager.getLabeledAccounts();
|
|
33
61
|
if (init.localNode)
|
|
34
62
|
this.localNode = init.localNode;
|
|
35
63
|
if (init.forkServer)
|
package/dist/helpers/setup.js
CHANGED
|
@@ -14,8 +14,10 @@ export async function setupTestEnvironment(networkName) {
|
|
|
14
14
|
fullnode: config.rpc,
|
|
15
15
|
});
|
|
16
16
|
const aptos = new Aptos(aptosConfig);
|
|
17
|
-
// Load account using AccountManager
|
|
18
|
-
|
|
17
|
+
// Load account using a throwaway AccountManager instance. This helper
|
|
18
|
+
// returns Aptos+Account directly (no runtime exposure), so a local
|
|
19
|
+
// instance suffices — no shared pool needed.
|
|
20
|
+
const account = new AccountManager().loadAccountFromPrivateKey(config.privateKey);
|
|
19
21
|
logger.success("Test environment ready");
|
|
20
22
|
logger.plain(` Account: ${account.accountAddress.toString()}`);
|
|
21
23
|
logger.plain(` Network: ${config.network}`);
|
|
@@ -28,5 +30,6 @@ export async function setupTestEnvironment(networkName) {
|
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
32
|
export function createTestAccount() {
|
|
31
|
-
|
|
33
|
+
// Throwaway instance — see setupTestEnvironment for rationale.
|
|
34
|
+
return new AccountManager().createAccount();
|
|
32
35
|
}
|
|
@@ -118,8 +118,12 @@ async function setupWithLocalNode(options, accountLabels, autoFund, defaultBalan
|
|
|
118
118
|
// leaks and port 8080 stays bound until the OS reaps it (manifests as
|
|
119
119
|
// "Movement command failed" on the next test:example run).
|
|
120
120
|
try {
|
|
121
|
+
// Per-context AccountManager. Threaded into initRuntime below so
|
|
122
|
+
// the runtime exposes the SAME instance — the labels created by
|
|
123
|
+
// createBatch here are visible on `runtime.accountManager`.
|
|
124
|
+
const accountManager = new AccountManager();
|
|
121
125
|
logger.step(`Generating ${accountLabels.length} test accounts...`);
|
|
122
|
-
const accounts =
|
|
126
|
+
const accounts = accountManager.createBatch(accountLabels);
|
|
123
127
|
for (const [label, account] of Object.entries(accounts)) {
|
|
124
128
|
logger.plain(` ${label}: ${account.accountAddress.toString()}`);
|
|
125
129
|
}
|
|
@@ -129,12 +133,13 @@ async function setupWithLocalNode(options, accountLabels, autoFund, defaultBalan
|
|
|
129
133
|
await localNode.fundAccounts(accountsList, defaultBalance);
|
|
130
134
|
}
|
|
131
135
|
logger.step("Initializing runtime for local network...");
|
|
132
|
-
const deployerPrivateKey =
|
|
136
|
+
const deployerPrivateKey = accountManager.exportPrivateKeys(["deployer"]).deployer;
|
|
133
137
|
if (!deployerPrivateKey) {
|
|
134
138
|
throw new Error("Failed to get deployer private key");
|
|
135
139
|
}
|
|
136
140
|
const runtime = await initRuntime({
|
|
137
141
|
network: "local",
|
|
142
|
+
accountManager,
|
|
138
143
|
configOverride: {
|
|
139
144
|
networks: {
|
|
140
145
|
local: {
|
|
@@ -256,8 +261,10 @@ async function setupWithFork(options, accountLabels, autoFund, defaultBalance) {
|
|
|
256
261
|
// we leak the listener.
|
|
257
262
|
try {
|
|
258
263
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
264
|
+
// Per-context AccountManager (mirror of setupWithLocalNode pattern).
|
|
265
|
+
const accountManager = new AccountManager();
|
|
259
266
|
logger.step(`Generating ${accountLabels.length} test accounts...`);
|
|
260
|
-
const accounts =
|
|
267
|
+
const accounts = accountManager.createBatch(accountLabels);
|
|
261
268
|
for (const [label, account] of Object.entries(accounts)) {
|
|
262
269
|
logger.plain(` ${label}: ${account.accountAddress.toString()}`);
|
|
263
270
|
}
|
|
@@ -267,12 +274,13 @@ async function setupWithFork(options, accountLabels, autoFund, defaultBalance) {
|
|
|
267
274
|
await forkManager.fundMultipleAccounts(addresses, defaultBalance);
|
|
268
275
|
}
|
|
269
276
|
logger.step("Initializing runtime for local network...");
|
|
270
|
-
const deployerPrivateKey =
|
|
277
|
+
const deployerPrivateKey = accountManager.exportPrivateKeys(["deployer"]).deployer;
|
|
271
278
|
if (!deployerPrivateKey) {
|
|
272
279
|
throw new Error("Failed to get deployer private key");
|
|
273
280
|
}
|
|
274
281
|
const runtime = await initRuntime({
|
|
275
282
|
network: "local",
|
|
283
|
+
accountManager,
|
|
276
284
|
configOverride: {
|
|
277
285
|
networks: {
|
|
278
286
|
local: {
|
|
@@ -22,10 +22,11 @@ export interface TestFixture<TModules extends string = string> {
|
|
|
22
22
|
/**
|
|
23
23
|
* Stop the local node / fork server this fixture started.
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* Each fixture owns its own `AccountManager` instance (via
|
|
26
|
+
* `setupLocalTesting` → `initRuntime`), so there is no shared pool
|
|
27
|
+
* to clear — accounts are garbage-collected when the fixture goes
|
|
28
|
+
* out of scope. Parallel `setupTestFixture` invocations have fully
|
|
29
|
+
* isolated label maps and pools (M9.2, #270).
|
|
29
30
|
*/
|
|
30
31
|
teardown: () => Promise<void>;
|
|
31
32
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { AccountManager } from "../core/AccountManager.js";
|
|
2
1
|
import { setupLocalTesting } from "./setupLocalTesting.js";
|
|
3
2
|
import { logger } from "../ui/index.js";
|
|
4
3
|
/**
|
|
@@ -59,7 +58,7 @@ export async function setupTestFixture(modules, accountLabels = ["alice", "bob"]
|
|
|
59
58
|
// the original assembly error unchanged.
|
|
60
59
|
try {
|
|
61
60
|
const mh = ctx.runtime;
|
|
62
|
-
const labeledAccounts =
|
|
61
|
+
const labeledAccounts = mh.accountManager.getLabeledAccounts();
|
|
63
62
|
// any: TestFixture.accounts has a structural shape with required
|
|
64
63
|
// `deployer/alice/bob` plus a `[key: string]: Account` index. The
|
|
65
64
|
// builder fills the index dynamically; typing this as the exact
|
|
@@ -72,7 +71,7 @@ export async function setupTestFixture(modules, accountLabels = ["alice", "bob"]
|
|
|
72
71
|
deployer: labeledAccounts.deployer,
|
|
73
72
|
};
|
|
74
73
|
for (const label of accountLabels) {
|
|
75
|
-
accounts[label] = labeledAccounts[label] ||
|
|
74
|
+
accounts[label] = labeledAccounts[label] || mh.accountManager.getOrCreateLabeled(label);
|
|
76
75
|
}
|
|
77
76
|
const contracts = {};
|
|
78
77
|
for (const moduleName of modules) {
|
|
@@ -119,14 +118,14 @@ export async function setupMinimalFixture(accountLabels = ["alice", "bob"], opti
|
|
|
119
118
|
// Same teardown-on-assembly-failure pattern as setupTestFixture.
|
|
120
119
|
try {
|
|
121
120
|
const mh = ctx.runtime;
|
|
122
|
-
const labeledAccounts =
|
|
121
|
+
const labeledAccounts = mh.accountManager.getLabeledAccounts();
|
|
123
122
|
// any: see setupTestFixture above — same dynamic-key builder pattern.
|
|
124
123
|
const accounts = {
|
|
125
124
|
// non-null: deployer is unconditionally added to allLabels at L156 above.
|
|
126
125
|
deployer: labeledAccounts.deployer,
|
|
127
126
|
};
|
|
128
127
|
for (const label of accountLabels) {
|
|
129
|
-
accounts[label] = labeledAccounts[label] ||
|
|
128
|
+
accounts[label] = labeledAccounts[label] || mh.accountManager.getOrCreateLabeled(label);
|
|
130
129
|
}
|
|
131
130
|
logger.newline();
|
|
132
131
|
logger.success("Minimal fixture ready!");
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export * from "./helpers/index.js";
|
|
2
|
-
export type { MovehatConfig } from "./types/config.js";
|
|
2
|
+
export type { MovehatConfig, NetworkConfig, LocalTestingMode, } from "./types/config.js";
|
|
3
3
|
export { initRuntime } from "./runtime.js";
|
|
4
|
+
export type { InitRuntimeOptions } from "./runtime.js";
|
|
4
5
|
export type { MovehatRuntime, NetworkInfo } from "./types/runtime.js";
|
|
6
|
+
export type { ChildProcessAdapter } from "./utils/childProcessAdapter.js";
|
|
5
7
|
export { ForkManager } from "./fork/manager.js";
|
|
6
8
|
export { MovementApiClient } from "./fork/api.js";
|
|
7
9
|
export { ForkStorage } from "./fork/storage.js";
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { MovehatRuntime } from "./types/runtime.js";
|
|
2
2
|
import { MovehatUserConfig } from "./types/config.js";
|
|
3
|
+
import { AccountManager } from "./core/AccountManager.js";
|
|
3
4
|
export interface InitRuntimeOptions {
|
|
4
5
|
network?: string;
|
|
5
6
|
accountIndex?: number;
|
|
6
7
|
configOverride?: Partial<MovehatUserConfig>;
|
|
8
|
+
/**
|
|
9
|
+
* Optional pre-constructed `AccountManager` instance. When supplied,
|
|
10
|
+
* the returned runtime's `accountManager` field is THIS instance —
|
|
11
|
+
* so labeled accounts created BEFORE `initRuntime` are visible on
|
|
12
|
+
* the runtime. When omitted, `initRuntime` constructs a fresh
|
|
13
|
+
* `new AccountManager()` per call.
|
|
14
|
+
*
|
|
15
|
+
* `setupLocalTesting` uses this option to thread one manager through
|
|
16
|
+
* `createBatch` → `exportPrivateKeys` → `initRuntime` so the deployer
|
|
17
|
+
* key it extracts ends up on the same manager the runtime exposes.
|
|
18
|
+
*/
|
|
19
|
+
accountManager?: AccountManager;
|
|
7
20
|
}
|
|
8
21
|
/**
|
|
9
22
|
* Initialize the Movehat Runtime Environment.
|
package/dist/runtime.js
CHANGED
|
@@ -35,9 +35,12 @@ export async function initRuntime(options = {}) {
|
|
|
35
35
|
fullnode: config.rpc,
|
|
36
36
|
});
|
|
37
37
|
const aptos = new Aptos(aptosConfig);
|
|
38
|
-
// Setup accounts using AccountManager
|
|
38
|
+
// Setup accounts using AccountManager. Use the caller-supplied
|
|
39
|
+
// instance when threaded in (preserves labels created before
|
|
40
|
+
// initRuntime); otherwise construct a fresh one.
|
|
41
|
+
const accountManager = options.accountManager ?? new AccountManager();
|
|
39
42
|
const accountIndex = options.accountIndex || 0;
|
|
40
|
-
const accounts =
|
|
43
|
+
const accounts = accountManager.loadAccountsFromConfig(config);
|
|
41
44
|
// Primary account (accounts[0] or selected index)
|
|
42
45
|
const account = accounts[accountIndex];
|
|
43
46
|
if (!account) {
|
|
@@ -73,10 +76,10 @@ export async function initRuntime(options = {}) {
|
|
|
73
76
|
return getDeployedAddress(config.network, moduleName);
|
|
74
77
|
};
|
|
75
78
|
const createAccount = () => {
|
|
76
|
-
return
|
|
79
|
+
return accountManager.createAccount();
|
|
77
80
|
};
|
|
78
81
|
const getAccountHelper = (privateKeyHex) => {
|
|
79
|
-
return
|
|
82
|
+
return accountManager.loadAccountFromPrivateKey(privateKeyHex);
|
|
80
83
|
};
|
|
81
84
|
const getAccountByIndex = (index) => {
|
|
82
85
|
const acc = accounts[index];
|
|
@@ -95,6 +98,7 @@ export async function initRuntime(options = {}) {
|
|
|
95
98
|
aptos,
|
|
96
99
|
account,
|
|
97
100
|
accounts,
|
|
101
|
+
accountManager,
|
|
98
102
|
getContract: getContractHelper,
|
|
99
103
|
deployContract,
|
|
100
104
|
getDeployment,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-nocheck - This is a template file, dependencies are installed in user projects
|
|
2
2
|
import { describe, it, before, after } from "mocha";
|
|
3
3
|
import { expect } from "chai";
|
|
4
|
-
import { Harness
|
|
4
|
+
import { Harness } from "movehat";
|
|
5
5
|
|
|
6
6
|
describe("Counter Contract", () => {
|
|
7
7
|
let harness;
|
|
@@ -23,10 +23,11 @@ describe("Counter Contract", () => {
|
|
|
23
23
|
autoDeploy: ["counter"],
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
alice
|
|
29
|
-
|
|
26
|
+
// harness.accounts is a snapshot of the labeled accounts created
|
|
27
|
+
// during Harness construction. Two `Harness.createLocal({ accountLabels:
|
|
28
|
+
// ["alice"] })` calls in the same process now produce DIFFERENT
|
|
29
|
+
// alice accounts (per-Harness isolation introduced in 0.2.7).
|
|
30
|
+
({ deployer, alice, bob } = harness.accounts);
|
|
30
31
|
|
|
31
32
|
const counterAddr = harness.runtime.getDeploymentAddress("counter");
|
|
32
33
|
counter = harness.runtime.getContract(counterAddr, "counter");
|
package/dist/types/runtime.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Aptos, Account } from "@aptos-labs/ts-sdk";
|
|
|
2
2
|
import { MovehatConfig } from "./config.js";
|
|
3
3
|
import { MoveContract } from "../core/contract.js";
|
|
4
4
|
import { DeploymentInfo } from "../core/deployments.js";
|
|
5
|
+
import type { AccountManager } from "../core/AccountManager.js";
|
|
5
6
|
import type { ChildProcessAdapter } from "../utils/childProcessAdapter.js";
|
|
6
7
|
export interface NetworkInfo {
|
|
7
8
|
name: string;
|
|
@@ -14,6 +15,7 @@ export interface MovehatRuntime {
|
|
|
14
15
|
aptos: Aptos;
|
|
15
16
|
account: Account;
|
|
16
17
|
accounts: Account[];
|
|
18
|
+
accountManager: AccountManager;
|
|
17
19
|
getContract: (address: string, moduleName: string) => MoveContract;
|
|
18
20
|
deployContract: (moduleName: string, options?: {
|
|
19
21
|
packageDir?: string;
|
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
/**
|
|
7
7
|
* Extract the transaction hash from a `movement` CLI subcommand's stdout.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* `txn hash: 0x…`, `hash: 0x…`)
|
|
11
|
-
*
|
|
9
|
+
* Only the context-bearing pattern is accepted (`transaction hash: 0x…`,
|
|
10
|
+
* `txn hash: 0x…`, `hash: 0x…`). When the context is absent we log a
|
|
11
|
+
* warning and return `undefined` so callers decide whether to throw —
|
|
12
|
+
* the previous behavior of falling back to "any 64-hex literal" was
|
|
13
|
+
* fragile: a padded module address or state root printed before the
|
|
14
|
+
* actual txhash would silently corrupt the cached deployment record.
|
|
12
15
|
*
|
|
13
16
|
* Shared by `core/Publisher.ts` (publish), `harness/codeObject.ts`
|
|
14
17
|
* (deploy-object / upgrade-object), and `harness/script.ts`
|
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @internal — not exported from `src/index.ts`.
|
|
5
5
|
*/
|
|
6
|
+
import { logger } from '../ui/index.js';
|
|
6
7
|
/**
|
|
7
8
|
* Extract the transaction hash from a `movement` CLI subcommand's stdout.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
* `txn hash: 0x…`, `hash: 0x…`)
|
|
11
|
-
*
|
|
10
|
+
* Only the context-bearing pattern is accepted (`transaction hash: 0x…`,
|
|
11
|
+
* `txn hash: 0x…`, `hash: 0x…`). When the context is absent we log a
|
|
12
|
+
* warning and return `undefined` so callers decide whether to throw —
|
|
13
|
+
* the previous behavior of falling back to "any 64-hex literal" was
|
|
14
|
+
* fragile: a padded module address or state root printed before the
|
|
15
|
+
* actual txhash would silently corrupt the cached deployment record.
|
|
12
16
|
*
|
|
13
17
|
* Shared by `core/Publisher.ts` (publish), `harness/codeObject.ts`
|
|
14
18
|
* (deploy-object / upgrade-object), and `harness/script.ts`
|
|
@@ -20,6 +24,7 @@ export function parseTxHash(stdout) {
|
|
|
20
24
|
const withContext = stdout.match(/(?:transaction\s*(?:hash)?|txn\s*(?:hash)?|hash):\s*(0x[a-fA-F0-9]{64})\b/i);
|
|
21
25
|
if (withContext?.[1])
|
|
22
26
|
return withContext[1];
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
logger.warning(`parseTxHash: no contextual 'transaction|txn|hash: 0x…' match in ${stdout.length}-byte CLI output. ` +
|
|
28
|
+
`Returning undefined; the caller decides whether to error.`);
|
|
29
|
+
return undefined;
|
|
25
30
|
}
|