movehat 0.0.1-alpha.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.
Files changed (170) hide show
  1. package/README.md +236 -0
  2. package/bin/movehat.js +21 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +93 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/commands/compile.d.ts +2 -0
  8. package/dist/commands/compile.d.ts.map +1 -0
  9. package/dist/commands/compile.js +71 -0
  10. package/dist/commands/compile.js.map +1 -0
  11. package/dist/commands/fork/create.d.ts +11 -0
  12. package/dist/commands/fork/create.d.ts.map +1 -0
  13. package/dist/commands/fork/create.js +56 -0
  14. package/dist/commands/fork/create.js.map +1 -0
  15. package/dist/commands/fork/fund.d.ts +12 -0
  16. package/dist/commands/fork/fund.d.ts.map +1 -0
  17. package/dist/commands/fork/fund.js +42 -0
  18. package/dist/commands/fork/fund.js.map +1 -0
  19. package/dist/commands/fork/list.d.ts +5 -0
  20. package/dist/commands/fork/list.d.ts.map +1 -0
  21. package/dist/commands/fork/list.js +61 -0
  22. package/dist/commands/fork/list.js.map +1 -0
  23. package/dist/commands/fork/serve.d.ts +10 -0
  24. package/dist/commands/fork/serve.d.ts.map +1 -0
  25. package/dist/commands/fork/serve.js +64 -0
  26. package/dist/commands/fork/serve.js.map +1 -0
  27. package/dist/commands/fork/view-resource.d.ts +11 -0
  28. package/dist/commands/fork/view-resource.d.ts.map +1 -0
  29. package/dist/commands/fork/view-resource.js +34 -0
  30. package/dist/commands/fork/view-resource.js.map +1 -0
  31. package/dist/commands/init.d.ts +2 -0
  32. package/dist/commands/init.d.ts.map +1 -0
  33. package/dist/commands/init.js +90 -0
  34. package/dist/commands/init.js.map +1 -0
  35. package/dist/commands/run.d.ts +2 -0
  36. package/dist/commands/run.d.ts.map +1 -0
  37. package/dist/commands/run.js +51 -0
  38. package/dist/commands/run.js.map +1 -0
  39. package/dist/commands/test.d.ts +2 -0
  40. package/dist/commands/test.d.ts.map +1 -0
  41. package/dist/commands/test.js +35 -0
  42. package/dist/commands/test.js.map +1 -0
  43. package/dist/core/config.d.ts +15 -0
  44. package/dist/core/config.d.ts.map +1 -0
  45. package/dist/core/config.js +121 -0
  46. package/dist/core/config.js.map +1 -0
  47. package/dist/core/contract.d.ts +20 -0
  48. package/dist/core/contract.d.ts.map +1 -0
  49. package/dist/core/contract.js +59 -0
  50. package/dist/core/contract.js.map +1 -0
  51. package/dist/core/deployments.d.ts +32 -0
  52. package/dist/core/deployments.d.ts.map +1 -0
  53. package/dist/core/deployments.js +122 -0
  54. package/dist/core/deployments.js.map +1 -0
  55. package/dist/core/shell.d.ts +25 -0
  56. package/dist/core/shell.d.ts.map +1 -0
  57. package/dist/core/shell.js +56 -0
  58. package/dist/core/shell.js.map +1 -0
  59. package/dist/errors.d.ts +12 -0
  60. package/dist/errors.d.ts.map +1 -0
  61. package/dist/errors.js +24 -0
  62. package/dist/errors.js.map +1 -0
  63. package/dist/fork/api.d.ts +33 -0
  64. package/dist/fork/api.d.ts.map +1 -0
  65. package/dist/fork/api.js +98 -0
  66. package/dist/fork/api.js.map +1 -0
  67. package/dist/fork/manager.d.ts +52 -0
  68. package/dist/fork/manager.d.ts.map +1 -0
  69. package/dist/fork/manager.js +221 -0
  70. package/dist/fork/manager.js.map +1 -0
  71. package/dist/fork/server.d.ts +55 -0
  72. package/dist/fork/server.d.ts.map +1 -0
  73. package/dist/fork/server.js +274 -0
  74. package/dist/fork/server.js.map +1 -0
  75. package/dist/fork/storage.d.ts +63 -0
  76. package/dist/fork/storage.d.ts.map +1 -0
  77. package/dist/fork/storage.js +183 -0
  78. package/dist/fork/storage.js.map +1 -0
  79. package/dist/fork/test.d.ts +75 -0
  80. package/dist/fork/test.d.ts.map +1 -0
  81. package/dist/fork/test.js +157 -0
  82. package/dist/fork/test.js.map +1 -0
  83. package/dist/helpers/assertions.d.ts +7 -0
  84. package/dist/helpers/assertions.d.ts.map +1 -0
  85. package/dist/helpers/assertions.js +17 -0
  86. package/dist/helpers/assertions.js.map +1 -0
  87. package/dist/helpers/banner.d.ts +3 -0
  88. package/dist/helpers/banner.d.ts.map +1 -0
  89. package/dist/helpers/banner.js +38 -0
  90. package/dist/helpers/banner.js.map +1 -0
  91. package/dist/helpers/index.d.ts +11 -0
  92. package/dist/helpers/index.d.ts.map +1 -0
  93. package/dist/helpers/index.js +7 -0
  94. package/dist/helpers/index.js.map +1 -0
  95. package/dist/helpers/setup.d.ts +10 -0
  96. package/dist/helpers/setup.d.ts.map +1 -0
  97. package/dist/helpers/setup.js +28 -0
  98. package/dist/helpers/setup.js.map +1 -0
  99. package/dist/index.d.ts +11 -0
  100. package/dist/index.d.ts.map +1 -0
  101. package/dist/index.js +12 -0
  102. package/dist/index.js.map +1 -0
  103. package/dist/runtime.d.ts +26 -0
  104. package/dist/runtime.d.ts.map +1 -0
  105. package/dist/runtime.js +247 -0
  106. package/dist/runtime.js.map +1 -0
  107. package/dist/templates/.env.example +9 -0
  108. package/dist/templates/.mocharc.json +8 -0
  109. package/dist/templates/README.md +92 -0
  110. package/dist/templates/move/Counter.move +64 -0
  111. package/dist/templates/move/Move.toml +16 -0
  112. package/dist/templates/movehat.config.ts +37 -0
  113. package/dist/templates/package.json +24 -0
  114. package/dist/templates/scripts/deploy-counter.ts +48 -0
  115. package/dist/templates/tests/Counter.test.ts +75 -0
  116. package/dist/templates/tsconfig.json +15 -0
  117. package/dist/templates/types/movehat.d.ts +104 -0
  118. package/dist/types/config.d.ts +35 -0
  119. package/dist/types/config.d.ts.map +1 -0
  120. package/dist/types/config.js +2 -0
  121. package/dist/types/config.js.map +1 -0
  122. package/dist/types/fork.d.ts +37 -0
  123. package/dist/types/fork.d.ts.map +1 -0
  124. package/dist/types/fork.js +5 -0
  125. package/dist/types/fork.js.map +1 -0
  126. package/dist/types/runtime.d.ts +28 -0
  127. package/dist/types/runtime.d.ts.map +1 -0
  128. package/dist/types/runtime.js +2 -0
  129. package/dist/types/runtime.js.map +1 -0
  130. package/package.json +66 -0
  131. package/src/cli.ts +106 -0
  132. package/src/commands/compile.ts +84 -0
  133. package/src/commands/fork/create.ts +70 -0
  134. package/src/commands/fork/fund.ts +57 -0
  135. package/src/commands/fork/list.ts +67 -0
  136. package/src/commands/fork/serve.ts +77 -0
  137. package/src/commands/fork/view-resource.ts +46 -0
  138. package/src/commands/init.ts +150 -0
  139. package/src/commands/run.ts +59 -0
  140. package/src/commands/test.ts +42 -0
  141. package/src/core/config.ts +151 -0
  142. package/src/core/contract.ts +97 -0
  143. package/src/core/deployments.ts +164 -0
  144. package/src/core/shell.ts +66 -0
  145. package/src/errors.ts +21 -0
  146. package/src/fork/api.ts +117 -0
  147. package/src/fork/manager.ts +264 -0
  148. package/src/fork/server.ts +311 -0
  149. package/src/fork/storage.ts +224 -0
  150. package/src/fork/test.ts +195 -0
  151. package/src/helpers/assertions.ts +29 -0
  152. package/src/helpers/banner.ts +47 -0
  153. package/src/helpers/index.ts +26 -0
  154. package/src/helpers/setup.ts +49 -0
  155. package/src/index.ts +17 -0
  156. package/src/runtime.ts +322 -0
  157. package/src/templates/.env.example +9 -0
  158. package/src/templates/.mocharc.json +8 -0
  159. package/src/templates/README.md +92 -0
  160. package/src/templates/move/Counter.move +64 -0
  161. package/src/templates/move/Move.toml +16 -0
  162. package/src/templates/movehat.config.ts +37 -0
  163. package/src/templates/package.json +24 -0
  164. package/src/templates/scripts/deploy-counter.ts +48 -0
  165. package/src/templates/tests/Counter.test.ts +75 -0
  166. package/src/templates/tsconfig.json +15 -0
  167. package/src/templates/types/movehat.d.ts +104 -0
  168. package/src/types/config.ts +36 -0
  169. package/src/types/fork.ts +41 -0
  170. package/src/types/runtime.ts +49 -0
