movehat 0.3.0 → 0.4.1

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
@@ -22,7 +22,7 @@
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
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).
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 (full tree on the movelite backend; the Movement node renders a degraded flat trace — events, state changes, and gas).
26
26
  - **Native fork system** — local JSON-backed snapshots of Movement L1 state, no BCS compatibility issues.
27
27
  - **TypeScript-first** — single `PRIVATE_KEY` across all networks (Hardhat-style); deployments tracked per-network in `deployments/`.
28
28
  - **SLSA-provenance releases** — every npm release ships with [Trusted Publishers](https://docs.npmjs.com/trusted-publishers) provenance. Verify with `npm view movehat@<version>`.
@@ -1,6 +1,7 @@
1
1
  import { logger } from "../ui/index.js";
2
2
  import { traceTransaction } from "./trace/client.js";
3
3
  import { renderTrace } from "./trace/renderer.js";
4
+ import { renderNodeTrace } from "./trace/nodeRenderer.js";
4
5
  export class MoveContract {
5
6
  aptos;
6
7
  moduleAddress;
@@ -71,6 +72,24 @@ export class MoveContract {
71
72
  const response = await this.aptos.waitForTransaction({
72
73
  transactionHash: committedTxn.hash,
73
74
  });
75
+ // Degraded trace: the Movement node does not expose internal call frames,
76
+ // but the REST response already carries the events, write-set, and gas.
77
+ // At raised verbosity render that flat view (a render failure must never
78
+ // fail a transaction that already committed).
79
+ //
80
+ // waitForTransaction returns the CommittedTransactionResponse union; the
81
+ // `in` guards narrow it to the user-transaction shape that carries events
82
+ // and changes, so renderNodeTrace receives a typed value without a cast.
83
+ const nodeLevel = logger.getVerbosityLevel();
84
+ if (nodeLevel >= 2 && "events" in response && "changes" in response) {
85
+ try {
86
+ renderNodeTrace(response, { level: nodeLevel });
87
+ }
88
+ catch (renderError) {
89
+ const msg = renderError instanceof Error ? renderError.message : String(renderError);
90
+ logger.warning(`Failed to render trace: ${msg}`);
91
+ }
92
+ }
74
93
  logger.success(`Transaction ${committedTxn.hash} committed with status: ${response.vm_status}`);
75
94
  logger.newline();
76
95
  return {
@@ -0,0 +1,27 @@
1
+ export declare const brightBlue: (s: string) => string;
2
+ export declare const indent: (depth: number) => string;
3
+ /** Shorten a 0x-prefixed address for display (`0xf903..9b16`); leave short
4
+ * framework addresses like `0x1` untouched. */
5
+ export declare const shortAddr: (addr: string) => string;
6
+ /** Shorten the address part of a `address::module[::Name]` path. */
7
+ export declare const shortenPath: (path: string) => string;
8
+ /** Format a decoded value. Struct (and vector) values arrive as objects with
9
+ * by-index keys; their fields can themselves be structs, so recurse rather
10
+ * than `String()`-ing a nested object into `[object Object]`. */
11
+ export declare const formatValue: (value: unknown) => string;
12
+ export declare const formatData: (data: unknown) => string;
13
+ /** One emitted-event line: `emit <module>::<Event> {…data}`. Reads only `type`
14
+ * and `data`, so it accepts both the movelite `TracedEvent` and the SDK's
15
+ * `Event` shape. */
16
+ export declare const formatEventLine: (event: {
17
+ type: string;
18
+ data: unknown;
19
+ }) => string;
20
+ /** One storage / state-change line: `<op> <module>::<Resource> [@addr]`. The
21
+ * `op` object is duck-typed so both movelite storage ops and SDK write-set
22
+ * changes (after reshaping) feed it. */
23
+ export declare const storageLine: (op: {
24
+ op: string;
25
+ type: string;
26
+ address: string | null;
27
+ }) => string;
@@ -0,0 +1,61 @@
1
+ import { colors, rgbToAnsi, shouldUseColor } from "../../ui/colors.js";
2
+ // Generic, stateless formatting helpers shared by the movelite call-tree
3
+ // renderer (renderer.ts) and the node degraded-trace renderer (nodeRenderer.ts).
4
+ // None of these are tied to a specific response shape.
5
+ // rgbToAnsi emits raw escapes unconditionally, so guard these ourselves
6
+ // (unlike colors.*, which already no-op when color is disabled). `orange` is
7
+ // module-private (only `leafValue` uses it); `brightBlue` is consumed by the
8
+ // movelite renderer's footer.
9
+ const orange = (s) => shouldUseColor() ? `${rgbToAnsi(255, 165, 0)}${s}\x1b[0m` : s;
10
+ export const brightBlue = (s) => shouldUseColor() ? `${rgbToAnsi(90, 170, 255)}${s}\x1b[0m` : s;
11
+ export const indent = (depth) => " ".repeat(depth);
12
+ /** Shorten a 0x-prefixed address for display (`0xf903..9b16`); leave short
13
+ * framework addresses like `0x1` untouched. */
14
+ export const shortAddr = (addr) => addr.startsWith("0x") && addr.length > 12
15
+ ? `${addr.slice(0, 6)}..${addr.slice(-4)}`
16
+ : addr;
17
+ /** Shorten the address part of a `address::module[::Name]` path. */
18
+ export const shortenPath = (path) => {
19
+ const [addr, ...rest] = path.split("::");
20
+ if (addr === undefined || rest.length === 0)
21
+ return path;
22
+ return [shortAddr(addr), ...rest].join("::");
23
+ };
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
+ export 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
+ export const formatData = (data) => {
45
+ if (data === null || data === undefined)
46
+ return "";
47
+ try {
48
+ return colors.dim(JSON.stringify(data));
49
+ }
50
+ catch {
51
+ return colors.dim(String(data));
52
+ }
53
+ };
54
+ /** One emitted-event line: `emit <module>::<Event> {…data}`. Reads only `type`
55
+ * and `data`, so it accepts both the movelite `TracedEvent` and the SDK's
56
+ * `Event` shape. */
57
+ export const formatEventLine = (event) => `${colors.warning(`emit ${shortenPath(event.type)}`)} ${formatData(event.data)}`.trimEnd();
58
+ /** One storage / state-change line: `<op> <module>::<Resource> [@addr]`. The
59
+ * `op` object is duck-typed so both movelite storage ops and SDK write-set
60
+ * changes (after reshaping) feed it. */
61
+ export const storageLine = (op) => `${colors.primary(`${op.op} ${shortenPath(op.type)}`)}${op.address ? colors.dim(` @${shortAddr(op.address)}`) : ""}`;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Minimal structural view of a committed transaction, as the Aptos SDK's
3
+ * `waitForTransaction` returns it (`UserTransactionResponse`). Typed loosely so
4
+ * unit tests can feed plain fixtures and so we never depend on the full SDK
5
+ * response union. The real Movement node REST API does NOT expose internal call
6
+ * frames, so this is a flat, degraded trace — events + write-set + gas — rather
7
+ * than the movelite call tree.
8
+ */
9
+ export interface NodeTxView {
10
+ success: boolean;
11
+ vm_status: string;
12
+ /** Transaction gas in octas (external unit). */
13
+ gas_used: string;
14
+ events: {
15
+ type: string;
16
+ data: unknown;
17
+ }[];
18
+ changes: NodeWriteSetChange[];
19
+ }
20
+ /** Loose shape of an SDK `WriteSetChange` — only the fields we render. */
21
+ interface NodeWriteSetChange {
22
+ type: string;
23
+ address?: string;
24
+ /** `write_resource` payload (a `MoveResource` `{type,data}`); typed `unknown`
25
+ * and narrowed at the use sites since the shape varies by change type. */
26
+ data?: unknown;
27
+ /** `delete_resource` carries the resource type as a string here. */
28
+ resource?: string;
29
+ /** Table-item changes carry a `handle` instead of an `address`. */
30
+ handle?: string;
31
+ }
32
+ /**
33
+ * Pure formatter for the node degraded trace. Snapshot-testable without
34
+ * touching stdout. `level` is the verbosity level (2..4): L2 = events, L3 =
35
+ * + state changes, L4 = + each change's decoded data. The footer carries the
36
+ * status and the octa `gas_used` (there is no per-frame trace timing off the
37
+ * node).
38
+ */
39
+ export declare function formatNodeTraceLines(tx: NodeTxView, level: number): string[];
40
+ /** Render a node degraded trace to the terminal. Wraps {@link formatNodeTraceLines}. */
41
+ export declare function renderNodeTrace(tx: NodeTxView, opts: {
42
+ level: number;
43
+ }): void;
44
+ export {};
@@ -0,0 +1,90 @@
1
+ import { logger } from "../../ui/index.js";
2
+ import { colors } from "../../ui/colors.js";
3
+ import { symbols } from "../../ui/symbols.js";
4
+ import { formatData, formatEventLine, storageLine } from "./format.js";
5
+ /** Reshape an SDK write-set change into the shared `storageLine` duck-type. */
6
+ const changeToOp = (c) => {
7
+ const address = typeof c.address === "string" ? c.address : null;
8
+ switch (c.type) {
9
+ case "write_resource": {
10
+ const t = c.data && typeof c.data === "object" && "type" in c.data
11
+ ? String(c.data.type ?? "?")
12
+ : "?";
13
+ return { op: c.type, type: t, address };
14
+ }
15
+ case "delete_resource":
16
+ return { op: c.type, type: c.resource ?? "?", address };
17
+ case "write_module":
18
+ case "delete_module":
19
+ return { op: c.type, type: "module", address };
20
+ case "write_table_item":
21
+ case "delete_table_item":
22
+ // Table items have no resource type; key off the table `handle`.
23
+ return { op: c.type, type: "table_item", address: c.handle ?? null };
24
+ default:
25
+ return { op: c.type, type: "?", address };
26
+ }
27
+ };
28
+ /** Level-4 payload for a change: the resource's decoded fields when present. */
29
+ const changePayload = (c) => {
30
+ if (c.data && typeof c.data === "object" && "data" in c.data) {
31
+ return c.data.data;
32
+ }
33
+ return c.data;
34
+ };
35
+ const nodeFooter = (tx) => {
36
+ const status = tx.success
37
+ ? colors.success(`${symbols.success} ${tx.vm_status}`)
38
+ : colors.error(`${symbols.error} ${tx.vm_status}`);
39
+ const sep = colors.dim(" · ");
40
+ const gas = colors.dim(`gas_used: ${tx.gas_used} octas`);
41
+ return `${status}${sep}${gas}`;
42
+ };
43
+ /**
44
+ * Pure formatter for the node degraded trace. Snapshot-testable without
45
+ * touching stdout. `level` is the verbosity level (2..4): L2 = events, L3 =
46
+ * + state changes, L4 = + each change's decoded data. The footer carries the
47
+ * status and the octa `gas_used` (there is no per-frame trace timing off the
48
+ * node).
49
+ */
50
+ export function formatNodeTraceLines(tx, level) {
51
+ const lines = [];
52
+ // Events (level 2+).
53
+ if (tx.events.length === 0) {
54
+ lines.push(colors.dim("(no events emitted)"));
55
+ }
56
+ else {
57
+ lines.push(colors.bold("Events"));
58
+ for (const e of tx.events)
59
+ lines.push(" " + formatEventLine(e));
60
+ }
61
+ // State changes (level 3+).
62
+ if (level >= 3) {
63
+ lines.push("");
64
+ if (tx.changes.length === 0) {
65
+ lines.push(colors.dim("(no state changes)"));
66
+ }
67
+ else {
68
+ lines.push(colors.bold("State changes"));
69
+ for (const c of tx.changes) {
70
+ lines.push(" " + storageLine(changeToOp(c)));
71
+ if (level >= 4) {
72
+ const payload = formatData(changePayload(c));
73
+ if (payload)
74
+ lines.push(" " + payload);
75
+ }
76
+ }
77
+ }
78
+ }
79
+ lines.push("");
80
+ lines.push(nodeFooter(tx));
81
+ return lines;
82
+ }
83
+ /** Render a node degraded trace to the terminal. Wraps {@link formatNodeTraceLines}. */
84
+ export function renderNodeTrace(tx, opts) {
85
+ logger.newline();
86
+ for (const line of formatNodeTraceLines(tx, opts.level)) {
87
+ logger.plain(line);
88
+ }
89
+ logger.newline();
90
+ }
@@ -1,46 +1,10 @@
1
1
  import { logger } from "../../ui/index.js";
2
- import { colors, rgbToAnsi, shouldUseColor } from "../../ui/colors.js";
2
+ import { colors } from "../../ui/colors.js";
3
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
- };
4
+ import { brightBlue, formatEventLine, formatValue, indent, shortenPath, storageLine, } from "./format.js";
21
5
  const isFramework = (module) => module !== null && module.startsWith("0x1::");
22
6
  /** Visible at level 3 (user-module tree): not a framework frame, not a native. */
23
7
  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
8
  const formatArgValue = (arg) => {
45
9
  if (arg.value === null)
46
10
  return "()";
@@ -61,18 +25,6 @@ const frameLabel = (node) => {
61
25
  const gas = colors.dim(` [${node.gas}]`);
62
26
  return `${colored}(${formatArgs(node.args)})${gas}`;
63
27
  };
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
28
  /** Non-unit return values only; null when nothing to show. */
77
29
  const returnLine = (ret) => {
78
30
  const meaningful = ret.filter((r) => r.type !== "()" && r.value !== null);
@@ -106,7 +58,7 @@ const renderNode = (node, depth, showFull, lines) => {
106
58
  const childDepth = depth + 1;
107
59
  if (showFull) {
108
60
  for (const e of node.events)
109
- lines.push(indent(childDepth) + eventLine(e));
61
+ lines.push(indent(childDepth) + formatEventLine(e));
110
62
  for (const s of node.storage)
111
63
  lines.push(indent(childDepth) + storageLine(s));
112
64
  const ret = returnLine(node.return);
@@ -122,7 +74,7 @@ const renderNode = (node, depth, showFull, lines) => {
122
74
  const visibleChildren = [];
123
75
  gatherHidden(node.children, bubbled, visibleChildren);
124
76
  for (const e of [...node.events, ...bubbled]) {
125
- lines.push(indent(childDepth) + eventLine(e));
77
+ lines.push(indent(childDepth) + formatEventLine(e));
126
78
  }
127
79
  for (const c of visibleChildren)
128
80
  renderNode(c, childDepth, showFull, lines);
@@ -170,7 +122,7 @@ export function formatTraceLines(response, level, elapsedMs) {
170
122
  else {
171
123
  lines.push(colors.bold("Events"));
172
124
  for (const { event } of events)
173
- lines.push(" " + eventLine(event));
125
+ lines.push(" " + formatEventLine(event));
174
126
  }
175
127
  }
176
128
  else {
@@ -1,5 +1,20 @@
1
1
  import ora from 'ora';
2
2
  import { shouldUseColor } from './colors.js';
3
+ import { coloredSymbol } from './symbols.js';
4
+ /**
5
+ * Persist a spinner's final line with a color-gated status symbol.
6
+ *
7
+ * ora's own `.succeed()` / `.fail()` color their log-symbols through ora's
8
+ * internal TTY detection, which still emits ANSI on the persisted line when
9
+ * stdout is piped (non-TTY) even though our `shouldUseColor()` says no color.
10
+ * Routing the symbol through `coloredSymbol` (gated by `shouldUseColor`) keeps
11
+ * piped output escape-free while preserving the colored glyph in a real TTY.
12
+ */
13
+ const persist = (spin, type, text) => {
14
+ spin.stopAndPersist(text === undefined
15
+ ? { symbol: coloredSymbol(type) }
16
+ : { symbol: coloredSymbol(type), text });
17
+ };
3
18
  /**
4
19
  * Create and start a spinner
5
20
  * Automatically disabled in non-TTY environments (CI, pipes)
@@ -63,12 +78,12 @@ export const withSpinner = async (startText, task, successText, errorText, inden
63
78
  const spin = spinner({ text: startText, indent });
64
79
  try {
65
80
  const result = await task();
66
- spin.succeed(successText || startText.replace(/\.\.\.?$/, ''));
81
+ persist(spin, 'success', successText || startText.replace(/\.\.\.?$/, ''));
67
82
  return result;
68
83
  }
69
84
  catch (error) {
70
85
  const errMsg = error instanceof Error ? error.message : String(error);
71
- spin.fail(errorText || `Failed: ${errMsg}`);
86
+ persist(spin, 'error', errorText || `Failed: ${errMsg}`);
72
87
  throw error;
73
88
  }
74
89
  };
@@ -104,12 +119,12 @@ export const withTimedSpinner = async (label, task, indent = 0) => {
104
119
  }, 500);
105
120
  try {
106
121
  const result = await task();
107
- spin.succeed(`${label} (${((Date.now() - start) / 1000).toFixed(1)}s)`);
122
+ persist(spin, 'success', `${label} (${((Date.now() - start) / 1000).toFixed(1)}s)`);
108
123
  return result;
109
124
  }
110
125
  catch (error) {
111
126
  const errMsg = error instanceof Error ? error.message : String(error);
112
- spin.fail(errMsg);
127
+ persist(spin, 'error', errMsg);
113
128
  throw error;
114
129
  }
115
130
  finally {
@@ -146,11 +161,11 @@ export const createSpinnerChain = () => {
146
161
  currentSpinner = spinner({ text, indent });
147
162
  try {
148
163
  const result = await task();
149
- currentSpinner.succeed();
164
+ persist(currentSpinner, 'success');
150
165
  return result;
151
166
  }
152
167
  catch (error) {
153
- currentSpinner.fail();
168
+ persist(currentSpinner, 'error');
154
169
  throw error;
155
170
  }
156
171
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "movehat",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "description": "Hardhat-like development framework for Movement L1 smart contracts",
6
6
  "bin": {