|
1 | 1 | import { spawnSync } from "child_process"; |
2 | | -import { mkdtempSync, rmSync } from "fs"; |
| 2 | +import { |
| 3 | + mkdtempSync, |
| 4 | + rmSync, |
| 5 | + readFileSync, |
| 6 | + writeFileSync, |
| 7 | + existsSync, |
| 8 | + mkdirSync, |
| 9 | +} from "fs"; |
3 | 10 | import { join } from "path"; |
4 | 11 | import { tmpdir } from "os"; |
5 | | -import { Ed25519PrivateKey } from "@aptos-labs/ts-sdk"; |
| 12 | +import { |
| 13 | + Account, |
| 14 | + Ed25519PrivateKey, |
| 15 | + PrivateKey, |
| 16 | + PrivateKeyVariants, |
| 17 | +} from "@aptos-labs/ts-sdk"; |
| 18 | +import yaml from "js-yaml"; |
6 | 19 |
|
7 | 20 | const APTOS_BINARY = "aptos"; |
8 | 21 |
|
@@ -45,7 +58,7 @@ export function runCommand( |
45 | 58 |
|
46 | 59 | if (result.status !== 0) { |
47 | 60 | throw new Error( |
48 | | - `Process exited with code ${result.status}: ${result.stderr}`, |
| 61 | + `Process exited with code ${result.status}.\n\nStdout:\n${result.stdout}\nStderr:\n${result.stderr}`, |
49 | 62 | ); |
50 | 63 | } |
51 | 64 |
|
@@ -134,51 +147,71 @@ class TestHarness { |
134 | 147 | this.init_cli_profile("default"); |
135 | 148 | this.init_session(options); |
136 | 149 | this.fundAccount("default", 10000000000 /* 100 APT */); |
137 | | - |
138 | | - // Auto-cleanup if running in Jest, Vitest, Jasmine, or Mocha |
139 | | - // |
140 | | - // Jest, Vitest, and Jasmine use `afterAll`, while Mocha uses `after`. |
141 | | - // We check for both and register the cleanup hook accordingly. |
142 | | - const globalAny = globalThis as any; |
143 | | - if (typeof globalAny.afterAll === "function") { |
144 | | - globalAny.afterAll(() => this.cleanup()); |
145 | | - } else if (typeof globalAny.after === "function") { |
146 | | - globalAny.after(() => this.cleanup()); |
147 | | - } |
148 | 150 | } |
149 | 151 |
|
150 | 152 | /** |
151 | 153 | * Initialize the Aptos CLI profile in the temporary directory. |
152 | | - * |
153 | 154 | * If a private key is not provided, a random one will be generated. |
154 | 155 | * |
| 156 | + * This is currently done by appending a new profile to the CLI's config file |
| 157 | + * (`.aptos/config.yaml`) as opposed to running the CLI's `init` command, in order to |
| 158 | + * avoid unnecessary communication with the actual network. |
| 159 | + * |
155 | 160 | * @throws Error if the initialization fails. |
156 | 161 | */ |
157 | 162 | init_cli_profile(profile_name: string, privateKey?: string): void { |
158 | | - const pk = privateKey |
159 | | - ? privateKey |
160 | | - : Ed25519PrivateKey.generate().toHexString(); |
| 163 | + const privKey = privateKey |
| 164 | + ? new Ed25519PrivateKey( |
| 165 | + PrivateKey.formatPrivateKey(privateKey, PrivateKeyVariants.Ed25519), |
| 166 | + ) |
| 167 | + : Ed25519PrivateKey.generate(); |
| 168 | + |
| 169 | + const pubKey = privKey.publicKey(); |
| 170 | + const addr = Account.fromPrivateKey({ |
| 171 | + privateKey: privKey, |
| 172 | + }).accountAddress.toString(); |
| 173 | + |
| 174 | + let profile = { |
| 175 | + network: "Local", |
| 176 | + rest_url: "https://fullnode.dummynetwork.aptoslabs.com", |
| 177 | + account: addr, |
| 178 | + private_key: privKey.toAIP80String(), |
| 179 | + public_key: "ed25519-pub-" + pubKey.toString(), |
| 180 | + }; |
| 181 | + |
| 182 | + const aptosDir = join(this.tempDir, ".aptos"); |
| 183 | + const configPath = join(aptosDir, "config.yaml"); |
| 184 | + |
| 185 | + if (!existsSync(aptosDir)) { |
| 186 | + mkdirSync(aptosDir, { recursive: true }); |
| 187 | + } |
161 | 188 |
|
162 | | - // prettier-ignore |
163 | | - const res = runCommand( |
164 | | - APTOS_BINARY, |
165 | | - [ |
166 | | - "init", |
167 | | - "--profile", profile_name, |
168 | | - "--network", "mainnet", |
169 | | - "--skip-faucet", |
170 | | - "--private-key", pk, |
171 | | - ], |
172 | | - { |
173 | | - cwd: this.tempDir, |
174 | | - }, |
175 | | - ); |
| 189 | + let config: any = { profiles: {} }; |
| 190 | + if (existsSync(configPath)) { |
| 191 | + try { |
| 192 | + const fileContent = readFileSync(configPath, "utf8"); |
| 193 | + config = yaml.load(fileContent) || { profiles: {} }; |
| 194 | + if (!config.profiles) { |
| 195 | + config.profiles = {}; |
| 196 | + } |
| 197 | + } catch (e) { |
| 198 | + throw new Error( |
| 199 | + `Failed to parse existing config at ${configPath}: ${e}`, |
| 200 | + ); |
| 201 | + } |
| 202 | + } |
176 | 203 |
|
177 | | - if (!res || res.Result !== "Success") { |
| 204 | + if (config.profiles[profile_name]) { |
178 | 205 | throw new Error( |
179 | | - `aptos init failed: expected Result = Success, got ${JSON.stringify(res)}`, |
| 206 | + `Profile ${profile_name} already exists in ${configPath}`, |
180 | 207 | ); |
181 | 208 | } |
| 209 | + config.profiles[profile_name] = profile; |
| 210 | + |
| 211 | + writeFileSync( |
| 212 | + configPath, |
| 213 | + yaml.dump(config, { indent: 2, sortKeys: true, lineWidth: 120 }), |
| 214 | + ); |
182 | 215 | } |
183 | 216 |
|
184 | 217 | /** |
@@ -222,7 +255,7 @@ class TestHarness { |
222 | 255 | * |
223 | 256 | * @throws Error if the funding operation fails |
224 | 257 | */ |
225 | | - fundAccount(account: string, amount: number): void { |
| 258 | + fundAccount(account: string, amount: number | bigint | string): void { |
226 | 259 | // prettier-ignore |
227 | 260 | const res = runCommand( |
228 | 261 | APTOS_BINARY, |
@@ -365,6 +398,64 @@ class TestHarness { |
365 | 398 | return res; |
366 | 399 | } |
367 | 400 |
|
| 401 | + /** |
| 402 | + * Views a resource group from the simulation session. |
| 403 | + * |
| 404 | + * @param account The account address or profile name |
| 405 | + * @param resourceGroup The resource group tag (e.g. 0x1::object::ObjectGroup) |
| 406 | + * @param derivedObjectAddress Optional address to derive an object address from |
| 407 | + * @returns The parsed JSON output |
| 408 | + */ |
| 409 | + viewResourceGroup( |
| 410 | + account: string, |
| 411 | + resourceGroup: string, |
| 412 | + derivedObjectAddress?: string, |
| 413 | + ): any { |
| 414 | + const args = [ |
| 415 | + "move", |
| 416 | + "sim", |
| 417 | + "view-resource-group", |
| 418 | + "--session", |
| 419 | + this.getSessionPath(), |
| 420 | + "--account", |
| 421 | + account, |
| 422 | + "--resource-group", |
| 423 | + resourceGroup, |
| 424 | + ]; |
| 425 | + |
| 426 | + if (derivedObjectAddress) { |
| 427 | + args.push("--derived-object-address", derivedObjectAddress); |
| 428 | + } |
| 429 | + |
| 430 | + return runCommand(APTOS_BINARY, args, { cwd: this.tempDir }); |
| 431 | + } |
| 432 | + |
| 433 | + /** |
| 434 | + * Gets the APT balance from the Fungible Store for a given account. |
| 435 | + * |
| 436 | + * Uses the primary store derived from the account and the APT metadata address (0xA). |
| 437 | + * |
| 438 | + * @param account The account address or profile name |
| 439 | + * @returns The balance as a bigint |
| 440 | + */ |
| 441 | + getAPTBalanceFungibleStore(account: string): bigint { |
| 442 | + const res = this.viewResourceGroup( |
| 443 | + account, |
| 444 | + "0x1::object::ObjectGroup", |
| 445 | + "0xA", |
| 446 | + ); |
| 447 | + |
| 448 | + if ( |
| 449 | + !res || |
| 450 | + !res.Result || |
| 451 | + !res.Result["0x1::fungible_asset::FungibleStore"] |
| 452 | + ) { |
| 453 | + return BigInt(0); |
| 454 | + } |
| 455 | + |
| 456 | + return BigInt(res.Result["0x1::fungible_asset::FungibleStore"].balance); |
| 457 | + } |
| 458 | + |
368 | 459 | cleanup(): void { |
369 | 460 | try { |
370 | 461 | rmSync(this.tempDir, { recursive: true, force: true }); |
|
0 commit comments