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.
@@ -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 readability.
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 static pool;
25
- private static privateKeys;
26
- private static labelMap;
27
- private static generatedAccountAddresses;
28
- private static poolLoaded;
29
- private static defaultPoolPath;
30
- /**
31
- * Get a test account from the pool. If label is provided and exists, returns that account.
32
- * Otherwise creates a new account with the optional label.
33
- *
34
- * @param label Optional label for the account (e.g., "alice", "bob", "deployer")
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
- static getTestAccount(label?: string): Account;
85
+ getTestAccount(label?: string): Account;
42
86
  /**
43
- * Create a new account and optionally add it to the pool with a label
44
- *
45
- * @param label Optional label for the account
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
- static createAccount(label?: string, fund?: boolean): Account;
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
- static getAccountByLabel(label: string): Account | undefined;
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
- * @example
75
- * const account = AccountManager.loadAccountFromEnv("MH_PRIVATE_KEY");
99
+ * @throws Error if the environment variable is not set
76
100
  */
77
- static loadAccountFromEnv(envVar?: string): Account;
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
- static loadAccountFromPrivateKey(privateKeyHex: string): Account;
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
- static loadAccountsFromConfig(config: MovehatConfig): Account[];
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
- static getLabeledAccounts(): Record<string, Account>;
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
- static saveAccountPool(): void;
118
- static saveAccountPool(poolPath: string): void;
119
- static saveAccountPool(options: SaveAccountPoolOptions): void;
120
- static saveAccountPool(poolPath: string, options: SaveAccountPoolOptions): void;
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
- static loadAccountPool(poolPath?: string): boolean;
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
- static clearPool(): void;
129
+ clearPool(): void;
143
130
  /**
144
- * Get the current size of the account pool
145
- *
146
- * @returns Number of accounts in the pool
131
+ * Number of accounts in the pool.
147
132
  */
148
- static getPoolSize(): number;
133
+ getPoolSize(): number;
149
134
  /**
150
- * Get all accounts in the pool
151
- *
152
- * @returns Array of all Account instances
135
+ * All accounts in the pool.
153
136
  */
154
- static getAllAccounts(): Account[];
137
+ getAllAccounts(): Account[];
155
138
  /**
156
- * Check if a label is already in use
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
- static hasLabel(label: string): boolean;
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
- static getOrCreateLabeled(label: string): Account;
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
- static createBatch(labels: readonly string[]): Record<string, Account>;
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 readability.
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
- // Class-static maps: shared across every consumer in the same Node
12
- // process (e.g. two Harness instances). Re-using a label across
13
- // "sessions" overwrites the labelMap entry. Documented + fixed by
14
- // contract in `AccountManager.global-state.test.ts` (audit F8).
15
- static pool = new Map(); // address → Account
16
- static privateKeys = new Map(); // address → privateKey hex
17
- static labelMap = new Map(); // labeladdress
18
- static generatedAccountAddresses = new Set();
19
- static poolLoaded = false;
20
- // `defaultPoolPath` is captured ONCE at module-import time. A later
21
- // `process.chdir(...)` does NOT redirect the save destination — pass
22
- // an explicit `poolPath` to `saveAccountPool`/`loadAccountPool` when
23
- // per-test isolation matters. See audit finding F8.
24
- static defaultPoolPath = join(process.cwd(), ".movehat", "accounts");
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(); // addressprivateKey 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
- * Get a test account from the pool. If label is provided and exists, returns that account.
27
- * Otherwise creates a new account with the optional label.
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", "deployer")
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
- static getTestAccount(label) {
65
+ getTestAccount(label) {
37
66
  if (label) {
38
- const existingAccount = this.getAccountByLabel(label);
39
- if (existingAccount) {
40
- return existingAccount;
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 label
47
- *
48
- * @param label Optional label for the account
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
- static createAccount(label, fund = false) {
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
- static getAccountByLabel(label) {
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
- * @example
94
- * const account = AccountManager.loadAccountFromEnv("MH_PRIVATE_KEY");
103
+ * @throws Error if the environment variable is not set
95
104
  */
