movehat 0.2.9 → 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 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', 'Show subprocess output (movement node, aptos move) for debugging')
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
- if (options.verbose) {
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
@@ -8,9 +8,10 @@ export declare class MoveContract {
8
8
  private aptos;
9
9
  private moduleAddress;
10
10
  private moduleName;
11
- constructor(aptos: Aptos, moduleAddress: string, moduleName: string);
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;
@@ -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
- constructor(aptos, moduleAddress, moduleName) {
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
+ }
@@ -0,0 +1,6 @@
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
+ export {};
@@ -171,9 +171,14 @@ async function setupWithLocalNode(options, accountLabels, autoFund, defaultBalan
171
171
  if (!deployerPrivateKey) {
172
172
  throw new Error("Failed to get deployer private key");
173
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;
174
178
  const runtime = await initRuntime({
175
179
  network: "local",
176
180
  accountManager,
181
+ ...(isMovelite ? { traceRpcUrl: `${nodeInfo.rpcUrl}/v1` } : {}),
177
182
  configOverride: {
178
183
  networks: {
179
184
  local: {
@@ -192,7 +197,7 @@ async function setupWithLocalNode(options, accountLabels, autoFund, defaultBalan
192
197
  process.env.MH_CLI_REDEPLOY = 'true';
193
198
  // movelite's REST responses can't drive the Movement CLI publish flow,
194
199
  // so deploy through the TypeScript SDK when it is the spawned backend.
195
- const sdkPublish = localNode instanceof MoveliteManager;
200
+ const sdkPublish = isMovelite;
196
201
  try {
197
202
  for (const moduleName of options.autoDeploy) {
198
203
  try {
@@ -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 (see §9 Console UX in CLAUDE.md):
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.
@@ -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.2.9",
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.0"
74
+ "movelite": "^0.2.1"
75
75
  },
76
76
  "devDependencies": {
77
77
  "@types/js-yaml": "^4.0.9",