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 +2 -0
- package/dist/cli.js +7 -2
- 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 +6 -1
- package/dist/node/LocalNodeManager.js +1 -1
- package/dist/runtime.d.ts +8 -0
- package/dist/runtime.js +1 -1
- 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/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
|
+
}
|
|
@@ -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 =
|
|
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 (
|
|
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.
|
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",
|