movehat 0.2.8 → 0.3.0
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/README.md +2 -0
- package/dist/cli.js +7 -2
- package/dist/core/Publisher.d.ts +20 -1
- package/dist/core/Publisher.js +167 -89
- package/dist/core/contract.d.ts +3 -2
- package/dist/core/contract.js +38 -3
- package/dist/core/trace/client.d.ts +18 -0
- package/dist/core/trace/client.js +58 -0
- package/dist/core/trace/renderer.d.ts +12 -0
- package/dist/core/trace/renderer.js +197 -0
- package/dist/core/trace/types.d.ts +68 -0
- package/dist/core/trace/types.js +6 -0
- package/dist/helpers/setupLocalTesting.js +21 -2
- package/dist/node/LocalNodeManager.js +1 -1
- package/dist/runtime.d.ts +8 -0
- package/dist/runtime.js +3 -1
- package/dist/types/runtime.d.ts +6 -0
- package/dist/ui/logger.d.ts +16 -0
- package/dist/ui/logger.js +24 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
- **Hardhat-style Harness API** — `Harness.createLocal`, `createFork`, `createLive` factory methods with explicit lifecycle (`cleanup()`) and use-after-cleanup safety (Proxy poisoning).
|
|
22
22
|
- **Three execution modes** — full local blockchain, read-only fork of a remote network, or live testnet/mainnet binding.
|
|
23
23
|
- **Auto-deploy in tests + auto-detect named addresses** — contracts compile and deploy automatically; no manual address wiring.
|
|
24
|
+
- **Fast local boot with movelite** — on supported platforms an auto-installed [movelite](https://github.com/gilbertsahumada/movelite) binary boots the local test chain in under a second instead of ~15s, with transparent fallback to the full Movement node.
|
|
25
|
+
- **Foundry-style execution traces** — raise verbosity (`-vv` … `-vvvv`) to render an indented call tree for each `contract.call(...)` with decoded arguments, gas, events, and the abort stack (movelite backend).
|
|
24
26
|
- **Native fork system** — local JSON-backed snapshots of Movement L1 state, no BCS compatibility issues.
|
|
25
27
|
- **TypeScript-first** — single `PRIVATE_KEY` across all networks (Hardhat-style); deployments tracked per-network in `deployments/`.
|
|
26
28
|
- **SLSA-provenance releases** — every npm release ships with [Trusted Publishers](https://docs.npmjs.com/trusted-publishers) provenance. Verify with `npm view movehat@<version>`.
|
package/dist/cli.js
CHANGED
|
@@ -44,7 +44,7 @@ program
|
|
|
44
44
|
.version(version)
|
|
45
45
|
.option('--network <name>', 'Network to use (testnet, mainnet, local, etc.)')
|
|
46
46
|
.option('--redeploy', 'Force redeploy even if already deployed')
|
|
47
|
-
.option('-v, --verbose', '
|
|
47
|
+
.option('-v, --verbose', 'Increase output verbosity (repeatable: -v subprocess output, -vv..-vvvv transaction traces on movelite)', (_value, previous) => previous + 1, 0)
|
|
48
48
|
.hook('preAction', (thisCommand) => {
|
|
49
49
|
// Store network option in environment for commands to access
|
|
50
50
|
const options = thisCommand.opts();
|
|
@@ -54,8 +54,13 @@ program
|
|
|
54
54
|
if (options.redeploy) {
|
|
55
55
|
process.env.MH_CLI_REDEPLOY = 'true';
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
// `-v` is counted (0..4). Level >= 1 keeps the legacy MOVEHAT_VERBOSE=1
|
|
58
|
+
// contract (isVerbose + its callers); MOVEHAT_VERBOSITY carries the
|
|
59
|
+
// numeric level across the spawned test/script subprocess boundary.
|
|
60
|
+
const level = options.verbose ?? 0;
|
|
61
|
+
if (level >= 1) {
|
|
58
62
|
process.env.MOVEHAT_VERBOSE = '1';
|
|
63
|
+
process.env.MOVEHAT_VERBOSITY = String(level);
|
|
59
64
|
}
|
|
60
65
|
});
|
|
61
66
|
program
|
package/dist/core/Publisher.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Account } from "@aptos-labs/ts-sdk";
|
|
1
|
+
import { Account, Aptos } from "@aptos-labs/ts-sdk";
|
|
2
2
|
import { MovehatConfig } from "../types/config.js";
|
|
3
3
|
import { DeploymentInfo } from "./deployments.js";
|
|
4
4
|
import type { ChildProcessAdapter } from "../utils/childProcessAdapter.js";
|
|
@@ -12,6 +12,13 @@ export interface PublishInput {
|
|
|
12
12
|
config: MovehatConfig;
|
|
13
13
|
account: Account;
|
|
14
14
|
packageDir?: string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Publish via the TypeScript SDK instead of the Movement CLI. Set when the
|
|
17
|
+
* backend is movelite, whose REST responses the Movement CLI cannot parse.
|
|
18
|
+
* Requires `aptos`.
|
|
19
|
+
*/
|
|
20
|
+
sdkPublish?: boolean | undefined;
|
|
21
|
+
aptos?: Aptos | undefined;
|
|
15
22
|
}
|
|
16
23
|
/**
|
|
17
24
|
* Publishes a Move module via the Movement CLI.
|
|
@@ -22,4 +29,16 @@ export declare class Publisher {
|
|
|
22
29
|
private readonly deps;
|
|
23
30
|
constructor(deps?: PublisherDeps);
|
|
24
31
|
deploy(input: PublishInput): Promise<DeploymentInfo>;
|
|
32
|
+
/**
|
|
33
|
+
* Publish via the Movement CLI (`movement move publish`). The default path
|
|
34
|
+
* for real Movement nodes, forks, and testnet. Returns the parsed tx hash.
|
|
35
|
+
*/
|
|
36
|
+
private publishViaCli;
|
|
37
|
+
/**
|
|
38
|
+
* Publish the already-built package via the TypeScript SDK. Used when the
|
|
39
|
+
* backend is movelite. Reads the compiled artifacts the CLI build produced
|
|
40
|
+
* under `<dir>/build/<pkg>/`, submits a `0x1::code::publish_package_txn`,
|
|
41
|
+
* and waits for it. Returns the on-chain tx hash.
|
|
42
|
+
*/
|
|
43
|
+
private publishViaSdk;
|
|
25
44
|
}
|
package/dist/core/Publisher.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { PrivateKey, PrivateKeyVariants } from "@aptos-labs/ts-sdk";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
2
4
|
import { extractNamedAddresses } from "../commands/compile.js";
|
|
3
5
|
import { saveDeployment, loadDeployment, validateSafeName, } from "./deployments.js";
|
|
4
6
|
import { validatePathSafety } from "./shell.js";
|
|
@@ -74,103 +76,34 @@ export class Publisher {
|
|
|
74
76
|
.join(","),
|
|
75
77
|
]
|
|
76
78
|
: [];
|
|
79
|
+
// The SDK publish path reads package-metadata.bcs from the build
|
|
80
|
+
// output; `--save-metadata` makes the build emit it. The CLI path
|
|
81
|
+
// doesn't need it (`move publish` rebuilds metadata internally).
|
|
82
|
+
const saveMetadataArgs = input.sdkPublish ? ["--save-metadata"] : [];
|
|
77
83
|
// Build first with named addresses
|
|
78
84
|
const buildResult = await withSpinner("Building package", () => runCli({
|
|
79
85
|
command: "movement",
|
|
80
|
-
args: [
|
|
86
|
+
args: [
|
|
87
|
+
"move",
|
|
88
|
+
"build",
|
|
89
|
+
"--package-dir",
|
|
90
|
+
safeDir,
|
|
91
|
+
...namedAddrArgs,
|
|
92
|
+
...saveMetadataArgs,
|
|
93
|
+
],
|
|
81
94
|
timeoutMs: 120000, // 2 minutes for git dependency downloads
|
|
82
95
|
}, { adapter: this.deps.adapter }));
|
|
83
96
|
if (isVerbose() && buildResult.stdout) {
|
|
84
97
|
logger.info(buildResult.stdout.trim(), 2);
|
|
85
98
|
}
|
|
86
|
-
// Publish
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
// is
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
// leaving the user's file mutated if the process died before the
|
|
95
|
-
// restore step.
|
|
96
|
-
let publishOut = "";
|
|
97
|
-
let publishErr = "";
|
|
98
|
-
// Pass the private key to Movement CLI via a 0o600 temp file
|
|
99
|
-
// (`--private-key-file <path>`) and the on-chain address via
|
|
100
|
-
// `--sender-account <addr>`. This avoids the CLI's profile-yaml
|
|
101
|
-
// lookup chain entirely — no CWD / HOME / .aptos / .movement
|
|
102
|
-
// dance, no CLI-variant dependency.
|
|
103
|
-
const keyFilePath = writeTempKeyFile(formattedPrivateKey);
|
|
104
|
-
// Register a sync cleanup hook BEFORE invoking the CLI. If the
|
|
105
|
-
// user Ctrl+C's (or the process is SIGTERM'd) between the file
|
|
106
|
-
// write and our finally, the SIGINT handler iterates every
|
|
107
|
-
// registered callback and unlinks this deploy's key file
|
|
108
|
-
// synchronously so the private key never persists on disk after
|
|
109
|
-
// an abnormal exit. The signal-handler path uses the
|
|
110
|
-
// best-effort variant because the event loop is dead and we
|
|
111
|
-
// cannot logger.warning.
|
|
112
|
-
ensureSignalHandler();
|
|
113
|
-
const syncCleanup = () => removeKeyFileSyncBestEffort(keyFilePath);
|
|
114
|
-
cleanupCallbacks.add(syncCleanup);
|
|
115
|
-
try {
|
|
116
|
-
// Execute publish command. Private key reaches the CLI via the
|
|
117
|
-
// temp key file path (--private-key-file) — never on the
|
|
118
|
-
// command line — so it can't leak through `ps aux`. runCli's
|
|
119
|
-
// stdout/stderr redaction still applies as defense in depth
|
|
120
|
-
// for any `ed25519-priv-…` substring that surfaces in CLI
|
|
121
|
-
// output (Movement CLI sometimes echoes the key on error).
|
|
122
|
-
const publishResult = await withSpinner("Publishing to blockchain", () => runCli({
|
|
123
|
-
command: "movement",
|
|
124
|
-
args: [
|
|
125
|
-
"move",
|
|
126
|
-
"publish",
|
|
127
|
-
"--package-dir",
|
|
128
|
-
safeDir,
|
|
129
|
-
"--url",
|
|
130
|
-
config.rpc,
|
|
131
|
-
"--private-key-file",
|
|
132
|
-
keyFilePath,
|
|
133
|
-
"--sender-account",
|
|
134
|
-
deployerAddress,
|
|
135
|
-
"--assume-yes",
|
|
136
|
-
...namedAddrArgs,
|
|
137
|
-
],
|
|
138
|
-
timeoutMs: 120000, // 2 minutes for blockchain transactions
|
|
139
|
-
}, { adapter: this.deps.adapter }));
|
|
140
|
-
publishOut = publishResult.stdout;
|
|
141
|
-
publishErr = publishResult.stderr;
|
|
142
|
-
// Both stdout and stderr from the publish subprocess are gated
|
|
143
|
-
// behind isVerbose() — Movement CLI emits progress to both
|
|
144
|
-
// streams ("Compiling, may take a little while..."), so a
|
|
145
|
-
// visible stderr line is not by itself a failure signal. The
|
|
146
|
-
// surrounding withSpinner converts the runCli throw on real
|
|
147
|
-
// failure into the visible spinner.fail() output instead.
|
|
148
|
-
if (isVerbose() && publishOut)
|
|
149
|
-
logger.info(publishOut.trim(), 2);
|
|
150
|
-
if (isVerbose() && publishErr)
|
|
151
|
-
logger.info(publishErr.trim(), 2);
|
|
152
|
-
}
|
|
153
|
-
finally {
|
|
154
|
-
// Unlink the temp key file via the observable cleanup helper.
|
|
155
|
-
// ENOENT and other already-gone outcomes are benign (null).
|
|
156
|
-
// A non-null Error means the unlink failed AND the file still
|
|
157
|
-
// exists on disk — the private key would persist silently
|
|
158
|
-
// otherwise, so we emit a warning with the manual-cleanup
|
|
159
|
-
// hint. The SIGINT signal handler's sync callback below also
|
|
160
|
-
// tries to remove the same file; if SIGINT fires before this
|
|
161
|
-
// finally runs the file is gone and the next finally call
|
|
162
|
-
// sees ENOENT (benign).
|
|
163
|
-
const cleanupErr = removeKeyFile(keyFilePath);
|
|
164
|
-
if (cleanupErr) {
|
|
165
|
-
logger.warning(`Failed to remove temp key file '${keyFilePath}': ${cleanupErr.message}. ` +
|
|
166
|
-
`The file has mode 0o600 but should be removed manually: rm ${keyFilePath}`);
|
|
167
|
-
}
|
|
168
|
-
cleanupCallbacks.delete(syncCleanup);
|
|
169
|
-
}
|
|
170
|
-
// Extract transaction hash from output via the shared helper
|
|
171
|
-
// (`utils/parseCliOutput.ts`). Same regex pair as before; lifted
|
|
172
|
-
// for reuse by harness/codeObject.ts and harness/script.ts.
|
|
173
|
-
const txHash = parseTxHash(publishOut);
|
|
99
|
+
// Publish the freshly-built package. movelite cannot consume the
|
|
100
|
+
// Movement CLI's `move publish` REST flow (its responses omit the
|
|
101
|
+
// ledger headers and fields the CLI requires), so when the backend
|
|
102
|
+
// is movelite we publish via the TypeScript SDK instead. Every other
|
|
103
|
+
// backend keeps the CLI path.
|
|
104
|
+
const txHash = input.sdkPublish
|
|
105
|
+
? await this.publishViaSdk(input, safeDir)
|
|
106
|
+
: await this.publishViaCli(config, safeDir, deployerAddress, namedAddrArgs);
|
|
174
107
|
logger.success("Module published successfully!");
|
|
175
108
|
// ←← "Publish succeeded" boundary. Anything thrown below this
|
|
176
109
|
// point did NOT cause the publish to fail — the module is on
|
|
@@ -226,4 +159,149 @@ export class Publisher {
|
|
|
226
159
|
throw error;
|
|
227
160
|
}
|
|
228
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Publish via the Movement CLI (`movement move publish`). The default path
|
|
164
|
+
* for real Movement nodes, forks, and testnet. Returns the parsed tx hash.
|
|
165
|
+
*/
|
|
166
|
+
async publishViaCli(config, safeDir, deployerAddress, namedAddrArgs) {
|
|
167
|
+
// Format the private key into AIP-80 shape so the Movement CLI
|
|
168
|
+
// doesn't emit its raw-hex deprecation warning. `formatPrivateKey`
|
|
169
|
+
// is idempotent for already-prefixed inputs.
|
|
170
|
+
const formattedPrivateKey = PrivateKey.formatPrivateKey(config.privateKey, PrivateKeyVariants.Ed25519);
|
|
171
|
+
// Move.toml is NOT mutated. All address overrides flow through
|
|
172
|
+
// the `--named-addresses` flag above, which Movement CLI applies
|
|
173
|
+
// during build + publish. Rewriting Move.toml on disk would risk
|
|
174
|
+
// leaving the user's file mutated if the process died before the
|
|
175
|
+
// restore step.
|
|
176
|
+
let publishOut = "";
|
|
177
|
+
let publishErr = "";
|
|
178
|
+
// Pass the private key to Movement CLI via a 0o600 temp file
|
|
179
|
+
// (`--private-key-file <path>`) and the on-chain address via
|
|
180
|
+
// `--sender-account <addr>`. This avoids the CLI's profile-yaml
|
|
181
|
+
// lookup chain entirely — no CWD / HOME / .aptos / .movement
|
|
182
|
+
// dance, no CLI-variant dependency.
|
|
183
|
+
const keyFilePath = writeTempKeyFile(formattedPrivateKey);
|
|
184
|
+
// Register a sync cleanup hook BEFORE invoking the CLI. If the
|
|
185
|
+
// user Ctrl+C's (or the process is SIGTERM'd) between the file
|
|
186
|
+
// write and our finally, the SIGINT handler iterates every
|
|
187
|
+
// registered callback and unlinks this deploy's key file
|
|
188
|
+
// synchronously so the private key never persists on disk after
|
|
189
|
+
// an abnormal exit. The signal-handler path uses the
|
|
190
|
+
// best-effort variant because the event loop is dead and we
|
|
191
|
+
// cannot logger.warning.
|
|
192
|
+
ensureSignalHandler();
|
|
193
|
+
const syncCleanup = () => removeKeyFileSyncBestEffort(keyFilePath);
|
|
194
|
+
cleanupCallbacks.add(syncCleanup);
|
|
195
|
+
try {
|
|
196
|
+
// Execute publish command. Private key reaches the CLI via the
|
|
197
|
+
// temp key file path (--private-key-file) — never on the
|
|
198
|
+
// command line — so it can't leak through `ps aux`. runCli's
|
|
199
|
+
// stdout/stderr redaction still applies as defense in depth
|
|
200
|
+
// for any `ed25519-priv-…` substring that surfaces in CLI
|
|
201
|
+
// output (Movement CLI sometimes echoes the key on error).
|
|
202
|
+
const publishResult = await withSpinner("Publishing to blockchain", () => runCli({
|
|
203
|
+
command: "movement",
|
|
204
|
+
args: [
|
|
205
|
+
"move",
|
|
206
|
+
"publish",
|
|
207
|
+
"--package-dir",
|
|
208
|
+
safeDir,
|
|
209
|
+
"--url",
|
|
210
|
+
config.rpc,
|
|
211
|
+
"--private-key-file",
|
|
212
|
+
keyFilePath,
|
|
213
|
+
"--sender-account",
|
|
214
|
+
deployerAddress,
|
|
215
|
+
"--assume-yes",
|
|
216
|
+
...namedAddrArgs,
|
|
217
|
+
],
|
|
218
|
+
timeoutMs: 120000, // 2 minutes for blockchain transactions
|
|
219
|
+
}, { adapter: this.deps.adapter }));
|
|
220
|
+
publishOut = publishResult.stdout;
|
|
221
|
+
publishErr = publishResult.stderr;
|
|
222
|
+
// Both stdout and stderr from the publish subprocess are gated
|
|
223
|
+
// behind isVerbose() — Movement CLI emits progress to both
|
|
224
|
+
// streams ("Compiling, may take a little while..."), so a
|
|
225
|
+
// visible stderr line is not by itself a failure signal. The
|
|
226
|
+
// surrounding withSpinner converts the runCli throw on real
|
|
227
|
+
// failure into the visible spinner.fail() output instead.
|
|
228
|
+
if (isVerbose() && publishOut)
|
|
229
|
+
logger.info(publishOut.trim(), 2);
|
|
230
|
+
if (isVerbose() && publishErr)
|
|
231
|
+
logger.info(publishErr.trim(), 2);
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
// Unlink the temp key file via the observable cleanup helper.
|
|
235
|
+
// ENOENT and other already-gone outcomes are benign (null).
|
|
236
|
+
// A non-null Error means the unlink failed AND the file still
|
|
237
|
+
// exists on disk — the private key would persist silently
|
|
238
|
+
// otherwise, so we emit a warning with the manual-cleanup
|
|
239
|
+
// hint. The SIGINT signal handler's sync callback below also
|
|
240
|
+
// tries to remove the same file; if SIGINT fires before this
|
|
241
|
+
// finally runs the file is gone and the next finally call
|
|
242
|
+
// sees ENOENT (benign).
|
|
243
|
+
const cleanupErr = removeKeyFile(keyFilePath);
|
|
244
|
+
if (cleanupErr) {
|
|
245
|
+
logger.warning(`Failed to remove temp key file '${keyFilePath}': ${cleanupErr.message}. ` +
|
|
246
|
+
`The file has mode 0o600 but should be removed manually: rm ${keyFilePath}`);
|
|
247
|
+
}
|
|
248
|
+
cleanupCallbacks.delete(syncCleanup);
|
|
249
|
+
}
|
|
250
|
+
// Extract transaction hash from output via the shared helper
|
|
251
|
+
// (`utils/parseCliOutput.ts`). Same regex pair as before; lifted
|
|
252
|
+
// for reuse by harness/codeObject.ts and harness/script.ts.
|
|
253
|
+
return parseTxHash(publishOut);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Publish the already-built package via the TypeScript SDK. Used when the
|
|
257
|
+
* backend is movelite. Reads the compiled artifacts the CLI build produced
|
|
258
|
+
* under `<dir>/build/<pkg>/`, submits a `0x1::code::publish_package_txn`,
|
|
259
|
+
* and waits for it. Returns the on-chain tx hash.
|
|
260
|
+
*/
|
|
261
|
+
async publishViaSdk(input, safeDir) {
|
|
262
|
+
const aptos = input.aptos;
|
|
263
|
+
if (!aptos) {
|
|
264
|
+
throw new Error("sdkPublish requires an Aptos client");
|
|
265
|
+
}
|
|
266
|
+
const buildRoot = join(safeDir, "build");
|
|
267
|
+
// The root package's compiled output is the single directory under
|
|
268
|
+
// build/ that carries a package-metadata.bcs; dependency builds live
|
|
269
|
+
// in nested bytecode_modules/dependencies/ and have no metadata here.
|
|
270
|
+
const pkgDirs = existsSync(buildRoot)
|
|
271
|
+
? readdirSync(buildRoot, { withFileTypes: true })
|
|
272
|
+
.filter((e) => e.isDirectory())
|
|
273
|
+
.map((e) => join(buildRoot, e.name))
|
|
274
|
+
.filter((d) => existsSync(join(d, "package-metadata.bcs")))
|
|
275
|
+
: [];
|
|
276
|
+
if (pkgDirs.length !== 1) {
|
|
277
|
+
throw new Error(`Expected exactly one compiled package under ${buildRoot}, found ${pkgDirs.length}.`);
|
|
278
|
+
}
|
|
279
|
+
const pkgDir = pkgDirs[0];
|
|
280
|
+
const metadataBytes = new Uint8Array(readFileSync(join(pkgDir, "package-metadata.bcs")));
|
|
281
|
+
const modulesDir = join(pkgDir, "bytecode_modules");
|
|
282
|
+
const moduleBytecode = readdirSync(modulesDir)
|
|
283
|
+
.filter((f) => f.endsWith(".mv"))
|
|
284
|
+
.sort()
|
|
285
|
+
.map((f) => new Uint8Array(readFileSync(join(modulesDir, f))));
|
|
286
|
+
if (moduleBytecode.length === 0) {
|
|
287
|
+
throw new Error(`No compiled modules (*.mv) found in ${modulesDir}`);
|
|
288
|
+
}
|
|
289
|
+
return withSpinner("Publishing to blockchain", async () => {
|
|
290
|
+
const tx = await aptos.publishPackageTransaction({
|
|
291
|
+
account: input.account.accountAddress,
|
|
292
|
+
metadataBytes,
|
|
293
|
+
moduleBytecode,
|
|
294
|
+
});
|
|
295
|
+
const senderAuth = aptos.transaction.sign({
|
|
296
|
+
signer: input.account,
|
|
297
|
+
transaction: tx,
|
|
298
|
+
});
|
|
299
|
+
const committed = await aptos.transaction.submit.simple({
|
|
300
|
+
transaction: tx,
|
|
301
|
+
senderAuthenticator: senderAuth,
|
|
302
|
+
});
|
|
303
|
+
await aptos.waitForTransaction({ transactionHash: committed.hash });
|
|
304
|
+
return committed.hash;
|
|
305
|
+
});
|
|
306
|
+
}
|
|
229
307
|
}
|
package/dist/core/contract.d.ts
CHANGED
|
@@ -8,9 +8,10 @@ export declare class MoveContract {
|
|
|
8
8
|
private aptos;
|
|
9
9
|
private moduleAddress;
|
|
10
10
|
private moduleName;
|
|
11
|
-
|
|
11
|
+
private traceRpcUrl?;
|
|
12
|
+
constructor(aptos: Aptos, moduleAddress: string, moduleName: string, traceRpcUrl?: string | undefined);
|
|
12
13
|
call(signer: Account, functionName: string, args?: any[], typeArgs?: string[]): Promise<TransactionResult>;
|
|
13
14
|
view<T = unknown>(functionName: string, args?: any[], typeArgs?: string[]): Promise<T>;
|
|
14
15
|
getModuleId(): string;
|
|
15
16
|
}
|
|
16
|
-
export declare function getContract(aptos: Aptos, moduleAddress: string, moduleName: string): MoveContract;
|
|
17
|
+
export declare function getContract(aptos: Aptos, moduleAddress: string, moduleName: string, traceRpcUrl?: string): MoveContract;
|
package/dist/core/contract.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { logger } from "../ui/index.js";
|
|
2
|
+
import { traceTransaction } from "./trace/client.js";
|
|
3
|
+
import { renderTrace } from "./trace/renderer.js";
|
|
2
4
|
export class MoveContract {
|
|
3
5
|
aptos;
|
|
4
6
|
moduleAddress;
|
|
5
7
|
moduleName;
|
|
6
|
-
|
|
8
|
+
traceRpcUrl;
|
|
9
|
+
constructor(aptos, moduleAddress, moduleName,
|
|
10
|
+
// movelite RPC base (ending in `/v1`). Presence enables Foundry-style
|
|
11
|
+
// execution traces at verbosity level >= 2. Undefined on the Movement node
|
|
12
|
+
// (no trace endpoint) — calls use the normal submit path.
|
|
13
|
+
traceRpcUrl) {
|
|
7
14
|
this.aptos = aptos;
|
|
8
15
|
this.moduleAddress = moduleAddress;
|
|
9
16
|
this.moduleName = moduleName;
|
|
17
|
+
this.traceRpcUrl = traceRpcUrl;
|
|
10
18
|
}
|
|
11
19
|
async call(signer, functionName,
|
|
12
20
|
// any[]: Move entry-function arguments are heterogeneous primitives
|
|
@@ -29,6 +37,33 @@ export class MoveContract {
|
|
|
29
37
|
signer,
|
|
30
38
|
transaction,
|
|
31
39
|
});
|
|
40
|
+
// Trace path: on movelite at raised verbosity, route through the
|
|
41
|
+
// instrumented `/transactions/trace?commit=true` endpoint, which executes
|
|
42
|
+
// AND commits in one pass (so we must NOT also submit), then render the
|
|
43
|
+
// returned call tree.
|
|
44
|
+
const traceLevel = logger.getVerbosityLevel();
|
|
45
|
+
if (this.traceRpcUrl && traceLevel >= 2) {
|
|
46
|
+
const { response, elapsedMs } = await traceTransaction({
|
|
47
|
+
rpcUrl: this.traceRpcUrl,
|
|
48
|
+
transaction,
|
|
49
|
+
senderAuthenticator: signature,
|
|
50
|
+
});
|
|
51
|
+
// A render failure must never fail a transaction that already committed.
|
|
52
|
+
try {
|
|
53
|
+
renderTrace(response, { level: traceLevel, elapsedMs });
|
|
54
|
+
}
|
|
55
|
+
catch (renderError) {
|
|
56
|
+
const msg = renderError instanceof Error ? renderError.message : String(renderError);
|
|
57
|
+
logger.warning(`Failed to render trace: ${msg}`);
|
|
58
|
+
}
|
|
59
|
+
logger.success(`Transaction ${response.txn_hash} committed with status: ${response.vm_status}`);
|
|
60
|
+
logger.newline();
|
|
61
|
+
return {
|
|
62
|
+
hash: response.txn_hash,
|
|
63
|
+
success: response.success,
|
|
64
|
+
vm_status: response.vm_status,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
32
67
|
const committedTxn = await this.aptos.transaction.submit.simple({
|
|
33
68
|
transaction,
|
|
34
69
|
senderAuthenticator: signature,
|
|
@@ -61,6 +96,6 @@ export class MoveContract {
|
|
|
61
96
|
return `${this.moduleAddress}::${this.moduleName}`;
|
|
62
97
|
}
|
|
63
98
|
}
|
|
64
|
-
export function getContract(aptos, moduleAddress, moduleName) {
|
|
65
|
-
return new MoveContract(aptos, moduleAddress, moduleName);
|
|
99
|
+
export function getContract(aptos, moduleAddress, moduleName, traceRpcUrl) {
|
|
100
|
+
return new MoveContract(aptos, moduleAddress, moduleName, traceRpcUrl);
|
|
66
101
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type AccountAuthenticator, type AnyRawTransaction } from "@aptos-labs/ts-sdk";
|
|
2
|
+
import type { TraceResponse } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Execute a signed transaction through movelite's instrumented VM and return
|
|
5
|
+
* the Foundry-style call tree. `commit=true` runs the trace AND commits in a
|
|
6
|
+
* single pass, so this is the sole execution — the caller must NOT also submit.
|
|
7
|
+
*
|
|
8
|
+
* @param rpcUrl movelite RPC base, already ending in `/v1`.
|
|
9
|
+
* @throws if the endpoint returns a non-2xx status (a submission failure).
|
|
10
|
+
*/
|
|
11
|
+
export declare function traceTransaction(args: {
|
|
12
|
+
rpcUrl: string;
|
|
13
|
+
transaction: AnyRawTransaction;
|
|
14
|
+
senderAuthenticator: AccountAuthenticator;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
response: TraceResponse;
|
|
17
|
+
elapsedMs: number;
|
|
18
|
+
}>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { generateSignedTransaction, } from "@aptos-labs/ts-sdk";
|
|
2
|
+
/** Content type movelite requires for BCS-signed transaction bodies. */
|
|
3
|
+
const BCS_SIGNED_TXN_CONTENT_TYPE = "application/x.aptos.signed_transaction+bcs";
|
|
4
|
+
/**
|
|
5
|
+
* Pull a readable detail out of a movelite error body. movelite (>= 0.2.1)
|
|
6
|
+
* returns structured JSON errors (`{ message, error_code, vm_error_code }`);
|
|
7
|
+
* older versions return plain text. Returns the `message` plus any codes when
|
|
8
|
+
* the body is JSON, otherwise the raw text unchanged.
|
|
9
|
+
*/
|
|
10
|
+
function extractErrorDetail(body) {
|
|
11
|
+
if (!body)
|
|
12
|
+
return "";
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(body);
|
|
15
|
+
if (parsed && typeof parsed.message === "string") {
|
|
16
|
+
const codes = [parsed.error_code, parsed.vm_error_code].filter((c) => c !== undefined && c !== null);
|
|
17
|
+
return codes.length > 0
|
|
18
|
+
? `${parsed.message} (${codes.join(", ")})`
|
|
19
|
+
: parsed.message;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Not JSON (e.g. older movelite plain-text errors) — fall through.
|
|
24
|
+
}
|
|
25
|
+
return body;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Execute a signed transaction through movelite's instrumented VM and return
|
|
29
|
+
* the Foundry-style call tree. `commit=true` runs the trace AND commits in a
|
|
30
|
+
* single pass, so this is the sole execution — the caller must NOT also submit.
|
|
31
|
+
*
|
|
32
|
+
* @param rpcUrl movelite RPC base, already ending in `/v1`.
|
|
33
|
+
* @throws if the endpoint returns a non-2xx status (a submission failure).
|
|
34
|
+
*/
|
|
35
|
+
export async function traceTransaction(args) {
|
|
36
|
+
const { rpcUrl, transaction, senderAuthenticator } = args;
|
|
37
|
+
const bytes = generateSignedTransaction({ transaction, senderAuthenticator });
|
|
38
|
+
const url = `${rpcUrl}/transactions/trace?commit=true`;
|
|
39
|
+
const start = performance.now();
|
|
40
|
+
const res = await fetch(url, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
// No Accept header (response is JSON); no auth header (movelite runs --no-auth).
|
|
43
|
+
// Copy into a fresh ArrayBuffer-backed Uint8Array: the SDK returns
|
|
44
|
+
// `Uint8Array<ArrayBufferLike>`, which the fetch `BodyInit` type rejects
|
|
45
|
+
// under this TS lib config (the ArrayBuffer / SharedArrayBuffer split).
|
|
46
|
+
headers: { "Content-Type": BCS_SIGNED_TXN_CONTENT_TYPE },
|
|
47
|
+
body: new Uint8Array(bytes),
|
|
48
|
+
});
|
|
49
|
+
const elapsedMs = performance.now() - start;
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
const body = await res.text().catch(() => "");
|
|
52
|
+
const detail = extractErrorDetail(body);
|
|
53
|
+
throw new Error(`movelite trace request failed (${res.status} ${res.statusText})` +
|
|
54
|
+
(detail ? `: ${detail}` : ""));
|
|
55
|
+
}
|
|
56
|
+
const response = (await res.json());
|
|
57
|
+
return { response, elapsedMs };
|
|
58
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TraceResponse } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Pure formatter — turns a trace into display lines. Snapshot-testable without
|
|
4
|
+
* touching stdout. `level` is the verbosity level (2..4); per-frame `gas` is in
|
|
5
|
+
* internal VM units while the footer `gas_used` is in octas (never mixed).
|
|
6
|
+
*/
|
|
7
|
+
export declare function formatTraceLines(response: TraceResponse, level: number, elapsedMs: number): string[];
|
|
8
|
+
/** Render a trace to the terminal. Wraps {@link formatTraceLines}. */
|
|
9
|
+
export declare function renderTrace(response: TraceResponse, opts: {
|
|
10
|
+
level: number;
|
|
11
|
+
elapsedMs: number;
|
|
12
|
+
}): void;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { logger } from "../../ui/index.js";
|
|
2
|
+
import { colors, rgbToAnsi, shouldUseColor } from "../../ui/colors.js";
|
|
3
|
+
import { symbols } from "../../ui/symbols.js";
|
|
4
|
+
// rgbToAnsi emits raw escapes unconditionally, so guard these two ourselves
|
|
5
|
+
// (unlike colors.*, which already no-op when color is disabled).
|
|
6
|
+
const orange = (s) => shouldUseColor() ? `${rgbToAnsi(255, 165, 0)}${s}\x1b[0m` : s;
|
|
7
|
+
const brightBlue = (s) => shouldUseColor() ? `${rgbToAnsi(90, 170, 255)}${s}\x1b[0m` : s;
|
|
8
|
+
const indent = (depth) => " ".repeat(depth);
|
|
9
|
+
/** Shorten a 0x-prefixed address for display (`0xf903..9b16`); leave short
|
|
10
|
+
* framework addresses like `0x1` untouched. */
|
|
11
|
+
const shortAddr = (addr) => addr.startsWith("0x") && addr.length > 12
|
|
12
|
+
? `${addr.slice(0, 6)}..${addr.slice(-4)}`
|
|
13
|
+
: addr;
|
|
14
|
+
/** Shorten the address part of a `address::module[::Name]` path. */
|
|
15
|
+
const shortenPath = (path) => {
|
|
16
|
+
const [addr, ...rest] = path.split("::");
|
|
17
|
+
if (addr === undefined || rest.length === 0)
|
|
18
|
+
return path;
|
|
19
|
+
return [shortAddr(addr), ...rest].join("::");
|
|
20
|
+
};
|
|
21
|
+
const isFramework = (module) => module !== null && module.startsWith("0x1::");
|
|
22
|
+
/** Visible at level 3 (user-module tree): not a framework frame, not a native. */
|
|
23
|
+
const isUserFrame = (node) => !isFramework(node.module) && node.kind !== "native";
|
|
24
|
+
const NUMERIC = /^\d+$/;
|
|
25
|
+
const leafValue = (value) => {
|
|
26
|
+
const s = String(value);
|
|
27
|
+
if (NUMERIC.test(s))
|
|
28
|
+
return orange(s);
|
|
29
|
+
if (s.startsWith("0x") && s.length > 12)
|
|
30
|
+
return shortAddr(s);
|
|
31
|
+
return s;
|
|
32
|
+
};
|
|
33
|
+
/** Format a decoded value. Struct (and vector) values arrive as objects with
|
|
34
|
+
* by-index keys; their fields can themselves be structs, so recurse rather
|
|
35
|
+
* than `String()`-ing a nested object into `[object Object]`. */
|
|
36
|
+
const formatValue = (value) => {
|
|
37
|
+
if (value !== null && typeof value === "object") {
|
|
38
|
+
return `{ ${Object.values(value)
|
|
39
|
+
.map(formatValue)
|
|
40
|
+
.join(", ")} }`;
|
|
41
|
+
}
|
|
42
|
+
return leafValue(value);
|
|
43
|
+
};
|
|
44
|
+
const formatArgValue = (arg) => {
|
|
45
|
+
if (arg.value === null)
|
|
46
|
+
return "()";
|
|
47
|
+
return formatValue(arg.value);
|
|
48
|
+
};
|
|
49
|
+
const formatArgs = (args) => args.map(formatArgValue).join(", ");
|
|
50
|
+
const frameName = (node) => {
|
|
51
|
+
const base = node.module
|
|
52
|
+
? `${shortenPath(node.module)}::${node.function ?? "?"}`
|
|
53
|
+
: node.function ?? `<${node.kind}>`;
|
|
54
|
+
return base;
|
|
55
|
+
};
|
|
56
|
+
const frameLabel = (node) => {
|
|
57
|
+
const name = frameName(node);
|
|
58
|
+
const colored = isFramework(node.module) || node.kind === "native"
|
|
59
|
+
? colors.dim(name)
|
|
60
|
+
: colors.bold(colors.info(name));
|
|
61
|
+
const gas = colors.dim(` [${node.gas}]`);
|
|
62
|
+
return `${colored}(${formatArgs(node.args)})${gas}`;
|
|
63
|
+
};
|
|
64
|
+
const formatData = (data) => {
|
|
65
|
+
if (data === null || data === undefined)
|
|
66
|
+
return "";
|
|
67
|
+
try {
|
|
68
|
+
return colors.dim(JSON.stringify(data));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return colors.dim(String(data));
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const eventLine = (event) => `${colors.warning(`emit ${shortenPath(event.type)}`)} ${formatData(event.data)}`.trimEnd();
|
|
75
|
+
const storageLine = (op) => `${colors.primary(`${op.op} ${shortenPath(op.type)}`)}${op.address ? colors.dim(` @${shortAddr(op.address)}`) : ""}`;
|
|
76
|
+
/** Non-unit return values only; null when nothing to show. */
|
|
77
|
+
const returnLine = (ret) => {
|
|
78
|
+
const meaningful = ret.filter((r) => r.type !== "()" && r.value !== null);
|
|
79
|
+
if (meaningful.length === 0)
|
|
80
|
+
return null;
|
|
81
|
+
return colors.success(`← ${meaningful.map(formatArgValue).join(", ")}`);
|
|
82
|
+
};
|
|
83
|
+
/** Collect every event in the tree with its emitting module — for the flat
|
|
84
|
+
* level-2 view. */
|
|
85
|
+
const collectEvents = (node, out) => {
|
|
86
|
+
for (const e of node.events)
|
|
87
|
+
out.push({ module: node.module, event: e });
|
|
88
|
+
for (const c of node.children)
|
|
89
|
+
collectEvents(c, out);
|
|
90
|
+
};
|
|
91
|
+
/** Level 3: descend through hidden (framework / native) frames, bubbling their
|
|
92
|
+
* events up and surfacing the nearest visible frames as children. */
|
|
93
|
+
const gatherHidden = (children, bubbled, visible) => {
|
|
94
|
+
for (const child of children) {
|
|
95
|
+
if (isUserFrame(child)) {
|
|
96
|
+
visible.push(child);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
bubbled.push(...child.events);
|
|
100
|
+
gatherHidden(child.children, bubbled, visible);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const renderNode = (node, depth, showFull, lines) => {
|
|
105
|
+
lines.push(indent(depth) + frameLabel(node));
|
|
106
|
+
const childDepth = depth + 1;
|
|
107
|
+
if (showFull) {
|
|
108
|
+
for (const e of node.events)
|
|
109
|
+
lines.push(indent(childDepth) + eventLine(e));
|
|
110
|
+
for (const s of node.storage)
|
|
111
|
+
lines.push(indent(childDepth) + storageLine(s));
|
|
112
|
+
const ret = returnLine(node.return);
|
|
113
|
+
if (ret)
|
|
114
|
+
lines.push(indent(childDepth) + ret);
|
|
115
|
+
for (const c of node.children)
|
|
116
|
+
renderNode(c, childDepth, showFull, lines);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// Level 3: own events + events bubbled from hidden descendants, then the
|
|
120
|
+
// nearest visible child frames.
|
|
121
|
+
const bubbled = [];
|
|
122
|
+
const visibleChildren = [];
|
|
123
|
+
gatherHidden(node.children, bubbled, visibleChildren);
|
|
124
|
+
for (const e of [...node.events, ...bubbled]) {
|
|
125
|
+
lines.push(indent(childDepth) + eventLine(e));
|
|
126
|
+
}
|
|
127
|
+
for (const c of visibleChildren)
|
|
128
|
+
renderNode(c, childDepth, showFull, lines);
|
|
129
|
+
};
|
|
130
|
+
const formatAbort = (abort) => {
|
|
131
|
+
const lines = [];
|
|
132
|
+
let header = colors.error(`${symbols.error} Aborted: code ${abort.code}`);
|
|
133
|
+
if (abort.sub_status !== null) {
|
|
134
|
+
header += colors.error(` (sub_status ${abort.sub_status})`);
|
|
135
|
+
}
|
|
136
|
+
if (abort.module !== null) {
|
|
137
|
+
header += colors.dim(` in ${shortenPath(abort.module)}`);
|
|
138
|
+
}
|
|
139
|
+
lines.push(header);
|
|
140
|
+
for (const entry of abort.stack) {
|
|
141
|
+
const mod = entry.module !== null ? shortenPath(entry.module) : "<unknown>";
|
|
142
|
+
const fn = entry.function ?? "<unknown>";
|
|
143
|
+
const off = entry.offset !== null ? ` @${entry.offset}` : "";
|
|
144
|
+
lines.push(" " + colors.error(`at ${mod}::${fn}${off}`));
|
|
145
|
+
}
|
|
146
|
+
return lines;
|
|
147
|
+
};
|
|
148
|
+
const formatFooter = (response, elapsedMs) => {
|
|
149
|
+
const status = response.success
|
|
150
|
+
? colors.success(`${symbols.success} Executed successfully`)
|
|
151
|
+
: colors.error(`${symbols.error} Aborted`);
|
|
152
|
+
const sep = colors.dim(" · ");
|
|
153
|
+
const gas = colors.dim(`gas_used: ${response.gas_used} octas`);
|
|
154
|
+
const timed = brightBlue(`traced in ${Math.round(elapsedMs)}ms`);
|
|
155
|
+
return `${status}${sep}${gas}${sep}${timed}`;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Pure formatter — turns a trace into display lines. Snapshot-testable without
|
|
159
|
+
* touching stdout. `level` is the verbosity level (2..4); per-frame `gas` is in
|
|
160
|
+
* internal VM units while the footer `gas_used` is in octas (never mixed).
|
|
161
|
+
*/
|
|
162
|
+
export function formatTraceLines(response, level, elapsedMs) {
|
|
163
|
+
const lines = [];
|
|
164
|
+
if (level <= 2) {
|
|
165
|
+
const events = [];
|
|
166
|
+
collectEvents(response.root, events);
|
|
167
|
+
if (events.length === 0) {
|
|
168
|
+
lines.push(colors.dim("(no events emitted)"));
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
lines.push(colors.bold("Events"));
|
|
172
|
+
for (const { event } of events)
|
|
173
|
+
lines.push(" " + eventLine(event));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
// Aborts always show the full tree so the failing frame is visible.
|
|
178
|
+
const showFull = level >= 4 || !response.success;
|
|
179
|
+
lines.push(colors.bold("Trace"));
|
|
180
|
+
renderNode(response.root, 0, showFull, lines);
|
|
181
|
+
}
|
|
182
|
+
if (!response.success && response.abort) {
|
|
183
|
+
lines.push("");
|
|
184
|
+
lines.push(...formatAbort(response.abort));
|
|
185
|
+
}
|
|
186
|
+
lines.push("");
|
|
187
|
+
lines.push(formatFooter(response, elapsedMs));
|
|
188
|
+
return lines;
|
|
189
|
+
}
|
|
190
|
+
/** Render a trace to the terminal. Wraps {@link formatTraceLines}. */
|
|
191
|
+
export function renderTrace(response, opts) {
|
|
192
|
+
logger.newline();
|
|
193
|
+
for (const line of formatTraceLines(response, opts.level, opts.elapsedMs)) {
|
|
194
|
+
logger.plain(line);
|
|
195
|
+
}
|
|
196
|
+
logger.newline();
|
|
197
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript mirror of the JSON contract movelite's `/v1/transactions/trace`
|
|
3
|
+
* endpoint serializes. Field names and shapes match the Rust structs exactly;
|
|
4
|
+
* do not rename keys (`return` is keyed literally, `address` is nullable, etc.).
|
|
5
|
+
*/
|
|
6
|
+
/** A decoded argument or return value. `value` shape depends on `type`:
|
|
7
|
+
* primitives like `u64` arrive as strings (`"5"`); a `struct` arrives as an
|
|
8
|
+
* object keyed by field index (`{ "0": .., "1": .. }`); the unit type `()` has
|
|
9
|
+
* `value: null`. */
|
|
10
|
+
export interface TracedArg {
|
|
11
|
+
type: string;
|
|
12
|
+
value: unknown;
|
|
13
|
+
}
|
|
14
|
+
/** An event emitted within a frame. `data` is the decoded event payload. */
|
|
15
|
+
export interface TracedEvent {
|
|
16
|
+
type: string;
|
|
17
|
+
data: unknown;
|
|
18
|
+
}
|
|
19
|
+
/** A storage access. `address` is populated for `load_resource` and null for
|
|
20
|
+
* `move_to` / `borrow_global_mut`. */
|
|
21
|
+
export interface StorageOp {
|
|
22
|
+
op: string;
|
|
23
|
+
type: string;
|
|
24
|
+
address: string | null;
|
|
25
|
+
}
|
|
26
|
+
/** One frame of the abort stack, innermost first. Any field may be null when
|
|
27
|
+
* the VM could not resolve it. */
|
|
28
|
+
export interface AbortStackEntry {
|
|
29
|
+
module: string | null;
|
|
30
|
+
function: string | null;
|
|
31
|
+
offset: number | null;
|
|
32
|
+
}
|
|
33
|
+
/** Abort details, present only when `TraceResponse.success` is false. */
|
|
34
|
+
export interface AbortInfo {
|
|
35
|
+
code: number;
|
|
36
|
+
sub_status: number | null;
|
|
37
|
+
module: string | null;
|
|
38
|
+
stack: AbortStackEntry[];
|
|
39
|
+
}
|
|
40
|
+
/** A single call frame in the execution tree. */
|
|
41
|
+
export interface CallNode {
|
|
42
|
+
kind: "function" | "native" | "script";
|
|
43
|
+
module: string | null;
|
|
44
|
+
function: string | null;
|
|
45
|
+
type_args: string[];
|
|
46
|
+
args: TracedArg[];
|
|
47
|
+
/** Keyed literally as `return` to match the wire format. */
|
|
48
|
+
return: TracedArg[];
|
|
49
|
+
/**
|
|
50
|
+
* Self gas of this frame in **internal** VM gas units. These are much larger
|
|
51
|
+
* than, and NOT comparable to, the external octa `gas_used` on the response —
|
|
52
|
+
* never mix the two when rendering.
|
|
53
|
+
*/
|
|
54
|
+
gas: number;
|
|
55
|
+
events: TracedEvent[];
|
|
56
|
+
storage: StorageOp[];
|
|
57
|
+
children: CallNode[];
|
|
58
|
+
}
|
|
59
|
+
/** The full trace response. */
|
|
60
|
+
export interface TraceResponse {
|
|
61
|
+
txn_hash: string;
|
|
62
|
+
success: boolean;
|
|
63
|
+
/** Transaction-level gas in octas (the external unit). */
|
|
64
|
+
gas_used: number;
|
|
65
|
+
vm_status: string;
|
|
66
|
+
abort: AbortInfo | null;
|
|
67
|
+
root: CallNode;
|
|
68
|
+
}
|
|
@@ -96,6 +96,17 @@ export async function setupLocalTesting(options = {}) {
|
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Resolve whether to prefer movelite. Explicit `useMovelite` wins; otherwise
|
|
101
|
+
* the `MOVEHAT_USE_MOVELITE` env var acts as an override (`0` disables, `1`
|
|
102
|
+
* enables); default is enabled. Lets tooling force a backend without editing
|
|
103
|
+
* test code.
|
|
104
|
+
*/
|
|
105
|
+
function resolveUseMovelite(useMovelite) {
|
|
106
|
+
if (useMovelite !== undefined)
|
|
107
|
+
return useMovelite;
|
|
108
|
+
return process.env.MOVEHAT_USE_MOVELITE !== "0";
|
|
109
|
+
}
|
|
99
110
|
/**
|
|
100
111
|
* Setup using local Movement node (full blockchain)
|
|
101
112
|
*/
|
|
@@ -112,7 +123,7 @@ async function setupWithLocalNode(options, accountLabels, autoFund, defaultBalan
|
|
|
112
123
|
}
|
|
113
124
|
nodeInfo = localNode.getNodeInfo();
|
|
114
125
|
}
|
|
115
|
-
else if (options.useMovelite
|
|
126
|
+
else if (resolveUseMovelite(options.useMovelite) && findMoveliteBinary()) {
|
|
116
127
|
localNode = new MoveliteManager(findMoveliteBinary());
|
|
117
128
|
nodeInfo = await localNode.start();
|
|
118
129
|
ownsNode = true;
|
|
@@ -160,9 +171,14 @@ async function setupWithLocalNode(options, accountLabels, autoFund, defaultBalan
|
|
|
160
171
|
if (!deployerPrivateKey) {
|
|
161
172
|
throw new Error("Failed to get deployer private key");
|
|
162
173
|
}
|
|
174
|
+
// movelite exposes the /transactions/trace endpoint that powers
|
|
175
|
+
// Foundry-style execution traces; a real Movement node does not. Compute
|
|
176
|
+
// this once and reuse it for both the trace wiring and SDK publish below.
|
|
177
|
+
const isMovelite = localNode instanceof MoveliteManager;
|
|
163
178
|
const runtime = await initRuntime({
|
|
164
179
|
network: "local",
|
|
165
180
|
accountManager,
|
|
181
|
+
...(isMovelite ? { traceRpcUrl: `${nodeInfo.rpcUrl}/v1` } : {}),
|
|
166
182
|
configOverride: {
|
|
167
183
|
networks: {
|
|
168
184
|
local: {
|
|
@@ -179,11 +195,14 @@ async function setupWithLocalNode(options, accountLabels, autoFund, defaultBalan
|
|
|
179
195
|
logger.step(`Auto-deploying ${options.autoDeploy.length} module(s)...`);
|
|
180
196
|
const previousRedeploy = process.env.MH_CLI_REDEPLOY;
|
|
181
197
|
process.env.MH_CLI_REDEPLOY = 'true';
|
|
198
|
+
// movelite's REST responses can't drive the Movement CLI publish flow,
|
|
199
|
+
// so deploy through the TypeScript SDK when it is the spawned backend.
|
|
200
|
+
const sdkPublish = isMovelite;
|
|
182
201
|
try {
|
|
183
202
|
for (const moduleName of options.autoDeploy) {
|
|
184
203
|
try {
|
|
185
204
|
logger.plain(` Deploying ${moduleName}...`);
|
|
186
|
-
await runtime.deployContract(moduleName);
|
|
205
|
+
await runtime.deployContract(moduleName, { sdkPublish });
|
|
187
206
|
logger.success(`${moduleName} deployed`, 2);
|
|
188
207
|
}
|
|
189
208
|
catch (error) {
|
|
@@ -94,7 +94,7 @@ export class LocalNodeManager {
|
|
|
94
94
|
...(usesDefaultAdapter ? { env: sanitizeMovementEnv() } : {}),
|
|
95
95
|
stdio: this.options.silent ? "ignore" : "pipe",
|
|
96
96
|
});
|
|
97
|
-
// Subprocess output handling (
|
|
97
|
+
// Subprocess output handling (verbosity-gated console UX):
|
|
98
98
|
// - stdout chatter is hidden by default; gated by isVerbose()
|
|
99
99
|
// - lines matching CRITICAL_NODE_OUTPUT always surface as warnings
|
|
100
100
|
// so the user is never silenced through a real failure
|
package/dist/runtime.d.ts
CHANGED
|
@@ -17,6 +17,14 @@ export interface InitRuntimeOptions {
|
|
|
17
17
|
* key it extracts ends up on the same manager the runtime exposes.
|
|
18
18
|
*/
|
|
19
19
|
accountManager?: AccountManager;
|
|
20
|
+
/**
|
|
21
|
+
* movelite RPC base URL (already ending in `/v1`) enabling Foundry-style
|
|
22
|
+
* execution traces on `contract.call(...)` at verbosity level >= 2. Set only
|
|
23
|
+
* when the active backend is movelite (its `/transactions/trace` endpoint
|
|
24
|
+
* does not exist on a real Movement node). When omitted, contracts use the
|
|
25
|
+
* normal submit path and never trace.
|
|
26
|
+
*/
|
|
27
|
+
traceRpcUrl?: string;
|
|
20
28
|
}
|
|
21
29
|
/**
|
|
22
30
|
* Initialize the Movehat Runtime Environment.
|
package/dist/runtime.js
CHANGED
|
@@ -55,7 +55,7 @@ export async function initRuntime(options = {}) {
|
|
|
55
55
|
};
|
|
56
56
|
// Helper functions
|
|
57
57
|
const getContractHelper = (address, moduleName) => {
|
|
58
|
-
return getContract(aptos, address, moduleName);
|
|
58
|
+
return getContract(aptos, address, moduleName, options.traceRpcUrl);
|
|
59
59
|
};
|
|
60
60
|
const deployContract = async (moduleName, options) => {
|
|
61
61
|
// Thin orchestrator; the actual logic lives in core/Publisher.ts.
|
|
@@ -64,6 +64,8 @@ export async function initRuntime(options = {}) {
|
|
|
64
64
|
config,
|
|
65
65
|
account,
|
|
66
66
|
packageDir: options?.packageDir,
|
|
67
|
+
sdkPublish: options?.sdkPublish,
|
|
68
|
+
aptos,
|
|
67
69
|
});
|
|
68
70
|
};
|
|
69
71
|
const getDeployment = (moduleName) => {
|
package/dist/types/runtime.d.ts
CHANGED
|
@@ -24,6 +24,12 @@ export interface MovehatRuntime {
|
|
|
24
24
|
* leave this undefined so the default spawn-based adapter is used.
|
|
25
25
|
*/
|
|
26
26
|
adapter?: ChildProcessAdapter;
|
|
27
|
+
/**
|
|
28
|
+
* Publish the compiled package via the TypeScript SDK instead of the
|
|
29
|
+
* Movement CLI. Internal: setupLocalTesting sets this when the backend
|
|
30
|
+
* is movelite, whose REST responses the Movement CLI cannot consume.
|
|
31
|
+
*/
|
|
32
|
+
sdkPublish?: boolean;
|
|
27
33
|
}) => Promise<DeploymentInfo>;
|
|
28
34
|
getDeployment: (moduleName: string) => DeploymentInfo | null;
|
|
29
35
|
getDeployments: () => Record<string, DeploymentInfo>;
|
package/dist/ui/logger.d.ts
CHANGED
|
@@ -29,6 +29,21 @@ export interface LoggerConfig {
|
|
|
29
29
|
* callers opt in before the CLI parses args, e.g. in shell scripts).
|
|
30
30
|
*/
|
|
31
31
|
export declare const isVerbose: () => boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Numeric verbosity level (0..4) for level-gated output such as the
|
|
34
|
+
* transaction-trace renderer. Driven by the counted `-v` CLI flag via the
|
|
35
|
+
* `MOVEHAT_VERBOSITY` env var, so it survives the spawned test/script
|
|
36
|
+
* subprocess boundary the same way {@link isVerbose} reads `MOVEHAT_VERBOSE`.
|
|
37
|
+
* Falls back to the legacy boolean `MOVEHAT_VERBOSE=1` (level 1) for callers
|
|
38
|
+
* that set only that.
|
|
39
|
+
*
|
|
40
|
+
* - 0 default — system logs only
|
|
41
|
+
* - 1 `-v` — + subprocess stdout (see {@link isVerbose})
|
|
42
|
+
* - 2 `-vv` — + decoded events per call
|
|
43
|
+
* - 3 `-vvv` — + user-module call tree
|
|
44
|
+
* - 4 `-vvvv` — + framework frames, natives, storage, return values
|
|
45
|
+
*/
|
|
46
|
+
export declare const getVerbosityLevel: () => number;
|
|
32
47
|
/**
|
|
33
48
|
* Configure logger globally
|
|
34
49
|
*
|
|
@@ -202,6 +217,7 @@ export declare const item: (text: string, indent?: number) => void;
|
|
|
202
217
|
export declare const logger: {
|
|
203
218
|
configure: (newConfig: Partial<LoggerConfig>) => void;
|
|
204
219
|
isVerbose: () => boolean;
|
|
220
|
+
getVerbosityLevel: () => number;
|
|
205
221
|
info: (message: string, indent?: number) => void;
|
|
206
222
|
success: (message: string, indent?: number) => void;
|
|
207
223
|
error: (message: string, indent?: number) => void;
|
package/dist/ui/logger.js
CHANGED
|
@@ -16,6 +16,29 @@ let config = {
|
|
|
16
16
|
* callers opt in before the CLI parses args, e.g. in shell scripts).
|
|
17
17
|
*/
|
|
18
18
|
export const isVerbose = () => config.verbosity === 'verbose' || process.env.MOVEHAT_VERBOSE === '1';
|
|
19
|
+
/**
|
|
20
|
+
* Numeric verbosity level (0..4) for level-gated output such as the
|
|
21
|
+
* transaction-trace renderer. Driven by the counted `-v` CLI flag via the
|
|
22
|
+
* `MOVEHAT_VERBOSITY` env var, so it survives the spawned test/script
|
|
23
|
+
* subprocess boundary the same way {@link isVerbose} reads `MOVEHAT_VERBOSE`.
|
|
24
|
+
* Falls back to the legacy boolean `MOVEHAT_VERBOSE=1` (level 1) for callers
|
|
25
|
+
* that set only that.
|
|
26
|
+
*
|
|
27
|
+
* - 0 default — system logs only
|
|
28
|
+
* - 1 `-v` — + subprocess stdout (see {@link isVerbose})
|
|
29
|
+
* - 2 `-vv` — + decoded events per call
|
|
30
|
+
* - 3 `-vvv` — + user-module call tree
|
|
31
|
+
* - 4 `-vvvv` — + framework frames, natives, storage, return values
|
|
32
|
+
*/
|
|
33
|
+
export const getVerbosityLevel = () => {
|
|
34
|
+
const raw = process.env.MOVEHAT_VERBOSITY;
|
|
35
|
+
if (raw !== undefined) {
|
|
36
|
+
const n = parseInt(raw, 10);
|
|
37
|
+
if (!Number.isNaN(n))
|
|
38
|
+
return Math.max(0, Math.min(4, n));
|
|
39
|
+
}
|
|
40
|
+
return process.env.MOVEHAT_VERBOSE === '1' ? 1 : 0;
|
|
41
|
+
};
|
|
19
42
|
/**
|
|
20
43
|
* Configure logger globally
|
|
21
44
|
*
|
|
@@ -261,6 +284,7 @@ export const item = (text, indent = 0) => {
|
|
|
261
284
|
export const logger = {
|
|
262
285
|
configure: configureLogger,
|
|
263
286
|
isVerbose,
|
|
287
|
+
getVerbosityLevel,
|
|
264
288
|
info,
|
|
265
289
|
success,
|
|
266
290
|
error,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "movehat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Hardhat-like development framework for Movement L1 smart contracts",
|
|
6
6
|
"bin": {
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"tsx": "^4.7.0"
|
|
72
72
|
},
|
|
73
73
|
"optionalDependencies": {
|
|
74
|
-
"movelite": "^0.1
|
|
74
|
+
"movelite": "^0.2.1"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
77
|
"@types/js-yaml": "^4.0.9",
|