96
- static loadAccountFromEnv(envVar = "PRIVATE_KEY") {
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
- static loadAccountFromPrivateKey(privateKeyHex) {
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
- static loadAccountsFromConfig(config) {
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
- console.warn(`Warning: Failed to load account from config: ${msg}`);
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
- static getLabeledAccounts() {
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
- static saveAccountPool(poolPathOrOptions, options = {}) {
165
- const poolPath = typeof poolPathOrOptions === "string" ? poolPathOrOptions : undefined;
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 = poolPath || this.defaultPoolPath;
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
- // Note: mkdirSync's mode is masked by the process umask, so we chmod
174
- // explicitly afterwards to guarantee 0o700 regardless of umask.
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
- console.warn(`Warning: No private key found for account ${address}, skipping`);
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
- static loadAccountPool(poolPath) {
212
+ loadAccountPool(poolPath) {
234
213
  if (this.poolLoaded) {
235
- return true; // Already loaded
214
+ return true;
236
215
  }
237
- const basePath = poolPath || this.defaultPoolPath;
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
- console.warn(`Warning: Failed to load account pool: ${msg}`);
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
- static clearPool() {
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
- * Get the current size of the account pool
288
- *
289
- * @returns Number of accounts in the pool
258
+ * Number of accounts in the pool.
290
259
  */
291
- static getPoolSize() {
260
+ getPoolSize() {
292
261
  return this.pool.size;
293
262
  }
294
263
  /**
295
- * Get all accounts in the pool
296
- *
297
- * @returns Array of all Account instances
264
+ * All accounts in the pool.
298
265
  */
299
- static getAllAccounts() {
266
+ getAllAccounts() {
300
267
  return Array.from(this.pool.values());
301
268
  }
302
269
  /**
303
- * Check if a label is already in use
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
- static hasLabel(label) {
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
- static getOrCreateLabeled(label) {
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
- static createBatch(labels) {
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
- static exportPrivateKeys(labels) {
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
- static accountFromPrivateKey(privateKeyHex) {
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
- static trackAccount(account, privateKeyHex, source) {
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
- * AccountManager note: the underlying account pool is a process-wide
17
- * static (see `core/AccountManager.ts`). Two Harness instances in the
18
- * same process share account labels; this is the same constraint that
19
- * already governs `setupTestFixture`.
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 */
@@ -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
- * AccountManager note: the underlying account pool is a process-wide
16
- * static (see `core/AccountManager.ts`). Two Harness instances in the
17
- * same process share account labels; this is the same constraint that
18
- * already governs `setupTestFixture`.
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)
@@ -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
- const account = AccountManager.loadAccountFromPrivateKey(config.privateKey);
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
- return AccountManager.createAccount();
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 = AccountManager.createBatch(accountLabels);
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 = AccountManager.exportPrivateKeys(["deployer"]).deployer;
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 = AccountManager.createBatch(accountLabels);
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 = AccountManager.exportPrivateKeys(["deployer"]).deployer;
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
- * Does **not** clear the shared `AccountManager` pool — clearing it
26
- * would break parallel `setupTestFixture` invocations that share the
27
- * pool. The pool grows for the lifetime of the process; the process
28
- * exit reclaims it.
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 = AccountManager.getLabeledAccounts();
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] || AccountManager.getOrCreateLabeled(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 = AccountManager.getLabeledAccounts();
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] || AccountManager.getOrCreateLabeled(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 = AccountManager.loadAccountsFromConfig(config);
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 AccountManager.createAccount();
79
+ return accountManager.createAccount();
77
80
  };
78
81
  const getAccountHelper = (privateKeyHex) => {
79
- return AccountManager.loadAccountFromPrivateKey(privateKeyHex);
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, AccountManager } from "movehat";
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
- const labeled = AccountManager.getLabeledAccounts();
27
- deployer = labeled.deployer;
28
- alice = labeled.alice;
29
- bob = labeled.bob;
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");
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "movehat",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "type": "module",
5
5
  "description": "Hardhat-like development framework for Movement L1 smart contracts",
6
6
  "bin": {