movehat 0.2.9 → 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 +2 -0
- package/dist/cli.js +7 -2
- package/dist/core/contract.d.ts +3 -2
- package/dist/core/contract.js +57 -3
- package/dist/core/trace/client.d.ts +18 -0
- package/dist/core/trace/client.js +58 -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.d.ts +12 -0
- package/dist/core/trace/renderer.js +149 -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 (full tree on the movelite backend; the Movement node renders a degraded flat trace — events, state changes, and gas).
|
|
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,21 @@
|
|
|
1
1
|
import { logger } from "../ui/index.js";
|
|
2
|
+
import { traceTransaction } from "./trace/client.js";
|
|
3
|
+
import { renderTrace } from "./trace/renderer.js";
|
|
4
|
+
import { renderNodeTrace } from "./trace/nodeRenderer.js";
|
|
2
5
|
export class MoveContract {
|
|
3
6
|
aptos;
|
|
4
7
|
moduleAddress;
|
|
5
8
|
moduleName;
|
|
6
|
-
|
|
9
|
+
traceRpcUrl;
|
|
10
|
+
constructor(aptos, moduleAddress, moduleName,
|
|
11
|
+
// movelite RPC base (ending in `/v1`). Presence enables Foundry-style
|
|
12
|
+
// execution traces at verbosity level >= 2. Undefined on the Movement node
|
|
13
|
+
// (no trace endpoint) — calls use the normal submit path.
|
|
14
|
+
traceRpcUrl) {
|
|
7
15
|
this.aptos = aptos;
|
|
8
16
|
this.moduleAddress = moduleAddress;
|
|
9
17
|
this.moduleName = moduleName;
|
|
18
|
+
this.traceRpcUrl = traceRpcUrl;
|
|
10
19
|
}
|
|
11
20
|
async call(signer, functionName,
|
|
12
21
|
// any[]: Move entry-function arguments are heterogeneous primitives
|
|
@@ -29,6 +38,33 @@ export class MoveContract {
|
|
|
29
38
|
signer,
|
|
30
39
|
transaction,
|
|
31
40
|
});
|
|
41
|
+
// Trace path: on movelite at raised verbosity, route through the
|
|
42
|
+
// instrumented `/transactions/trace?commit=true` endpoint, which executes
|
|
43
|
+
// AND commits in one pass (so we must NOT also submit), then render the
|
|
44
|
+
// returned call tree.
|
|
45
|
+
const traceLevel = logger.getVerbosityLevel();
|
|
46
|
+
if (this.traceRpcUrl && traceLevel >= 2) {
|
|
47
|
+
const { response, elapsedMs } = await traceTransaction({
|
|
48
|
+
rpcUrl: this.traceRpcUrl,
|
|
49
|
+
transaction,
|
|
50
|
+
senderAuthenticator: signature,
|
|
51
|
+
});
|
|
52
|
+
// A render failure must never fail a transaction that already committed.
|
|
53
|
+
try {
|
|
54
|
+
renderTrace(response, { level: traceLevel, elapsedMs });
|
|
55
|
+
}
|
|
56
|
+
catch (renderError) {
|
|
57
|
+
const msg = renderError instanceof Error ? renderError.message : String(renderError);
|
|
58
|
+
logger.warning(`Failed to render trace: ${msg}`);
|
|
59
|
+
}
|
|
60
|
+
logger.success(`Transaction ${response.txn_hash} committed with status: ${response.vm_status}`);
|
|
61
|
+
logger.newline();
|
|
62
|
+
return {
|
|
63
|
+
hash: response.txn_hash,
|
|
64
|
+
success: response.success,
|
|
65
|
+
vm_status: response.vm_status,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
32
68
|
const committedTxn = await this.aptos.transaction.submit.simple({
|
|
33
69
|
transaction,
|
|
34
70
|
senderAuthenticator: signature,
|
|
@@ -36,6 +72,24 @@ export class MoveContract {
|
|
|
36
72
|
const response = await this.aptos.waitForTransaction({
|
|
37
73
|
transactionHash: committedTxn.hash,
|
|
38
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
|
+
}
|
|
39
93
|
logger.success(`Transaction ${committedTxn.hash} committed with status: ${response.vm_status}`);
|
|
40
94
|
logger.newline();
|
|
41
95
|
return {
|
|
@@ -61,6 +115,6 @@ export class MoveContract {
|
|
|
61
115
|
return `${this.moduleAddress}::${this.moduleName}`;
|
|
62
116
|
}
|
|
63
117
|
}
|
|
64
|
-
export function getContract(aptos, moduleAddress, moduleName) {
|
|
65
|
-
return new MoveContract(aptos, moduleAddress, moduleName);
|
|
118
|
+
export function getContract(aptos, moduleAddress, moduleName, traceRpcUrl) {
|
|
119
|
+
return new MoveContract(aptos, moduleAddress, moduleName, traceRpcUrl);
|
|
66
120
|
}
|
|
@@ -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,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
|
+
}
|
|
@@ -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,149 @@
|
|
|
1
|
+
import { logger } from "../../ui/index.js";
|
|
2
|
+
import { colors } from "../../ui/colors.js";
|
|
3
|
+
import { symbols } from "../../ui/symbols.js";
|
|
4
|
+
import { brightBlue, formatEventLine, formatValue, indent, shortenPath, storageLine, } from "./format.js";
|
|
5
|
+
const isFramework = (module) => module !== null && module.startsWith("0x1::");
|
|
6
|
+
/** Visible at level 3 (user-module tree): not a framework frame, not a native. */
|
|
7
|
+
const isUserFrame = (node) => !isFramework(node.module) && node.kind !== "native";
|
|
8
|
+
const formatArgValue = (arg) => {
|
|
9
|
+
if (arg.value === null)
|
|
10
|
+
return "()";
|
|
11
|
+
return formatValue(arg.value);
|
|
12
|
+
};
|
|
13
|
+
const formatArgs = (args) => args.map(formatArgValue).join(", ");
|
|
14
|
+
const frameName = (node) => {
|
|
15
|
+
const base = node.module
|
|
16
|
+
? `${shortenPath(node.module)}::${node.function ?? "?"}`
|
|
17
|
+
: node.function ?? `<${node.kind}>`;
|
|
18
|
+
return base;
|
|
19
|
+
};
|
|
20
|
+
const frameLabel = (node) => {
|
|
21
|
+
const name = frameName(node);
|
|
22
|
+
const colored = isFramework(node.module) || node.kind === "native"
|
|
23
|
+
? colors.dim(name)
|
|
24
|
+
: colors.bold(colors.info(name));
|
|
25
|
+
const gas = colors.dim(` [${node.gas}]`);
|
|
26
|
+
return `${colored}(${formatArgs(node.args)})${gas}`;
|
|
27
|
+
};
|
|
28
|
+
/** Non-unit return values only; null when nothing to show. */
|
|
29
|
+
const returnLine = (ret) => {
|
|
30
|
+
const meaningful = ret.filter((r) => r.type !== "()" && r.value !== null);
|
|
31
|
+
if (meaningful.length === 0)
|
|
32
|
+
return null;
|
|
33
|
+
return colors.success(`← ${meaningful.map(formatArgValue).join(", ")}`);
|
|
34
|
+
};
|
|
35
|
+
/** Collect every event in the tree with its emitting module — for the flat
|
|
36
|
+
* level-2 view. */
|
|
37
|
+
const collectEvents = (node, out) => {
|
|
38
|
+
for (const e of node.events)
|
|
39
|
+
out.push({ module: node.module, event: e });
|
|
40
|
+
for (const c of node.children)
|
|
41
|
+
collectEvents(c, out);
|
|
42
|
+
};
|
|
43
|
+
/** Level 3: descend through hidden (framework / native) frames, bubbling their
|
|
44
|
+
* events up and surfacing the nearest visible frames as children. */
|
|
45
|
+
const gatherHidden = (children, bubbled, visible) => {
|
|
46
|
+
for (const child of children) {
|
|
47
|
+
if (isUserFrame(child)) {
|
|
48
|
+
visible.push(child);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
bubbled.push(...child.events);
|
|
52
|
+
gatherHidden(child.children, bubbled, visible);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const renderNode = (node, depth, showFull, lines) => {
|
|
57
|
+
lines.push(indent(depth) + frameLabel(node));
|
|
58
|
+
const childDepth = depth + 1;
|
|
59
|
+
if (showFull) {
|
|
60
|
+
for (const e of node.events)
|
|
61
|
+
lines.push(indent(childDepth) + formatEventLine(e));
|
|
62
|
+
for (const s of node.storage)
|
|
63
|
+
lines.push(indent(childDepth) + storageLine(s));
|
|
64
|
+
const ret = returnLine(node.return);
|
|
65
|
+
if (ret)
|
|
66
|
+
lines.push(indent(childDepth) + ret);
|
|
67
|
+
for (const c of node.children)
|
|
68
|
+
renderNode(c, childDepth, showFull, lines);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// Level 3: own events + events bubbled from hidden descendants, then the
|
|
72
|
+
// nearest visible child frames.
|
|
73
|
+
const bubbled = [];
|
|
74
|
+
const visibleChildren = [];
|
|
75
|
+
gatherHidden(node.children, bubbled, visibleChildren);
|
|
76
|
+
for (const e of [...node.events, ...bubbled]) {
|
|
77
|
+
lines.push(indent(childDepth) + formatEventLine(e));
|
|
78
|
+
}
|
|
79
|
+
for (const c of visibleChildren)
|
|
80
|
+
renderNode(c, childDepth, showFull, lines);
|
|
81
|
+
};
|
|
82
|
+
const formatAbort = (abort) => {
|
|
83
|
+
const lines = [];
|
|
84
|
+
let header = colors.error(`${symbols.error} Aborted: code ${abort.code}`);
|
|
85
|
+
if (abort.sub_status !== null) {
|
|
86
|
+
header += colors.error(` (sub_status ${abort.sub_status})`);
|
|
87
|
+
}
|
|
88
|
+
if (abort.module !== null) {
|
|
89
|
+
header += colors.dim(` in ${shortenPath(abort.module)}`);
|
|
90
|
+
}
|
|
91
|
+
lines.push(header);
|
|
92
|
+
for (const entry of abort.stack) {
|
|
93
|
+
const mod = entry.module !== null ? shortenPath(entry.module) : "<unknown>";
|
|
94
|
+
const fn = entry.function ?? "<unknown>";
|
|
95
|
+
const off = entry.offset !== null ? ` @${entry.offset}` : "";
|
|
96
|
+
lines.push(" " + colors.error(`at ${mod}::${fn}${off}`));
|
|
97
|
+
}
|
|
98
|
+
return lines;
|
|
99
|
+
};
|
|
100
|
+
const formatFooter = (response, elapsedMs) => {
|
|
101
|
+
const status = response.success
|
|
102
|
+
? colors.success(`${symbols.success} Executed successfully`)
|
|
103
|
+
: colors.error(`${symbols.error} Aborted`);
|
|
104
|
+
const sep = colors.dim(" · ");
|
|
105
|
+
const gas = colors.dim(`gas_used: ${response.gas_used} octas`);
|
|
106
|
+
const timed = brightBlue(`traced in ${Math.round(elapsedMs)}ms`);
|
|
107
|
+
return `${status}${sep}${gas}${sep}${timed}`;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Pure formatter — turns a trace into display lines. Snapshot-testable without
|
|
111
|
+
* touching stdout. `level` is the verbosity level (2..4); per-frame `gas` is in
|
|
112
|
+
* internal VM units while the footer `gas_used` is in octas (never mixed).
|
|
113
|
+
*/
|
|
114
|
+
export function formatTraceLines(response, level, elapsedMs) {
|
|
115
|
+
const lines = [];
|
|
116
|
+
if (level <= 2) {
|
|
117
|
+
const events = [];
|
|
118
|
+
collectEvents(response.root, events);
|
|
119
|
+
if (events.length === 0) {
|
|
120
|
+
lines.push(colors.dim("(no events emitted)"));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
lines.push(colors.bold("Events"));
|
|
124
|
+
for (const { event } of events)
|
|
125
|
+
lines.push(" " + formatEventLine(event));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
// Aborts always show the full tree so the failing frame is visible.
|
|
130
|
+
const showFull = level >= 4 || !response.success;
|
|
131
|
+
lines.push(colors.bold("Trace"));
|
|
132
|
+
renderNode(response.root, 0, showFull, lines);
|
|
133
|
+
}
|
|
134
|
+
if (!response.success && response.abort) {
|
|
135
|
+
lines.push("");
|
|
136
|
+
lines.push(...formatAbort(response.abort));
|
|
137
|
+
}
|
|
138
|
+
lines.push("");
|
|
139
|
+
lines.push(formatFooter(response, elapsedMs));
|
|
140
|
+
return lines;
|
|
141
|
+
}
|
|
142
|
+
/** Render a trace to the terminal. Wraps {@link formatTraceLines}. */
|
|
143
|
+
export function renderTrace(response, opts) {
|
|
144
|
+
logger.newline();
|
|
145
|
+
for (const line of formatTraceLines(response, opts.level, opts.elapsedMs)) {
|
|
146
|
+
logger.plain(line);
|
|
147
|
+
}
|
|
148
|
+
logger.newline();
|
|
149
|
+
}
|
|
@@ -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.4.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",
|