movehat 0.2.6 → 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/core/AccountManager.d.ts +121 -139
- package/dist/core/AccountManager.js +217 -158
- 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/package.json +1 -1
|
@@ -14,183 +14,165 @@ export interface SaveAccountPoolOptions {
|
|
|
14
14
|
*/
|
|
15
15
|
includeImported?: boolean | undefined;
|
|
16
16
|
}
|
|
17
|
+
export interface AccountManagerOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Filesystem location where `saveAccountPool` writes `test-pool.json`
|
|
20
|
+
* and `loadAccountPool` reads from. When omitted, the path is computed
|
|
21
|
+
* LAZILY on each call from `join(process.cwd(), ".movehat", "accounts")`
|
|
22
|
+
* — so `process.chdir()` between construction and pool I/O is respected.
|
|
23
|
+
*
|
|
24
|
+
* Pass an explicit value when per-instance isolation matters (parallel
|
|
25
|
+
* test files, harness pools that must not collide with another suite's).
|
|
26
|
+
*
|
|
27
|
+
* The process-wide static facade exposed on the `AccountManager` class
|
|
28
|
+
* itself uses a separate singleton whose `poolPath` is EAGERLY captured
|
|
29
|
+
* at module-import time. That preserves the legacy F8(b) behavior for
|
|
30
|
+
* any code still on the static API during the 0.2.7 deprecation window;
|
|
31
|
+
* the static facade is removed in 0.3.0.
|
|
32
|
+
*/
|
|
33
|
+
poolPath?: string | undefined;
|
|
34
|
+
}
|
|
17
35
|
/**
|
|
18
36
|
* Centralized Account Manager for movehat
|
|
19
37
|
*
|
|
20
38
|
* Manages all account creation, loading, and lifecycle operations.
|
|
21
|
-
* Provides a pool of reusable test accounts with labels for better test
|
|
39
|
+
* Provides a pool of reusable test accounts with labels for better test
|
|
40
|
+
* readability.
|
|
41
|
+
*
|
|
42
|
+
* Two API surfaces are exposed:
|
|
43
|
+
*
|
|
44
|
+
* **Instance API (recommended; M9.1+)** — `new AccountManager(options?)`
|
|
45
|
+
* gives a fully isolated pool. Two instances in the same process have
|
|
46
|
+
* independent labelMaps, private-key maps, and account pools. Each
|
|
47
|
+
* `Harness` constructed in 0.3.0+ will own one of these; user code
|
|
48
|
+
* accesses labeled accounts via `harness.accounts.<label>` (Harness
|
|
49
|
+
* snapshots them at construction time).
|
|
50
|
+
*
|
|
51
|
+
* **Static facade (deprecated; will be removed in 0.3.0)** — every
|
|
52
|
+
* former static method (`AccountManager.createAccount(...)`, etc.) still
|
|
53
|
+
* works and forwards to a single process-wide singleton. This preserves
|
|
54
|
+
* the existing label-sharing behavior across calls during the
|
|
55
|
+
* deprecation window. Migration target is the instance API; see M9 meta
|
|
56
|
+
* (#270) and migration guide at `/docs/upgrading/0.3.0` (published in
|
|
57
|
+
* M9.4).
|
|
22
58
|
*/
|
|
23
59
|
export declare class AccountManager {
|
|
24
|
-
private
|
|
25
|
-
private
|
|
26
|
-
private
|
|
27
|
-
private
|
|
28
|
-
private
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
60
|
+
private readonly pool;
|
|
61
|
+
private readonly privateKeys;
|
|
62
|
+
private readonly labelMap;
|
|
63
|
+
private readonly generatedAccountAddresses;
|
|
64
|
+
private poolLoaded;
|
|
65
|
+
private readonly poolPathOverride;
|
|
66
|
+
constructor(options?: AccountManagerOptions);
|
|
67
|
+
/**
|
|
68
|
+
* Resolved pool directory. When the caller supplied an explicit
|
|
69
|
+
* `poolPath` to the constructor, that value is returned verbatim.
|
|
70
|
+
* Otherwise the path is computed LAZILY from the current
|
|
71
|
+
* `process.cwd()` on each access — so `process.chdir()` performed
|
|
72
|
+
* between construction and pool I/O takes effect, unlike the legacy
|
|
73
|
+
* static-only API which captured cwd at module import time
|
|
74
|
+
* (audit finding F8(b)).
|
|
75
|
+
*/
|
|
76
|
+
private get poolPath();
|
|
77
|
+
/**
|
|
78
|
+
* Get a test account from the pool. If label is provided and exists,
|
|
79
|
+
* returns that account. Otherwise creates a new account with the
|
|
80
|
+
* optional label.
|
|
81
|
+
*
|
|
82
|
+
* @param label Optional label for the account (e.g., "alice", "bob")
|
|
35
83
|
* @returns An Account instance
|
|
36
|
-
*
|
|
37
|
-
* @example
|
|
38
|
-
* const alice = AccountManager.getTestAccount("alice");
|
|
39
|
-
* const randomAccount = AccountManager.getTestAccount();
|
|
40
84
|
*/
|
|
41
|
-
|
|
85
|
+
getTestAccount(label?: string): Account;
|
|
42
86
|
/**
|
|
43
|
-
* Create a new account and optionally add it to the pool with a
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* @param fund Whether to fund the account (handled externally)
|
|
47
|
-
* @returns A new Account instance
|
|
48
|
-
*
|
|
49
|
-
* @example
|
|
50
|
-
* const deployer = AccountManager.createAccount("deployer");
|
|
51
|
-
* const bob = AccountManager.createAccount("bob", true);
|
|
87
|
+
* Create a new account and optionally add it to the pool with a
|
|
88
|
+
* label. The `fund` parameter is accepted for API symmetry; actual
|
|
89
|
+
* funding is the caller's responsibility.
|
|
52
90
|
*/
|
|
53
|
-
|
|
91
|
+
createAccount(label?: string, _fund?: boolean): Account;
|
|
54
92
|
/**
|
|
55
|
-
* Get an account by its label
|
|
56
|
-
*
|
|
57
|
-
* @param label The label to look up
|
|
58
|
-
* @returns The Account if found, undefined otherwise
|
|
59
|
-
*
|
|
60
|
-
* @example
|
|
61
|
-
* const alice = AccountManager.getAccountByLabel("alice");
|
|
62
|
-
* if (alice) {
|
|
63
|
-
* console.log(`Alice's address: ${alice.accountAddress.toString()}`);
|
|
64
|
-
* }
|
|
93
|
+
* Get an account by its label, or undefined if not present.
|
|
65
94
|
*/
|
|
66
|
-
|
|
95
|
+
getAccountByLabel(label: string): Account | undefined;
|
|
67
96
|
/**
|
|
68
|
-
* Load account from environment variable
|
|
69
|
-
*
|
|
70
|
-
* @param envVar The environment variable name (defaults to "PRIVATE_KEY")
|
|
71
|
-
* @returns Account instance
|
|
72
|
-
* @throws Error if environment variable is not set
|
|
97
|
+
* Load account from environment variable.
|
|
73
98
|
*
|
|
74
|
-
* @
|
|
75
|
-
* const account = AccountManager.loadAccountFromEnv("MH_PRIVATE_KEY");
|
|
99
|
+
* @throws Error if the environment variable is not set
|
|
76
100
|
*/
|
|
77
|
-
|
|
101
|
+
loadAccountFromEnv(envVar?: string): Account;
|
|
78
102
|
/**
|
|
79
|
-
* Load account from a private key hex string
|
|
80
|
-
*
|
|
81
|
-
* @param privateKeyHex The private key as a hex string (with or without 0x prefix)
|
|
82
|
-
* @returns Account instance
|
|
83
|
-
*
|
|
84
|
-
* @example
|
|
85
|
-
* const account = AccountManager.loadAccountFromPrivateKey("0xabc123...");
|
|
103
|
+
* Load account from a private key hex string (with or without 0x prefix).
|
|
86
104
|
*/
|
|
87
|
-
|
|
105
|
+
loadAccountFromPrivateKey(privateKeyHex: string): Account;
|
|
88
106
|
/**
|
|
89
|
-
* Load all accounts from movehat config
|
|
90
|
-
*
|
|
91
|
-
* @param config The resolved MovehatConfig
|
|
92
|
-
* @returns Array of Account instances
|
|
93
|
-
*
|
|
94
|
-
* @example
|
|
95
|
-
* const config = await resolveNetworkConfig(userConfig);
|
|
96
|
-
* const accounts = AccountManager.loadAccountsFromConfig(config);
|
|
107
|
+
* Load all accounts from movehat config.
|
|
97
108
|
*/
|
|
98
|
-
|
|
109
|
+
loadAccountsFromConfig(config: MovehatConfig): Account[];
|
|
99
110
|
/**
|
|
100
|
-
* Get all labeled accounts in the pool
|
|
101
|
-
*
|
|
102
|
-
* @returns Record of label to Account mappings
|
|
103
|
-
*
|
|
104
|
-
* @example
|
|
105
|
-
* const labeled = AccountManager.getLabeledAccounts();
|
|
106
|
-
* console.log(`Deployer: ${labeled.deployer?.accountAddress.toString()}`);
|
|
111
|
+
* Get all labeled accounts in the pool.
|
|
107
112
|
*/
|
|
108
|
-
|
|
113
|
+
getLabeledAccounts(): Record<string, Account>;
|
|
109
114
|
/**
|
|
110
|
-
* Save the current account pool to disk for persistence
|
|
111
|
-
*
|
|
112
|
-
* @param poolPath Optional custom path (defaults to .movehat/accounts)
|
|
113
|
-
*
|
|
114
|
-
* @example
|
|
115
|
-
* AccountManager.saveAccountPool();
|
|
115
|
+
* Save the current account pool to disk for persistence.
|
|
116
116
|
*/
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
saveAccountPool(): void;
|
|
118
|
+
saveAccountPool(poolPath: string): void;
|
|
119
|
+
saveAccountPool(options: SaveAccountPoolOptions): void;
|
|
120
|
+
saveAccountPool(poolPath: string, options: SaveAccountPoolOptions): void;
|
|
121
121
|
/**
|
|
122
|
-
* Load account pool from disk
|
|
123
|
-
*
|
|
124
|
-
* @param poolPath Optional custom path (defaults to .movehat/accounts)
|
|
125
|
-
* @returns true if pool was loaded, false if file doesn't exist
|
|
126
|
-
*
|
|
127
|
-
* @example
|
|
128
|
-
* if (AccountManager.loadAccountPool()) {
|
|
129
|
-
* console.log("Pool loaded successfully");
|
|
130
|
-
* }
|
|
122
|
+
* Load account pool from disk. Returns true if a pool file was found
|
|
123
|
+
* and parsed, false if the file does not exist or parsing failed.
|
|
131
124
|
*/
|
|
132
|
-
|
|
125
|
+
loadAccountPool(poolPath?: string): boolean;
|
|
133
126
|
/**
|
|
134
|
-
* Clear the entire account pool
|
|
135
|
-
* Useful for test isolation
|
|
136
|
-
*
|
|
137
|
-
* @example
|
|
138
|
-
* after(() => {
|
|
139
|
-
* AccountManager.clearPool();
|
|
140
|
-
* });
|
|
127
|
+
* Clear the entire account pool. Useful for test isolation.
|
|
141
128
|
*/
|
|
142
|
-
|
|
129
|
+
clearPool(): void;
|
|
143
130
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* @returns Number of accounts in the pool
|
|
131
|
+
* Number of accounts in the pool.
|
|
147
132
|
*/
|
|
148
|
-
|
|
133
|
+
getPoolSize(): number;
|
|
149
134
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* @returns Array of all Account instances
|
|
135
|
+
* All accounts in the pool.
|
|
153
136
|
*/
|
|
154
|
-
|
|
137
|
+
getAllAccounts(): Account[];
|
|
155
138
|
/**
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* @param label The label to check
|
|
159
|
-
* @returns true if label exists, false otherwise
|
|
139
|
+
* Whether a label is currently bound to an account in this pool.
|
|
160
140
|
*/
|
|
161
|
-
|
|
141
|
+
hasLabel(label: string): boolean;
|
|
162
142
|
/**
|
|
163
|
-
* Get or create an account with a specific label
|
|
164
|
-
* If the label exists, returns the existing account
|
|
165
|
-
* Otherwise creates a new account with that label
|
|
166
|
-
*
|
|
167
|
-
* @param label The label for the account
|
|
168
|
-
* @returns Account instance
|
|
169
|
-
*
|
|
170
|
-
* @example
|
|
171
|
-
* const alice = AccountManager.getOrCreateLabeled("alice");
|
|
172
|
-
* const aliceAgain = AccountManager.getOrCreateLabeled("alice"); // Same account
|
|
143
|
+
* Get or create an account with a specific label.
|
|
173
144
|
*/
|
|
174
|
-
|
|
145
|
+
getOrCreateLabeled(label: string): Account;
|
|
175
146
|
/**
|
|
176
|
-
* Batch create multiple labeled accounts
|
|
177
|
-
*
|
|
178
|
-
* @param labels Array of labels to create
|
|
179
|
-
* @returns Record of label to Account mappings
|
|
180
|
-
*
|
|
181
|
-
* @example
|
|
182
|
-
* const accounts = AccountManager.createBatch(["alice", "bob", "charlie"]);
|
|
183
|
-
* console.log(accounts.alice.accountAddress.toString());
|
|
147
|
+
* Batch create multiple labeled accounts.
|
|
184
148
|
*/
|
|
185
|
-
|
|
149
|
+
createBatch(labels: readonly string[]): Record<string, Account>;
|
|
186
150
|
/**
|
|
187
|
-
* Export account private keys for backup/sharing
|
|
188
|
-
* WARNING: Only use this for test accounts, never production keys
|
|
189
|
-
*
|
|
190
|
-
* @param labels Optional array of labels to export (exports all if not provided)
|
|
191
|
-
* @returns Record of label/address to private key hex string
|
|
151
|
+
* Export account private keys for backup/sharing.
|
|
152
|
+
* WARNING: Only use this for test accounts, never production keys.
|
|
192
153
|
*/
|
|
154
|
+
exportPrivateKeys(labels?: string[]): Record<string, string>;
|
|
155
|
+
private accountFromPrivateKey;
|
|
156
|
+
private trackAccount;
|
|
157
|
+
static getTestAccount(label?: string): Account;
|
|
158
|
+
static createAccount(label?: string, fund?: boolean): Account;
|
|
159
|
+
static getAccountByLabel(label: string): Account | undefined;
|
|
160
|
+
static loadAccountFromEnv(envVar?: string): Account;
|
|
161
|
+
static loadAccountFromPrivateKey(privateKeyHex: string): Account;
|
|
162
|
+
static loadAccountsFromConfig(config: MovehatConfig): Account[];
|
|
163
|
+
static getLabeledAccounts(): Record<string, Account>;
|
|
164
|
+
static saveAccountPool(): void;
|
|
165
|
+
static saveAccountPool(poolPath: string): void;
|
|
166
|
+
static saveAccountPool(options: SaveAccountPoolOptions): void;
|
|
167
|
+
static saveAccountPool(poolPath: string, options: SaveAccountPoolOptions): void;
|
|
168
|
+
static loadAccountPool(poolPath?: string): boolean;
|
|
169
|
+
static clearPool(): void;
|
|
170
|
+
static getPoolSize(): number;
|
|
171
|
+
static getAllAccounts(): Account[];
|
|
172
|
+
static hasLabel(label: string): boolean;
|
|
173
|
+
static getOrCreateLabeled(label: string): Account;
|
|
174
|
+
static createBatch(labels: readonly string[]): Record<string, Account>;
|
|
193
175
|
static exportPrivateKeys(labels?: string[]): Record<string, string>;
|
|
194
|
-
private static accountFromPrivateKey;
|
|
195
|
-
private static trackAccount;
|
|
196
176
|
}
|
|
177
|
+
/** @internal — test-only. Resets the once-per-method dedup Set. */
|
|
178
|
+
export declare function _resetDeprecationWarnings(): void;
|
|
@@ -1,59 +1,82 @@
|
|
|
1
1
|
import { Account, Ed25519PrivateKey, PrivateKey, PrivateKeyVariants, } from "@aptos-labs/ts-sdk";
|
|
2
2
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
|
+
import { logger } from "../ui/index.js";
|
|
4
5
|
/**
|
|
5
6
|
* Centralized Account Manager for movehat
|
|
6
7
|
*
|
|
7
8
|
* Manages all account creation, loading, and lifecycle operations.
|
|
8
|
-
* Provides a pool of reusable test accounts with labels for better test
|
|
9
|
+
* Provides a pool of reusable test accounts with labels for better test
|
|
10
|
+
* readability.
|
|
11
|
+
*
|
|
12
|
+
* Two API surfaces are exposed:
|
|
13
|
+
*
|
|
14
|
+
* **Instance API (recommended; M9.1+)** — `new AccountManager(options?)`
|
|
15
|
+
* gives a fully isolated pool. Two instances in the same process have
|
|
16
|
+
* independent labelMaps, private-key maps, and account pools. Each
|
|
17
|
+
* `Harness` constructed in 0.3.0+ will own one of these; user code
|
|
18
|
+
* accesses labeled accounts via `harness.accounts.<label>` (Harness
|
|
19
|
+
* snapshots them at construction time).
|
|
20
|
+
*
|
|
21
|
+
* **Static facade (deprecated; will be removed in 0.3.0)** — every
|
|
22
|
+
* former static method (`AccountManager.createAccount(...)`, etc.) still
|
|
23
|
+
* works and forwards to a single process-wide singleton. This preserves
|
|
24
|
+
* the existing label-sharing behavior across calls during the
|
|
25
|
+
* deprecation window. Migration target is the instance API; see M9 meta
|
|
26
|
+
* (#270) and migration guide at `/docs/upgrading/0.3.0` (published in
|
|
27
|
+
* M9.4).
|
|
9
28
|
*/
|
|
10
29
|
export class AccountManager {
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
// ── Instance state ──────────────────────────────────────────────
|
|
31
|
+
// All former class-static fields are now instance fields. Two
|
|
32
|
+
// `new AccountManager()` calls produce fully isolated state. The
|
|
33
|
+
// static facade below operates on a single module-level singleton,
|
|
34
|
+
// so existing callers see no behavior change.
|
|
35
|
+
pool = new Map(); // address → Account
|
|
36
|
+
privateKeys = new Map(); // address → privateKey hex
|
|
37
|
+
labelMap = new Map(); // label → address
|
|
38
|
+
generatedAccountAddresses = new Set();
|
|
39
|
+
poolLoaded = false;
|
|
40
|
+
poolPathOverride;
|
|
41
|
+
constructor(options = {}) {
|
|
42
|
+
this.poolPathOverride = options.poolPath;
|
|
43
|
+
}
|
|
25
44
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
45
|
+
* Resolved pool directory. When the caller supplied an explicit
|
|
46
|
+
* `poolPath` to the constructor, that value is returned verbatim.
|
|
47
|
+
* Otherwise the path is computed LAZILY from the current
|
|
48
|
+
* `process.cwd()` on each access — so `process.chdir()` performed
|
|
49
|
+
* between construction and pool I/O takes effect, unlike the legacy
|
|
50
|
+
* static-only API which captured cwd at module import time
|
|
51
|
+
* (audit finding F8(b)).
|
|
52
|
+
*/
|
|
53
|
+
get poolPath() {
|
|
54
|
+
return this.poolPathOverride ?? join(process.cwd(), ".movehat", "accounts");
|
|
55
|
+
}
|
|
56
|
+
// ── Instance methods (real implementations) ─────────────────────
|
|
57
|
+
/**
|
|
58
|
+
* Get a test account from the pool. If label is provided and exists,
|
|
59
|
+
* returns that account. Otherwise creates a new account with the
|
|
60
|
+
* optional label.
|
|
28
61
|
*
|
|
29
|
-
* @param label Optional label for the account (e.g., "alice", "bob"
|
|
62
|
+
* @param label Optional label for the account (e.g., "alice", "bob")
|
|
30
63
|
* @returns An Account instance
|
|
31
|
-
*
|
|
32
|
-
* @example
|
|
33
|
-
* const alice = AccountManager.getTestAccount("alice");
|
|
34
|
-
* const randomAccount = AccountManager.getTestAccount();
|
|
35
64
|
*/
|
|
36
|
-
|
|
65
|
+
getTestAccount(label) {
|
|
37
66
|
if (label) {
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
40
|
-
return
|
|
67
|
+
const existing = this.getAccountByLabel(label);
|
|
68
|
+
if (existing) {
|
|
69
|
+
return existing;
|
|
41
70
|
}
|
|
42
71
|
}
|
|
43
72
|
return this.createAccount(label, false);
|
|
44
73
|
}
|
|
45
74
|
/**
|
|
46
|
-
* Create a new account and optionally add it to the pool with a
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* @param fund Whether to fund the account (handled externally)
|
|
50
|
-
* @returns A new Account instance
|
|
51
|
-
*
|
|
52
|
-
* @example
|
|
53
|
-
* const deployer = AccountManager.createAccount("deployer");
|
|
54
|
-
* const bob = AccountManager.createAccount("bob", true);
|
|
75
|
+
* Create a new account and optionally add it to the pool with a
|
|
76
|
+
* label. The `fund` parameter is accepted for API symmetry; actual
|
|
77
|
+
* funding is the caller's responsibility.
|
|
55
78
|
*/
|
|
56
|
-
|
|
79
|
+
createAccount(label, _fund = false) {
|
|
57
80
|
const account = Account.generate();
|
|
58
81
|
const address = account.accountAddress.toString();
|
|
59
82
|
this.pool.set(address, account);
|
|
@@ -65,18 +88,9 @@ export class AccountManager {
|
|
|
65
88
|
return account;
|
|
66
89
|
}
|
|
67
90
|
/**
|
|
68
|
-
* Get an account by its label
|
|
69
|
-
*
|
|
70
|
-
* @param label The label to look up
|
|
71
|
-
* @returns The Account if found, undefined otherwise
|
|
72
|
-
*
|
|
73
|
-
* @example
|
|
74
|
-
* const alice = AccountManager.getAccountByLabel("alice");
|
|
75
|
-
* if (alice) {
|
|
76
|
-
* console.log(`Alice's address: ${alice.accountAddress.toString()}`);
|
|
77
|
-
* }
|
|
91
|
+
* Get an account by its label, or undefined if not present.
|
|
78
92
|
*/
|
|
79
|
-
|
|
93
|
+
getAccountByLabel(label) {
|
|
80
94
|
const address = this.labelMap.get(label);
|
|
81
95
|
if (!address) {
|
|
82
96
|
return undefined;
|
|
@@ -84,16 +98,11 @@ export class AccountManager {
|
|
|
84
98
|
return this.pool.get(address);
|
|
85
99
|
}
|
|
86
100
|
/**
|
|
87
|
-
* Load account from environment variable
|
|
88
|
-
*
|
|
89
|
-
* @param envVar The environment variable name (defaults to "PRIVATE_KEY")
|
|
90
|
-
* @returns Account instance
|
|
91
|
-
* @throws Error if environment variable is not set
|
|
101
|
+
* Load account from environment variable.
|
|
92
102
|
*
|
|
93
|
-
* @
|
|
94
|
-
* const account = AccountManager.loadAccountFromEnv("MH_PRIVATE_KEY");
|
|
103
|
+
* @throws Error if the environment variable is not set
|
|
95
104
|
*/
|
|
96
|
-
|
|
105
|
+
loadAccountFromEnv(envVar = "PRIVATE_KEY") {
|
|
97
106
|
const privateKeyHex = process.env[envVar];
|
|
98
107
|
if (!privateKeyHex) {
|
|
99
108
|
throw new Error(`Environment variable ${envVar} not found. ` +
|
|
@@ -102,15 +111,9 @@ export class AccountManager {
|
|
|
102
111
|
return this.loadAccountFromPrivateKey(privateKeyHex);
|
|
103
112
|
}
|
|
104
113
|
/**
|
|
105
|
-
* Load account from a private key hex string
|
|
106
|
-
*
|
|
107
|
-
* @param privateKeyHex The private key as a hex string (with or without 0x prefix)
|
|
108
|
-
* @returns Account instance
|
|
109
|
-
*
|
|
110
|
-
* @example
|
|
111
|
-
* const account = AccountManager.loadAccountFromPrivateKey("0xabc123...");
|
|
114
|
+
* Load account from a private key hex string (with or without 0x prefix).
|
|
112
115
|
*/
|
|
113
|
-
|
|
116
|
+
loadAccountFromPrivateKey(privateKeyHex) {
|
|
114
117
|
// Format into AIP-80 shape (`ed25519-priv-0x…`) before constructing
|
|
115
118
|
// the SDK type. Without this, raw-hex inputs trigger a noisy
|
|
116
119
|
// deprecation warning from `@aptos-labs/ts-sdk` on every call.
|
|
@@ -119,16 +122,9 @@ export class AccountManager {
|
|
|
119
122
|
return account;
|
|
120
123
|
}
|
|
121
124
|
/**
|
|
122
|
-
* Load all accounts from movehat config
|
|
123
|
-
*
|
|
124
|
-
* @param config The resolved MovehatConfig
|
|
125
|
-
* @returns Array of Account instances
|
|
126
|
-
*
|
|
127
|
-
* @example
|
|
128
|
-
* const config = await resolveNetworkConfig(userConfig);
|
|
129
|
-
* const accounts = AccountManager.loadAccountsFromConfig(config);
|
|
125
|
+
* Load all accounts from movehat config.
|
|
130
126
|
*/
|
|
131
|
-
|
|
127
|
+
loadAccountsFromConfig(config) {
|
|
132
128
|
const accounts = [];
|
|
133
129
|
for (const privateKeyHex of config.allAccounts) {
|
|
134
130
|
try {
|
|
@@ -137,21 +133,15 @@ export class AccountManager {
|
|
|
137
133
|
}
|
|
138
134
|
catch (error) {
|
|
139
135
|
const msg = error instanceof Error ? error.message : String(error);
|
|
140
|
-
|
|
136
|
+
logger.warning(`Failed to load account from config: ${msg}`);
|
|
141
137
|
}
|
|
142
138
|
}
|
|
143
139
|
return accounts;
|
|
144
140
|
}
|
|
145
141
|
/**
|
|
146
|
-
* Get all labeled accounts in the pool
|
|
147
|
-
*
|
|
148
|
-
* @returns Record of label to Account mappings
|
|
149
|
-
*
|
|
150
|
-
* @example
|
|
151
|
-
* const labeled = AccountManager.getLabeledAccounts();
|
|
152
|
-
* console.log(`Deployer: ${labeled.deployer?.accountAddress.toString()}`);
|
|
142
|
+
* Get all labeled accounts in the pool.
|
|
153
143
|
*/
|
|
154
|
-
|
|
144
|
+
getLabeledAccounts() {
|
|
155
145
|
const result = {};
|
|
156
146
|
for (const [label, address] of this.labelMap.entries()) {
|
|
157
147
|
const account = this.pool.get(address);
|
|
@@ -161,25 +151,25 @@ export class AccountManager {
|
|
|
161
151
|
}
|
|
162
152
|
return result;
|
|
163
153
|
}
|
|
164
|
-
|
|
165
|
-
const
|
|
154
|
+
saveAccountPool(poolPathOrOptions, options = {}) {
|
|
155
|
+
const explicitPath = typeof poolPathOrOptions === "string" ? poolPathOrOptions : undefined;
|
|
166
156
|
const effectiveOptions = typeof poolPathOrOptions === "object" && poolPathOrOptions !== null
|
|
167
157
|
? poolPathOrOptions
|
|
168
158
|
: options;
|
|
169
159
|
const includeImported = effectiveOptions.includeImported ?? false;
|
|
170
|
-
const basePath =
|
|
160
|
+
const basePath = explicitPath || this.poolPath;
|
|
171
161
|
// Ensure directory exists with restrictive perms (the pool file holds
|
|
172
162
|
// plaintext private keys, so the directory must not be world-readable).
|
|
173
|
-
//
|
|
174
|
-
//
|
|
163
|
+
// mkdirSync's mode is masked by the process umask; chmod explicitly
|
|
164
|
+
// afterwards to guarantee 0o700.
|
|
175
165
|
if (!existsSync(basePath)) {
|
|
176
166
|
mkdirSync(basePath, { recursive: true, mode: 0o700 });
|
|
177
167
|
}
|
|
178
168
|
chmodSync(basePath, 0o700);
|
|
179
|
-
// Build stored accounts array
|
|
180
169
|
const storedAccounts = [];
|
|
181
170
|
const labelMapObject = {};
|
|
182
171
|
for (const [address, account] of this.pool.entries()) {
|
|
172
|
+
void account;
|
|
183
173
|
if (!includeImported && !this.generatedAccountAddresses.has(address)) {
|
|
184
174
|
continue;
|
|
185
175
|
}
|
|
@@ -194,7 +184,7 @@ export class AccountManager {
|
|
|
194
184
|
}
|
|
195
185
|
const privateKey = this.privateKeys.get(address);
|
|
196
186
|
if (!privateKey) {
|
|
197
|
-
|
|
187
|
+
logger.warning(`No private key found for account ${address}, skipping`);
|
|
198
188
|
continue;
|
|
199
189
|
}
|
|
200
190
|
storedAccounts.push({
|
|
@@ -208,10 +198,6 @@ export class AccountManager {
|
|
|
208
198
|
accounts: storedAccounts,
|
|
209
199
|
labelMap: labelMapObject,
|
|
210
200
|
};
|
|
211
|
-
// Write to file with owner-only permissions — file contains plaintext
|
|
212
|
-
// private keys for test accounts. writeFileSync's mode is masked by
|
|
213
|
-
// the process umask, so we chmod explicitly afterwards to guarantee
|
|
214
|
-
// 0o600 regardless of umask.
|
|
215
201
|
const poolFilePath = join(basePath, "test-pool.json");
|
|
216
202
|
writeFileSync(poolFilePath, JSON.stringify(poolData, null, 2), {
|
|
217
203
|
encoding: "utf-8",
|
|
@@ -220,31 +206,22 @@ export class AccountManager {
|
|
|
220
206
|
chmodSync(poolFilePath, 0o600);
|
|
221
207
|
}
|
|
222
208
|
/**
|
|
223
|
-
* Load account pool from disk
|
|
224
|
-
*
|
|
225
|
-
* @param poolPath Optional custom path (defaults to .movehat/accounts)
|
|
226
|
-
* @returns true if pool was loaded, false if file doesn't exist
|
|
227
|
-
*
|
|
228
|
-
* @example
|
|
229
|
-
* if (AccountManager.loadAccountPool()) {
|
|
230
|
-
* console.log("Pool loaded successfully");
|
|
231
|
-
* }
|
|
209
|
+
* Load account pool from disk. Returns true if a pool file was found
|
|
210
|
+
* and parsed, false if the file does not exist or parsing failed.
|
|
232
211
|
*/
|
|
233
|
-
|
|
212
|
+
loadAccountPool(poolPath) {
|
|
234
213
|
if (this.poolLoaded) {
|
|
235
|
-
return true;
|
|
214
|
+
return true;
|
|
236
215
|
}
|
|
237
|
-
const basePath = poolPath || this.
|
|
216
|
+
const basePath = poolPath || this.poolPath;
|
|
238
217
|
const poolFilePath = join(basePath, "test-pool.json");
|
|
239
218
|
if (!existsSync(poolFilePath)) {
|
|
240
219
|
return false;
|
|
241
220
|
}
|
|
242
221
|
try {
|
|
243
222
|
const poolData = JSON.parse(readFileSync(poolFilePath, "utf-8"));
|
|
244
|
-
// Clear existing pool
|
|
245
223
|
this.pool.clear();
|
|
246
224
|
this.labelMap.clear();
|
|
247
|
-
// Restore accounts
|
|
248
225
|
for (const stored of poolData.accounts) {
|
|
249
226
|
const account = this.accountFromPrivateKey(stored.privateKey);
|
|
250
227
|
this.trackAccount(account, stored.privateKey, "generated");
|
|
@@ -263,20 +240,14 @@ export class AccountManager {
|
|
|
263
240
|
}
|
|
264
241
|
catch (error) {
|
|
265
242
|
const msg = error instanceof Error ? error.message : String(error);
|
|
266
|
-
|
|
243
|
+
logger.warning(`Failed to load account pool: ${msg}`);
|
|
267
244
|
return false;
|
|
268
245
|
}
|
|
269
246
|
}
|
|
270
247
|
/**
|
|
271
|
-
* Clear the entire account pool
|
|
272
|
-
* Useful for test isolation
|
|
273
|
-
*
|
|
274
|
-
* @example
|
|
275
|
-
* after(() => {
|
|
276
|
-
* AccountManager.clearPool();
|
|
277
|
-
* });
|
|
248
|
+
* Clear the entire account pool. Useful for test isolation.
|
|
278
249
|
*/
|
|
279
|
-
|
|
250
|
+
clearPool() {
|
|
280
251
|
this.pool.clear();
|
|
281
252
|
this.privateKeys.clear();
|
|
282
253
|
this.labelMap.clear();
|
|
@@ -284,43 +255,27 @@ export class AccountManager {
|
|
|
284
255
|
this.poolLoaded = false;
|
|
285
256
|
}
|
|
286
257
|
/**
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
* @returns Number of accounts in the pool
|
|
258
|
+
* Number of accounts in the pool.
|
|
290
259
|
*/
|
|
291
|
-
|
|
260
|
+
getPoolSize() {
|
|
292
261
|
return this.pool.size;
|
|
293
262
|
}
|
|
294
263
|
/**
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
* @returns Array of all Account instances
|
|
264
|
+
* All accounts in the pool.
|
|
298
265
|
*/
|
|
299
|
-
|
|
266
|
+
getAllAccounts() {
|
|
300
267
|
return Array.from(this.pool.values());
|
|
301
268
|
}
|
|
302
269
|
/**
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
* @param label The label to check
|
|
306
|
-
* @returns true if label exists, false otherwise
|
|
270
|
+
* Whether a label is currently bound to an account in this pool.
|
|
307
271
|
*/
|
|
308
|
-
|
|
272
|
+
hasLabel(label) {
|
|
309
273
|
return this.labelMap.has(label);
|
|
310
274
|
}
|
|
311
275
|
/**
|
|
312
|
-
* Get or create an account with a specific label
|
|
313
|
-
* If the label exists, returns the existing account
|
|
314
|
-
* Otherwise creates a new account with that label
|
|
315
|
-
*
|
|
316
|
-
* @param label The label for the account
|
|
317
|
-
* @returns Account instance
|
|
318
|
-
*
|
|
319
|
-
* @example
|
|
320
|
-
* const alice = AccountManager.getOrCreateLabeled("alice");
|
|
321
|
-
* const aliceAgain = AccountManager.getOrCreateLabeled("alice"); // Same account
|
|
276
|
+
* Get or create an account with a specific label.
|
|
322
277
|
*/
|
|
323
|
-
|
|
278
|
+
getOrCreateLabeled(label) {
|
|
324
279
|
const existing = this.getAccountByLabel(label);
|
|
325
280
|
if (existing) {
|
|
326
281
|
return existing;
|
|
@@ -328,16 +283,9 @@ export class AccountManager {
|
|
|
328
283
|
return this.createAccount(label, false);
|
|
329
284
|
}
|
|
330
285
|
/**
|
|
331
|
-
* Batch create multiple labeled accounts
|
|
332
|
-
*
|
|
333
|
-
* @param labels Array of labels to create
|
|
334
|
-
* @returns Record of label to Account mappings
|
|
335
|
-
*
|
|
336
|
-
* @example
|
|
337
|
-
* const accounts = AccountManager.createBatch(["alice", "bob", "charlie"]);
|
|
338
|
-
* console.log(accounts.alice.accountAddress.toString());
|
|
286
|
+
* Batch create multiple labeled accounts.
|
|
339
287
|
*/
|
|
340
|
-
|
|
288
|
+
createBatch(labels) {
|
|
341
289
|
const result = {};
|
|
342
290
|
for (const label of labels) {
|
|
343
291
|
result[label] = this.getOrCreateLabeled(label);
|
|
@@ -345,16 +293,12 @@ export class AccountManager {
|
|
|
345
293
|
return result;
|
|
346
294
|
}
|
|
347
295
|
/**
|
|
348
|
-
* Export account private keys for backup/sharing
|
|
349
|
-
* WARNING: Only use this for test accounts, never production keys
|
|
350
|
-
*
|
|
351
|
-
* @param labels Optional array of labels to export (exports all if not provided)
|
|
352
|
-
* @returns Record of label/address to private key hex string
|
|
296
|
+
* Export account private keys for backup/sharing.
|
|
297
|
+
* WARNING: Only use this for test accounts, never production keys.
|
|
353
298
|
*/
|
|
354
|
-
|
|
299
|
+
exportPrivateKeys(labels) {
|
|
355
300
|
const result = {};
|
|
356
301
|
if (labels) {
|
|
357
|
-
// Export specific labels
|
|
358
302
|
for (const label of labels) {
|
|
359
303
|
const address = this.labelMap.get(label);
|
|
360
304
|
if (address) {
|
|
@@ -366,7 +310,6 @@ export class AccountManager {
|
|
|
366
310
|
}
|
|
367
311
|
}
|
|
368
312
|
else {
|
|
369
|
-
// Export all labeled accounts
|
|
370
313
|
for (const [label, address] of this.labelMap.entries()) {
|
|
371
314
|
const privateKey = this.privateKeys.get(address);
|
|
372
315
|
if (privateKey) {
|
|
@@ -376,12 +319,12 @@ export class AccountManager {
|
|
|
376
319
|
}
|
|
377
320
|
return result;
|
|
378
321
|
}
|
|
379
|
-
|
|
322
|
+
accountFromPrivateKey(privateKeyHex) {
|
|
380
323
|
const formatted = PrivateKey.formatPrivateKey(privateKeyHex, PrivateKeyVariants.Ed25519);
|
|
381
324
|
const privateKey = new Ed25519PrivateKey(formatted);
|
|
382
325
|
return Account.fromPrivateKey({ privateKey });
|
|
383
326
|
}
|
|
384
|
-
|
|
327
|
+
trackAccount(account, privateKeyHex, source) {
|
|
385
328
|
const address = account.accountAddress.toString();
|
|
386
329
|
this.pool.set(address, account);
|
|
387
330
|
this.privateKeys.set(address, privateKeyHex);
|
|
@@ -392,4 +335,120 @@ export class AccountManager {
|
|
|
392
335
|
this.generatedAccountAddresses.delete(address);
|
|
393
336
|
}
|
|
394
337
|
}
|
|
338
|
+
// ── Static facade (deprecated; removed in 0.3.0) ────────────────
|
|
339
|
+
//
|
|
340
|
+
// Every former static method below forwards to `__defaultManager`,
|
|
341
|
+
// a module-level singleton constructed once at import time. This
|
|
342
|
+
// preserves the legacy "process-wide pool shared across consumers"
|
|
343
|
+
// behavior so existing callers see no change during the 0.2.7
|
|
344
|
+
// deprecation window. The singleton is constructed with its
|
|
345
|
+
// `poolPath` eagerly captured from the import-time `process.cwd()`
|
|
346
|
+
// — preserving F8(b) for the static API specifically.
|
|
347
|
+
//
|
|
348
|
+
// Removal happens in M9.4 (0.3.0). Until then the runtime warning
|
|
349
|
+
// added in M9.3 nudges users toward `harness.accounts.<label>` or
|
|
350
|
+
// `harness.runtime.accountManager.*`.
|
|
351
|
+
static getTestAccount(label) {
|
|
352
|
+
__warnDeprecated("getTestAccount");
|
|
353
|
+
return __defaultManager.getTestAccount(label);
|
|
354
|
+
}
|
|
355
|
+
static createAccount(label, fund = false) {
|
|
356
|
+
__warnDeprecated("createAccount");
|
|
357
|
+
return __defaultManager.createAccount(label, fund);
|
|
358
|
+
}
|
|
359
|
+
static getAccountByLabel(label) {
|
|
360
|
+
__warnDeprecated("getAccountByLabel");
|
|
361
|
+
return __defaultManager.getAccountByLabel(label);
|
|
362
|
+
}
|
|
363
|
+
static loadAccountFromEnv(envVar = "PRIVATE_KEY") {
|
|
364
|
+
__warnDeprecated("loadAccountFromEnv");
|
|
365
|
+
return __defaultManager.loadAccountFromEnv(envVar);
|
|
366
|
+
}
|
|
367
|
+
static loadAccountFromPrivateKey(privateKeyHex) {
|
|
368
|
+
__warnDeprecated("loadAccountFromPrivateKey");
|
|
369
|
+
return __defaultManager.loadAccountFromPrivateKey(privateKeyHex);
|
|
370
|
+
}
|
|
371
|
+
static loadAccountsFromConfig(config) {
|
|
372
|
+
__warnDeprecated("loadAccountsFromConfig");
|
|
373
|
+
return __defaultManager.loadAccountsFromConfig(config);
|
|
374
|
+
}
|
|
375
|
+
static getLabeledAccounts() {
|
|
376
|
+
__warnDeprecated("getLabeledAccounts");
|
|
377
|
+
return __defaultManager.getLabeledAccounts();
|
|
378
|
+
}
|
|
379
|
+
static saveAccountPool(poolPathOrOptions, options = {}) {
|
|
380
|
+
__warnDeprecated("saveAccountPool");
|
|
381
|
+
// Re-dispatch via the instance overloads, preserving caller intent.
|
|
382
|
+
if (poolPathOrOptions === undefined) {
|
|
383
|
+
__defaultManager.saveAccountPool();
|
|
384
|
+
}
|
|
385
|
+
else if (typeof poolPathOrOptions === "string") {
|
|
386
|
+
__defaultManager.saveAccountPool(poolPathOrOptions, options);
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
__defaultManager.saveAccountPool(poolPathOrOptions);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
static loadAccountPool(poolPath) {
|
|
393
|
+
__warnDeprecated("loadAccountPool");
|
|
394
|
+
return __defaultManager.loadAccountPool(poolPath);
|
|
395
|
+
}
|
|
396
|
+
static clearPool() {
|
|
397
|
+
__warnDeprecated("clearPool");
|
|
398
|
+
__defaultManager.clearPool();
|
|
399
|
+
}
|
|
400
|
+
static getPoolSize() {
|
|
401
|
+
__warnDeprecated("getPoolSize");
|
|
402
|
+
return __defaultManager.getPoolSize();
|
|
403
|
+
}
|
|
404
|
+
static getAllAccounts() {
|
|
405
|
+
__warnDeprecated("getAllAccounts");
|
|
406
|
+
return __defaultManager.getAllAccounts();
|
|
407
|
+
}
|
|
408
|
+
static hasLabel(label) {
|
|
409
|
+
__warnDeprecated("hasLabel");
|
|
410
|
+
return __defaultManager.hasLabel(label);
|
|
411
|
+
}
|
|
412
|
+
static getOrCreateLabeled(label) {
|
|
413
|
+
__warnDeprecated("getOrCreateLabeled");
|
|
414
|
+
return __defaultManager.getOrCreateLabeled(label);
|
|
415
|
+
}
|
|
416
|
+
static createBatch(labels) {
|
|
417
|
+
__warnDeprecated("createBatch");
|
|
418
|
+
return __defaultManager.createBatch(labels);
|
|
419
|
+
}
|
|
420
|
+
static exportPrivateKeys(labels) {
|
|
421
|
+
__warnDeprecated("exportPrivateKeys");
|
|
422
|
+
return __defaultManager.exportPrivateKeys(labels);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// Module-level singleton backing the static facade. The eager poolPath
|
|
426
|
+
// capture mirrors the pre-M9 behavior — `process.chdir` after import
|
|
427
|
+
// does NOT redirect static-facade pool I/O. F8(b) is preserved for
|
|
428
|
+
// the static API and only flips for callers using the instance API.
|
|
429
|
+
const __defaultManager = new AccountManager({
|
|
430
|
+
poolPath: join(process.cwd(), ".movehat", "accounts"),
|
|
431
|
+
});
|
|
432
|
+
// Once-per-method-per-process deprecation tracking for the static
|
|
433
|
+
// facade. The warning fires on the FIRST call to each static method
|
|
434
|
+
// per process, then stays silent for subsequent calls. A user with
|
|
435
|
+
// 50 tests calling `AccountManager.getLabeledAccounts()` sees the
|
|
436
|
+
// warning once, not 50 times.
|
|
437
|
+
//
|
|
438
|
+
// Exported as `_resetDeprecationWarnings` for tests that need to
|
|
439
|
+
// verify warning behavior across multiple `it` blocks (vitest workers
|
|
440
|
+
// share module state within a single test file).
|
|
441
|
+
const __warnedMethods = new Set();
|
|
442
|
+
/** @internal — test-only. Resets the once-per-method dedup Set. */
|
|
443
|
+
export function _resetDeprecationWarnings() {
|
|
444
|
+
__warnedMethods.clear();
|
|
445
|
+
}
|
|
446
|
+
function __warnDeprecated(method) {
|
|
447
|
+
if (__warnedMethods.has(method))
|
|
448
|
+
return;
|
|
449
|
+
__warnedMethods.add(method);
|
|
450
|
+
logger.warning(`AccountManager.${method} is deprecated and will be removed in 0.3.0. ` +
|
|
451
|
+
`Use 'harness.accounts.<label>' for labeled-account access, or ` +
|
|
452
|
+
`'harness.runtime.accountManager.${method}' for direct manager calls. ` +
|
|
453
|
+
`See migration tracker: https://github.com/gilbertsahumada/movehat/issues/270`);
|
|
395
454
|
}
|
|
@@ -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;
|