@@ -0,0 +1,195 @@
1
+ import { execFile } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { join } from 'path';
4
+ import { existsSync } from 'fs';
5
+
6
+ const execFileAsync = promisify(execFile);
7
+
8
+ export interface SnapshotOptions {
9
+ path?: string;
10
+ name?: string;
11
+ }
12
+
13
+ export interface ForkInfo {
14
+ path: string;
15
+ networkVersion?: number;
16
+ nodeUrl?: string;
17
+ exists: boolean;
18
+ }
19
+
20
+ /**
21
+ * Create a snapshot (fork) of the current network state
22
+ * Useful for debugging test failures or inspecting state after tests
23
+ *
24
+ * @param options - Snapshot configuration
25
+ * @returns Path to the created snapshot
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * // In your test
30
+ * after(async () => {
31
+ * const snapshotPath = await snapshot({ name: 'after-counter-test' });
32
+ * console.log(`Snapshot saved to ${snapshotPath}`);
33
+ * });
34
+ * ```
35
+ */
36
+ export async function snapshot(options: SnapshotOptions = {}): Promise<string> {
37
+ const name = options.name || `snapshot-${Date.now()}`;
38
+ const snapshotPath = options.path || join(process.cwd(), '.movehat', 'snapshots', name);
39
+
40
+ console.log(`📸 Creating snapshot: ${name}...`);
41
+
42
+ try {
43
+ // Initialize fork/snapshot using aptos CLI
44
+ // Use execFile with argument array to prevent command injection
45
+ const { stdout, stderr } = await execFileAsync('aptos', [
46
+ 'move',
47
+ 'sim',
48
+ 'init',
49
+ '--path',
50
+ snapshotPath
51
+ ]);
52
+
53
+ if (stderr && !stderr.includes('Success')) {
54
+ throw new Error(stderr);
55
+ }
56
+
57
+ if (!existsSync(snapshotPath)) {
58
+ throw new Error('Snapshot directory was not created');
59
+ }
60
+
61
+ console.log(` ✓ Snapshot created at ${snapshotPath}`);
62
+ return snapshotPath;
63
+ } catch (error: any) {
64
+ throw new Error(`Failed to create snapshot: ${error.message}`);
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Get information about a fork/snapshot
70
+ *
71
+ * @param path - Path to the fork directory
72
+ * @returns Fork information
73
+ */
74
+ export async function getForkInfo(path: string): Promise<ForkInfo> {
75
+ const configPath = join(path, 'config.json');
76
+
77
+ if (!existsSync(configPath)) {
78
+ return {
79
+ path,
80
+ exists: false
81
+ };
82
+ }
83
+
84
+ try {
85
+ const fs = await import('fs/promises');
86
+ const configContent = await fs.readFile(configPath, 'utf-8');
87
+ const config = JSON.parse(configContent);
88
+
89
+ return {
90
+ path,
91
+ exists: true,
92
+ networkVersion: config.base?.Remote?.network_version,
93
+ nodeUrl: config.base?.Remote?.node_url
94
+ };
95
+ } catch (error) {
96
+ return {
97
+ path,
98
+ exists: false
99
+ };
100
+ }
101
+ }
102
+
103
+ /**
104
+ * View a resource from a fork/snapshot
105
+ * Useful for inspecting state without modifying it
106
+ *
107
+ * @param sessionPath - Path to the fork session
108
+ * @param account - Account address
109
+ * @param resourceType - Full resource type (e.g., '0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>')
110
+ * @returns Resource data
111
+ *
112
+ * @example
113
+ * ```typescript
114
+ * const balance = await viewForkResource(
115
+ * '.movehat/snapshots/test-snapshot',
116
+ * '0x123...',
117
+ * '0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>'
118
+ * );
119
+ * console.log(`Balance: ${balance.coin.value}`);
120
+ * ```
121
+ */
122
+ export async function viewForkResource(
123
+ sessionPath: string,
124
+ account: string,
125
+ resourceType: string
126
+ ): Promise<any> {
127
+ try {
128
+ // Use execFile with argument array to prevent command injection
129
+ const { stdout } = await execFileAsync('aptos', [
130
+ 'move',
131
+ 'sim',
132
+ 'view-resource',
133
+ '--session',
134
+ sessionPath,
135
+ '--account',
136
+ account,
137
+ '--resource',
138
+ resourceType
139
+ ]);
140
+
141
+ const result = JSON.parse(stdout);
142
+
143
+ if (result.Error) {
144
+ throw new Error(result.Error);
145
+ }
146
+
147
+ return result.Result;
148
+ } catch (error: any) {
149
+ throw new Error(`Failed to view resource: ${error.message}`);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Compare a resource between current network state and a fork
155
+ * Useful for verifying state changes after tests
156
+ *
157
+ * @param forkPath - Path to the fork
158
+ * @param account - Account address
159
+ * @param resourceType - Resource type to compare
160
+ * @param currentValue - Current value from network (pass from your test)
161
+ * @returns Comparison result
162
+ */
163
+ export async function compareForkState(
164
+ forkPath: string,
165
+ account: string,
166
+ resourceType: string,
167
+ currentValue: any
168
+ ): Promise<{ fork: any; current: any; changed: boolean }> {
169
+ const forkValue = await viewForkResource(forkPath, account, resourceType);
170
+
171
+ return {
172
+ fork: forkValue,
173
+ current: currentValue,
174
+ changed: JSON.stringify(forkValue) !== JSON.stringify(currentValue)
175
+ };
176
+ }
177
+
178
+ /**
179
+ * List all snapshots in the project
180
+ * @returns Array of snapshot paths
181
+ */
182
+ export async function listSnapshots(): Promise<string[]> {
183
+ const snapshotsDir = join(process.cwd(), '.movehat', 'snapshots');
184
+
185
+ if (!existsSync(snapshotsDir)) {
186
+ return [];
187
+ }
188
+
189
+ const fs = await import('fs/promises');
190
+ const entries = await fs.readdir(snapshotsDir, { withFileTypes: true });
191
+
192
+ return entries
193
+ .filter(entry => entry.isDirectory())
194
+ .map(entry => join(snapshotsDir, entry.name));
195
+ }
@@ -0,0 +1,29 @@
1
+ import { type TransactionResult } from "../core/contract.js";
2
+
3
+ /**
4
+ * Assert that a transaction was successful
5
+ */
6
+ export function assertTransactionSuccess(result: TransactionResult): void {
7
+ if (!result.success) {
8
+ throw new Error(
9
+ `Transaction failed with status: ${result.vm_status}\nHash: ${result.hash}`
10
+ );
11
+ }
12
+ }
13
+
14
+ export function assertTransactionFailed(
15
+ result: TransactionResult,
16
+ expectedError?: string
17
+ ): void {
18
+ if (result.success) {
19
+ throw new Error(
20
+ `Transaction was expected to fail but succeeded.\nHash: ${result.hash}`
21
+ );
22
+ }
23
+
24
+ if(expectedError && !result.vm_status.includes(expectedError)) {
25
+ throw new Error(
26
+ `Transaction failed with unexpected error.\nExpected to include: ${expectedError}\nActual status: ${result.vm_status}\nHash: ${result.hash}`
27
+ );
28
+ }
29
+ }
@@ -0,0 +1,47 @@
1
+ type Rgb = [number, number, number];
2
+
3
+ // Warm yellow-to-amber palette for a subtle gradient.
4
+ const gradientPalette: Rgb[] = [
5
+ [255, 239, 150],
6
+ [255, 223, 88],
7
+ [255, 207, 64],
8
+ [255, 181, 45],
9
+ [255, 160, 30],
10
+ ];
11
+
12
+ const bannerLines = [
13
+ " ███╗ ███╗ ██████╗ ██╗ ██╗███████╗██╗ ██╗ █████╗ ████████╗",
14
+ " ████╗ ████║██╔═══██╗██║ ██║██╔════╝██║ ██║██╔══██╗╚══██╔══╝",
15
+ " ██╔████╔██║██║ ██║██║ ██║█████╗ ███████║███████║ ██║ ",
16
+ " ██║╚██╔╝██║██║ ██║╚██╗ ██╔╝██╔══╝ ██╔══██║██╔══██║ ██║ ",
17
+ " ██║ ╚═╝ ██║╚██████╔╝ ╚████╔╝ ███████╗██║ ██║██║ ██║ ██║ ",
18
+ " ╚═╝ ╚═╝ ╚═════╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ",
19
+ ];
20
+
21
+ const reset = "\x1b[0m";
22
+
23
+ const shouldColorize = () => process.env.NO_COLOR === undefined && Boolean(process.stdout.isTTY);
24
+
25
+ const toAnsi = ([r, g, b]: Rgb) => `\x1b[38;2;${r};${g};${b}m`;
26
+
27
+ const applyGradient = (line: string, offset: number) => {
28
+ let painted = "";
29
+ for (let i = 0; i < line.length; i++) {
30
+ const color = gradientPalette[(i + offset) % gradientPalette.length];
31
+ painted += `${toAnsi(color)}${line[i]}`;
32
+ }
33
+ return painted;
34
+ };
35
+
36
+ export const renderMovehatBanner = () => {
37
+ if (!shouldColorize()) {
38
+ return bannerLines.join("\n");
39
+ }
40
+
41
+ const coloredLines = bannerLines.map((line, idx) => applyGradient(line, idx * 2));
42
+ return `${coloredLines.join("\n")}${reset}`;
43
+ };
44
+
45
+ export const printMovehatBanner = () => {
46
+ console.log(renderMovehatBanner());
47
+ };
@@ -0,0 +1,26 @@
1
+ // Re-export all helpers
2
+ export { setupTestEnvironment, createTestAccount } from "./setup.js";
3
+ export type { TestEnvironment } from "./setup.js";
4
+ export { MoveContract, getContract } from "../core/contract.js";
5
+ export type { TransactionResult } from "../core/contract.js";
6
+ export {
7
+ assertTransactionSuccess,
8
+ assertTransactionFailed,
9
+ } from "./assertions.js";
10
+ export {
11
+ saveDeployment,
12
+ loadDeployment,
13
+ getAllDeployments,
14
+ getDeployedAddress,
15
+ } from "../core/deployments.js";
16
+ export type { DeploymentInfo } from "../core/deployments.js";
17
+ export {
18
+ snapshot,
19
+ getForkInfo,
20
+ viewForkResource,
21
+ compareForkState,
22
+ listSnapshots,
23
+ } from "../fork/test.js";
24
+ export type { SnapshotOptions, ForkInfo } from "../fork/test.js";
25
+
26
+ export type { MovehatConfig } from "../types/config.js";
@@ -0,0 +1,49 @@
1
+ import {
2
+ Account,
3
+ Aptos,
4
+ AptosConfig,
5
+ Ed25519PrivateKey,
6
+ Network,
7
+ } from "@aptos-labs/ts-sdk";
8
+ import { loadUserConfig, resolveNetworkConfig } from "../core/config.js";
9
+ import { MovehatConfig } from "../types/config.js";
10
+
11
+ export interface TestEnvironment {
12
+ aptos: Aptos;
13
+ account: Account;
14
+ config: MovehatConfig;
15
+ }
16
+
17
+ export async function setupTestEnvironment(networkName?: string): Promise<TestEnvironment> {
18
+ // Load and resolve config for selected network
19
+ const userConfig = await loadUserConfig();
20
+ const network = networkName || process.env.MH_CLI_NETWORK;
21
+ const config = await resolveNetworkConfig(userConfig, network);
22
+
23
+ const aptosConfig = new AptosConfig({
24
+ network: config.network as Network,
25
+ fullnode: config.rpc,
26
+ });
27
+
28
+ const aptos = new Aptos(aptosConfig);
29
+
30
+ const privateKey = new Ed25519PrivateKey(config.privateKey);
31
+ const account = Account.fromPrivateKey({ privateKey });
32
+
33
+ console.log(`✅ Test environment ready`);
34
+ console.log(` Account: ${account.accountAddress.toString()}`);
35
+ console.log(` Network: ${config.network}`);
36
+ console.log(` RPC: ${config.rpc}\n`);
37
+
38
+ return {
39
+ aptos,
40
+ account,
41
+ config,
42
+ }
43
+ }
44
+
45
+ export function createTestAccount(): Account {
46
+ return Account.generate();
47
+ }
48
+
49
+
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ // Export all helpers for end users
2
+ export * from "./helpers/index.js";
3
+ export type { MovehatConfig } from "./types/config.js";
4
+
5
+ // Export Movehat Runtime Environment
6
+ export { initRuntime, getRuntime, getMovehat, mh } from "./runtime.js";
7
+ export type { MovehatRuntime, NetworkInfo } from "./types/runtime.js";
8
+
9
+ // Export Fork system
10
+ export { ForkManager } from "./fork/manager.js";
11
+ export { MovementApiClient } from "./fork/api.js";
12
+ export { ForkStorage } from "./fork/storage.js";
13
+ export { ForkServer } from "./fork/server.js";
14
+ export type { ForkMetadata, AccountState, LedgerInfo, AccountData, AccountResource } from "./types/fork.js";
15
+
16
+ // Export custom errors
17
+ export { ModuleAlreadyDeployedError } from "./errors.js";
package/src/runtime.ts ADDED
@@ -0,0 +1,322 @@
1
+ import {
2
+ Account,
3
+ Aptos,
4
+ AptosConfig,
5
+ Ed25519PrivateKey,
6
+ Network,
7
+ } from "@aptos-labs/ts-sdk";
8
+ import { MovehatRuntime, NetworkInfo } from "./types/runtime.js";
9
+ import { MovehatUserConfig } from "./types/config.js";
10
+ import { loadUserConfig, resolveNetworkConfig } from "./core/config.js";
11
+ import { getContract, MoveContract } from "./core/contract.js";
12
+ import {
13
+ saveDeployment,
14
+ loadDeployment,
15
+ getAllDeployments,
16
+ getDeployedAddress,
17
+ DeploymentInfo,
18
+ validateSafeName,
19
+ } from "./core/deployments.js";
20
+ import { ModuleAlreadyDeployedError } from "./errors.js";
21
+
22
+ let cachedRuntime: MovehatRuntime | null = null;
23
+
24
+ export interface InitRuntimeOptions {
25
+ network?: string;
26
+ accountIndex?: number;
27
+ configOverride?: Partial<MovehatUserConfig>;
28
+ }
29
+
30
+ /**
31
+ * Initialize the Movehat Runtime Environment
32
+ * This function loads the configuration and creates the runtime context
33
+ */
34
+ export async function initRuntime(
35
+ options: InitRuntimeOptions = {}
36
+ ): Promise<MovehatRuntime> {
37
+ // Load user config from movehat.config.ts
38
+ const userConfig = await loadUserConfig();
39
+
40
+ // Apply config override if provided
41
+ const mergedUserConfig: MovehatUserConfig = options.configOverride
42
+ ? { ...userConfig, ...options.configOverride }
43
+ : userConfig;
44
+
45
+ // Resolve configuration for selected network
46
+ const config = await resolveNetworkConfig(mergedUserConfig, options.network);
47
+
48
+ // Setup Aptos client
49
+ const aptosConfig = new AptosConfig({
50
+ network: config.network as Network,
51
+ fullnode: config.rpc,
52
+ });
53
+ const aptos = new Aptos(aptosConfig);
54
+
55
+ // Setup accounts
56
+ const accountIndex = options.accountIndex || 0;
57
+ const accounts: Account[] = config.allAccounts.map((pk) => {
58
+ const privateKey = new Ed25519PrivateKey(pk);
59
+ return Account.fromPrivateKey({ privateKey });
60
+ });
61
+
62
+ // Primary account (accounts[0] or selected index)
63
+ const account = accounts[accountIndex];
64
+ if (!account) {
65
+ throw new Error(`Account index ${accountIndex} not found. Only ${accounts.length} accounts configured.`);
66
+ }
67
+
68
+ // Update config.account with derived address
69
+ config.account = account.accountAddress.toString();
70
+
71
+ // Network info
72
+ const network: NetworkInfo = {
73
+ name: config.network,
74
+ rpc: config.rpc,
75
+ };
76
+
77
+ // Helper functions
78
+ const getContractHelper = (address: string, moduleName: string): MoveContract => {
79
+ return getContract(aptos, address, moduleName);
80
+ };
81
+
82
+ const deployContract = async (
83
+ moduleName: string,
84
+ options?: {
85
+ packageDir?: string;
86
+ }
87
+ ): Promise<DeploymentInfo> => {
88
+ // Validate moduleName early
89
+ validateSafeName(moduleName, "module");
90
+
91
+ const { exec } = await import("child_process");
92
+ const { promisify } = await import("util");
93
+ const { existsSync, mkdirSync, writeFileSync, chmodSync } = await import("fs");
94
+ const { join } = await import("path");
95
+ const { homedir } = await import("os");
96
+ const yaml = await import("js-yaml");
97
+ const { validateAndEscapePath, validateAndEscapeProfile } = await import("./core/shell.js");
98
+ const execAsync = promisify(exec);
99
+
100
+ // Check if --redeploy flag was passed via CLI
101
+ const forceRedeploy = process.env.MH_CLI_REDEPLOY === 'true';
102
+
103
+ // Check if already deployed
104
+ const existingDeployment = loadDeployment(config.network, moduleName);
105
+ if (existingDeployment && !forceRedeploy) {
106
+ // Build detailed error message with all deployment info
107
+ const errorDetails = [
108
+ `Module "${moduleName}" is already deployed on ${config.network}`,
109
+ `Address: ${existingDeployment.address}`,
110
+ `Deployed at: ${new Date(existingDeployment.timestamp).toLocaleString()}`,
111
+ existingDeployment.txHash ? `Transaction: ${existingDeployment.txHash}` : null,
112
+ `\nTo redeploy, run with the --redeploy flag:`,
113
+ `movehat run <script> --network ${config.network} --redeploy`,
114
+ ].filter(Boolean).join('\n');
115
+
116
+ // Log formatted error message for user
117
+ const formattedMessage = [
118
+ `\n❌ Module "${moduleName}" is already deployed on ${config.network}`,
119
+ ` Address: ${existingDeployment.address}`,
120
+ ` Deployed at: ${new Date(existingDeployment.timestamp).toLocaleString()}`,
121
+ existingDeployment.txHash ? ` Transaction: ${existingDeployment.txHash}` : null,
122
+ `\n💡 To redeploy, run with the --redeploy flag:`,
123
+ ` movehat run <script> --network ${config.network} --redeploy\n`,
124
+ ].filter(Boolean).join('\n');
125
+
126
+ console.error(formattedMessage);
127
+
128
+ // Throw custom error with complete context for programmatic handling
129
+ throw new ModuleAlreadyDeployedError(
130
+ errorDetails,
131
+ moduleName,
132
+ config.network,
133
+ existingDeployment.address,
134
+ existingDeployment.timestamp,
135
+ existingDeployment.txHash
136
+ );
137
+ }
138
+
139
+ if (forceRedeploy && existingDeployment) {
140
+ console.log(`🔄 Redeploying module "${moduleName}" on ${config.network}...`);
141
+ }
142
+
143
+ const dir = options?.packageDir || config.moveDir;
144
+ const profile = config.profile || "default";
145
+
146
+ // Validate and escape to prevent command injection
147
+ const safeDir = validateAndEscapePath(dir, "package directory");
148
+ const safeProfile = validateAndEscapeProfile(profile);
149
+
150
+ console.log(`📦 Publishing module "${moduleName}" from ${dir}...`);
151
+
152
+ try {
153
+ // Ensure Movement CLI config exists
154
+ const aptosConfigDir = join(homedir(), ".aptos");
155
+ const aptosConfigPath = join(aptosConfigDir, "config.yaml");
156
+
157
+ if (!existsSync(aptosConfigPath)) {
158
+ console.log("⚙️ Creating Movement CLI configuration...");
159
+ if (!existsSync(aptosConfigDir)) {
160
+ mkdirSync(aptosConfigDir, { recursive: true });
161
+ }
162
+
163
+ // Create minimal config.yaml using js-yaml to prevent YAML injection
164
+ const configData = {
165
+ profiles: {
166
+ [profile]: {
167
+ private_key: config.privateKey,
168
+ public_key: account.publicKey.toString(),
169
+ account: account.accountAddress.toString(),
170
+ rest_url: config.rpc,
171
+ },
172
+ },
173
+ };
174
+ const configContent = yaml.dump(configData);
175
+ writeFileSync(aptosConfigPath, configContent, "utf-8");
176
+
177
+ // Restrict file permissions to owner only (600) for security
178
+ // This prevents other users from reading the private key
179
+ try {
180
+ chmodSync(aptosConfigPath, 0o600);
181
+ } catch (error) {
182
+ // chmod may fail on Windows, but that's okay
183
+ // Windows has different permission model (ACLs)
184
+ console.warn("⚠️ Could not set file permissions (this is normal on Windows)");
185
+ }
186
+ }
187
+
188
+ // Build first
189
+ console.log("🔨 Building package...");
190
+ const buildCmd = `movement move build --package-dir ${safeDir}`;
191
+ const { stdout: buildOut } = await execAsync(buildCmd);
192
+ if (buildOut) console.log(buildOut.trim());
193
+
194
+ // Publish
195
+ console.log("📤 Publishing to blockchain...");
196
+ const publishCmd = `movement move publish --profile ${safeProfile} --package-dir ${safeDir} --assume-yes`;
197
+ const { stdout: publishOut } = await execAsync(publishCmd);
198
+ if (publishOut) console.log(publishOut.trim());
199
+
200
+ // Extract transaction hash from output
201
+ // Look for patterns like "Transaction hash: 0x..." or "Txn: 0x..." or just a 64-char hex
202
+ // The regex tries to match with context first, then falls back to any 64-char hex
203
+ let txHash: string | undefined;
204
+ const txHashMatchWithContext = publishOut.match(/(?:transaction\s*(?:hash)?|txn\s*(?:hash)?|hash):\s*(0x[a-fA-F0-9]{64})\b/i);
205
+ if (txHashMatchWithContext) {
206
+ txHash = txHashMatchWithContext[1];
207
+ } else {
208
+ // Fallback: try to find any 64-char hex string (exactly, not more)
209
+ const txHashMatch = publishOut.match(/\b(0x[a-fA-F0-9]{64})\b/);
210
+ if (txHashMatch) {
211
+ txHash = txHashMatch[1];
212
+ }
213
+ }
214
+
215
+ console.log(`✅ Module published successfully!`);
216
+
217
+ // Create deployment info
218
+ const deployment: DeploymentInfo = {
219
+ address: account.accountAddress.toString(),
220
+ moduleName,
221
+ network: config.network,
222
+ deployer: account.accountAddress.toString(),
223
+ timestamp: Date.now(),
224
+ txHash,
225
+ };
226
+
227
+ // Save deployment
228
+ saveDeployment(deployment);
229
+
230
+ return deployment;
231
+ } catch (error: any) {
232
+ console.error(`❌ Failed to publish module: ${error.message}`);
233
+ throw error;
234
+ }
235
+ };
236
+
237
+ const getDeployment = (moduleName: string): DeploymentInfo | null => {
238
+ return loadDeployment(config.network, moduleName);
239
+ };
240
+
241
+ const getDeployments = (): Record<string, DeploymentInfo> => {
242
+ return getAllDeployments(config.network);
243
+ };
244
+
245
+ const getDeploymentAddress = (moduleName: string): string | null => {
246
+ return getDeployedAddress(config.network, moduleName);
247
+ };
248
+
249
+ const createAccount = (): Account => {
250
+ return Account.generate();
251
+ };
252
+
253
+ const getAccountHelper = (privateKeyHex: string): Account => {
254
+ const pk = new Ed25519PrivateKey(privateKeyHex);
255
+ return Account.fromPrivateKey({ privateKey: pk });
256
+ };
257
+
258
+ const getAccountByIndex = (index: number): Account => {
259
+ if (index < 0 || index >= accounts.length) {
260
+ throw new Error(`Account index ${index} out of range. Available accounts: 0-${accounts.length - 1}`);
261
+ }
262
+ return accounts[index];
263
+ };
264
+
265
+ const switchNetwork = async (networkName: string): Promise<void> => {
266
+ // Clear cache and reinitialize with new network
267
+ cachedRuntime = null;
268
+ await initRuntime({ ...options, network: networkName });
269
+ };
270
+
271
+ // Build runtime object
272
+ const runtime: MovehatRuntime = {
273
+ config,
274
+ network,
275
+ aptos,
276
+ account,
277
+ accounts,
278
+ getContract: getContractHelper,
279
+ deployContract,
280
+ getDeployment,
281
+ getDeployments,
282
+ getDeploymentAddress,
283
+ createAccount,
284
+ getAccount: getAccountHelper,
285
+ getAccountByIndex,
286
+ switchNetwork,
287
+ };
288
+
289
+ cachedRuntime = runtime;
290
+ return runtime;
291
+ }
292
+
293
+ /**
294
+ * Get the current Movehat Runtime Environment
295
+ * Throws error if runtime hasn't been initialized
296
+ */
297
+ export function getRuntime(): MovehatRuntime {
298
+ if (!cachedRuntime) {
299
+ throw new Error(
300
+ "Movehat Runtime not initialized. Call initRuntime() first or use getMovehat()."
301
+ );
302
+ }
303
+ return cachedRuntime;
304
+ }
305
+
306
+ /**
307
+ * Get or initialize the Movehat Runtime Environment
308
+ * This is a convenience function that initializes if needed
309
+ */
310
+ export async function getMovehat(): Promise<MovehatRuntime> {
311
+ if (cachedRuntime) {
312
+ return cachedRuntime;
313
+ }
314
+ return initRuntime();
315
+ }
316
+
317
+ // Export a default instance getter for convenience
318
+ export const mh = {
319
+ get runtime() {
320
+ return getRuntime();
321
+ },
322
+ };
@@ -0,0 +1,9 @@
1
+ # Private Key (used for all networks - Hardhat-style)
2
+ # This is your wallet's private key that will be used to deploy and interact with contracts
3
+ PRIVATE_KEY=your_private_key_here
4
+
5
+ # Optional: Override RPC URL (useful for custom endpoints)
6
+ # MOVEMENT_RPC_URL=https://custom-testnet.movementnetwork.xyz/v1
7
+
8
+ # Optional: Override default network from config
9
+ # MH_DEFAULT_NETWORK=testnet