movehat 0.3.0 → 0.4.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 +1 -1
- package/dist/core/contract.js +19 -0
- package/dist/core/trace/format.d.ts +27 -0
- package/dist/core/trace/format.js +61 -0
- package/dist/core/trace/nodeRenderer.d.ts +44 -0
- package/dist/core/trace/nodeRenderer.js +90 -0
- package/dist/core/trace/renderer.js +5 -53
- package/package.json +1 -1
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>`.
|
package/dist/core/contract.js
CHANGED
|
@@ -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
|
|
2
|
+
import { colors } from "../../ui/colors.js";
|
|
3
3
|
import { symbols } from "../../ui/symbols.js";
|
|
4
|
-
|
|
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) +
|
|
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) +
|
|
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(" " +
|
|
125
|
+
lines.push(" " + formatEventLine(event));
|
|
174
126
|
}
|
|
175
127
|
}
|
|
176
128
|
else {
|